From 8c82f175add224e01a51d34d4ace83741302e0a7 Mon Sep 17 00:00:00 2001 From: VladKovtun99 Date: Wed, 30 Jul 2025 10:38:59 +0200 Subject: [PATCH 1/5] Refactored Orleans.Identity. Removed unnecessary parts, leaved grain authorization and authentication. Implemented extension method for grain to get user claims from request context --- .../AuthenticationHandlerExtensions.cs | 56 -- .../OrleansContextMiddlewareExtensions.cs | 30 - .../Extensions/OrleansIdentityExtensions.cs | 31 + .../Middlewares/OrleansContextMiddleware.cs | 19 +- .../OrleansIdentityAuthenticationHandler.cs | 73 -- .../Middlewares/SignalRAuthorizationFilter.cs | 45 ++ .../Extensions/GrainExtensions.cs | 43 ++ .../Constants/SessionGrainConstants.cs | 6 - .../Constants/TokenGrainConstants.cs | 9 - .../GrainAuthorizationIncomingFilter.cs | 77 +- .../Grains/SessionGrain.cs | 340 --------- .../Grains/Tokens/Base/TokenGrain.cs | 115 --- .../Tokens/CodeVerificationTokenGrain.cs | 43 -- .../Tokens/EmailVerificationTokenGrain.cs | 43 -- .../Grains/Tokens/MagicLinkTokenGrain.cs | 41 - .../Grains/{Interfaces => }/IUserGrain.cs | 3 +- .../Grains/Interfaces/IModeratorGrain.cs | 9 - .../Cluster/Grains/Interfaces/IPublicGrain.cs | 9 - .../UserGrains/IBaseTestUserGrain.cs | 14 - .../Interfaces/UserGrains/ICodeUserGrain.cs | 9 - .../Interfaces/UserGrains/ILinkUserGrain.cs | 8 - .../Interfaces/UserGrains/ISocialUserGrain.cs | 7 - .../Cluster/Grains/ModeratorGrain.cs | 27 - .../Cluster/Grains/PublicGrain.cs | 31 - .../Cluster/Grains/UserGrain.cs | 53 +- .../Grains/UserGrains/BaseTestUserGrain.cs | 32 - .../Grains/UserGrains/CodeUserGrain.cs | 28 - .../Grains/UserGrains/LinkUserGrain.cs | 30 - .../Grains/UserGrains/SocialUserGrain.cs | 29 - .../ShortLifetimeSiloConfiguration.cs | 31 - .../ShortLifetimeSiloTestApp.cs | 55 -- .../ControllerTests.cs | 268 ------- .../JwtControllerTests.cs | 203 +++++ .../JwtSignalRTests.cs | 151 ++++ .../ManagedCode.Orleans.Identity.Tests.csproj | 2 + .../SessionGrainReminderTests.cs | 149 ---- .../SessionGrainTests.cs | 703 ------------------ .../SomeTest.cs | 49 -- .../TestApp/Controllers/AdminController.cs | 36 - .../TestApp/Controllers/AuthController.cs | 61 +- .../Controllers/ModeratorController.cs | 43 -- .../TestApp/Controllers/PublicController.cs | 50 -- .../TestApp/Controllers/TestController.cs | 44 -- .../TestApp/Controllers/UserController.cs | 84 ++- .../TestApp/HttpHostProgram.cs | 71 +- .../TestApp/Models/TestUser.cs | 33 - .../TestApp/Services/IJwtService.cs | 9 + .../TestApp/Services/JwtService.cs | 69 ++ .../TestApp/TestAuthorizeHub.cs | 44 +- .../TokenGrainTests/BaseTokenGrainTests.cs | 188 ----- .../CodeVerificationTokenGrainTests.cs | 14 - .../EmailVerificationTokenGrainTests.cs | 15 - .../MagicLinkTokenGrainTests.cs | 14 - .../BaseTokenGrainReminderTests.cs | 62 -- .../EmailVerificationGrainReminderTests.cs | 15 - .../UserGrainTests/BaseUserGrainsTests.cs | 191 ----- .../CodeVerificationTokenUserGrainTests.cs | 16 - .../EmailVerificationUserGrainTests.cs | 17 - .../MagicLinkTokenUserGrainTests.cs | 15 - README.md | 135 +++- 60 files changed, 968 insertions(+), 3129 deletions(-) delete mode 100644 ManagedCode.Orleans.Identity.Client/Extensions/AuthenticationHandlerExtensions.cs delete mode 100644 ManagedCode.Orleans.Identity.Client/Extensions/OrleansContextMiddlewareExtensions.cs create mode 100644 ManagedCode.Orleans.Identity.Client/Extensions/OrleansIdentityExtensions.cs delete mode 100644 ManagedCode.Orleans.Identity.Client/Middlewares/OrleansIdentityAuthenticationHandler.cs create mode 100644 ManagedCode.Orleans.Identity.Client/Middlewares/SignalRAuthorizationFilter.cs create mode 100644 ManagedCode.Orleans.Identity.Core/Extensions/GrainExtensions.cs delete mode 100644 ManagedCode.Orleans.Identity.Server/Constants/SessionGrainConstants.cs delete mode 100644 ManagedCode.Orleans.Identity.Server/Constants/TokenGrainConstants.cs delete mode 100644 ManagedCode.Orleans.Identity.Server/Grains/SessionGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Server/Grains/Tokens/Base/TokenGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Server/Grains/Tokens/CodeVerificationTokenGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Server/Grains/Tokens/EmailVerificationTokenGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Server/Grains/Tokens/MagicLinkTokenGrain.cs rename ManagedCode.Orleans.Identity.Tests/Cluster/Grains/{Interfaces => }/IUserGrain.cs (68%) delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/IModeratorGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/IPublicGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/IBaseTestUserGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/ICodeUserGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/ILinkUserGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/ISocialUserGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/Grains/ModeratorGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/Grains/PublicGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/BaseTestUserGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/CodeUserGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/LinkUserGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/SocialUserGrain.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/ShortLifetimeSilo/ShortLifetimeSiloConfiguration.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/Cluster/ShortLifetimeSilo/ShortLifetimeSiloTestApp.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/ControllerTests.cs create mode 100644 ManagedCode.Orleans.Identity.Tests/JwtControllerTests.cs create mode 100644 ManagedCode.Orleans.Identity.Tests/JwtSignalRTests.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/SessionGrainReminderTests.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/SessionGrainTests.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/SomeTest.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/AdminController.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/ModeratorController.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/PublicController.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/TestController.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TestApp/Models/TestUser.cs create mode 100644 ManagedCode.Orleans.Identity.Tests/TestApp/Services/IJwtService.cs create mode 100644 ManagedCode.Orleans.Identity.Tests/TestApp/Services/JwtService.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TokenGrainTests/BaseTokenGrainTests.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TokenGrainTests/CodeVerificationTokenGrainTests.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TokenGrainTests/EmailVerificationTokenGrainTests.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TokenGrainTests/MagicLinkTokenGrainTests.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TokenGrainTests/RemainderTests/BaseTokenGrainReminderTests.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TokenGrainTests/RemainderTests/EmailVerificationGrainReminderTests.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/BaseUserGrainsTests.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/CodeVerificationTokenUserGrainTests.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/EmailVerificationUserGrainTests.cs delete mode 100644 ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/MagicLinkTokenUserGrainTests.cs diff --git a/ManagedCode.Orleans.Identity.Client/Extensions/AuthenticationHandlerExtensions.cs b/ManagedCode.Orleans.Identity.Client/Extensions/AuthenticationHandlerExtensions.cs deleted file mode 100644 index 5ac5933..0000000 --- a/ManagedCode.Orleans.Identity.Client/Extensions/AuthenticationHandlerExtensions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using ManagedCode.Orleans.Identity.Client.Middlewares; -using ManagedCode.Orleans.Identity.Core.Constants; -using ManagedCode.Orleans.Identity.Core.Options; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; -using Microsoft.Extensions.DependencyInjection; - -namespace ManagedCode.Orleans.Identity.Client.Extensions; - -public static class AuthenticationHandlerExtensions -{ - /// - /// Use Orleans.Identity authentication as default authentication scheme - /// - /// - /// Options for working with session - public static void AddOrleansIdentity(this IServiceCollection services, Action sessionOption) - { - var option = new SessionOption(); - sessionOption?.Invoke(option); - AddOrleansIdentity(services, option); - } - - /// - /// Use Orleans.Identity authentication as default authentication scheme - /// - /// - /// Options for working with session - /// - public static void AddOrleansIdentity(this IServiceCollection services, SessionOption? sessionOption = null, Action? authenticationBuilder = null) - - { - sessionOption ??= new SessionOption(); - - // Add custom authentication and authorization - services.AddScoped(); - services.AddScoped(); - - services.AddAuthentication(options => - { - options.DefaultScheme = OrleansIdentityConstants.AUTHENTICATION_TYPE; - }) - .AddScheme(OrleansIdentityConstants.AUTHENTICATION_TYPE, op => - { - - }); - services.AddAuthorization(options => - { - var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(OrleansIdentityConstants.AUTHENTICATION_TYPE); - defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser(); - options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build(); - }); - - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Client/Extensions/OrleansContextMiddlewareExtensions.cs b/ManagedCode.Orleans.Identity.Client/Extensions/OrleansContextMiddlewareExtensions.cs deleted file mode 100644 index e3fa98c..0000000 --- a/ManagedCode.Orleans.Identity.Client/Extensions/OrleansContextMiddlewareExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using ManagedCode.Orleans.Identity.Client.Middlewares; -using Microsoft.AspNetCore.Builder; - -namespace ManagedCode.Orleans.Identity.Client.Extensions; - -public static class OrleansContextMiddlewareExtensions -{ - /// - /// Use middleware to set claims and session id for request to cluster - /// - /// - /// - public static IApplicationBuilder UseOrleansIdentity(this IApplicationBuilder builder) - { - return builder.UseMiddleware() - .UseMiddleware(); - } - - /// - /// Use middleware to set claims and session id for request to cluster, this method includes UseAuthentication and UseAuthorization middlewares - /// - /// - /// - public static IApplicationBuilder UseAuthenticationAndOrleansIdentity(this IApplicationBuilder builder) - { - builder.UseAuthentication(); - builder.UseAuthorization(); - return builder.UseOrleansIdentity(); - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Client/Extensions/OrleansIdentityExtensions.cs b/ManagedCode.Orleans.Identity.Client/Extensions/OrleansIdentityExtensions.cs new file mode 100644 index 0000000..e107870 --- /dev/null +++ b/ManagedCode.Orleans.Identity.Client/Extensions/OrleansIdentityExtensions.cs @@ -0,0 +1,31 @@ +using ManagedCode.Orleans.Identity.Client.Middlewares; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; + +namespace ManagedCode.Orleans.Identity.Client.Extensions; + +public static class OrleansIdentityExtensions +{ + public static IServiceCollection AddOrleansIdentity(this IServiceCollection services) + { + // Add middleware for storing claims in RequestContext + services.AddScoped(); + + // Add SignalR filter for authorization + services.AddSignalR(options => + { + options.AddFilter(); + }); + + return services; + } + + public static IApplicationBuilder UseOrleansIdentity(this IApplicationBuilder app) + { + // Add middleware to the pipeline + app.UseMiddleware(); + + return app; + } +} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Client/Middlewares/OrleansContextMiddleware.cs b/ManagedCode.Orleans.Identity.Client/Middlewares/OrleansContextMiddleware.cs index 4c37d70..443f966 100644 --- a/ManagedCode.Orleans.Identity.Client/Middlewares/OrleansContextMiddleware.cs +++ b/ManagedCode.Orleans.Identity.Client/Middlewares/OrleansContextMiddleware.cs @@ -1,14 +1,25 @@ +using System.Security.Claims; using System.Threading.Tasks; -using ManagedCode.Orleans.Identity.Core.Extensions; +using System.Linq; using Microsoft.AspNetCore.Http; +using Orleans.Runtime; namespace ManagedCode.Orleans.Identity.Client.Middlewares; -public class OrleansContextMiddleware(RequestDelegate next) +public class OrleansContextMiddleware : IMiddleware { - public async Task InvokeAsync(HttpContext context) + public async Task InvokeAsync(HttpContext context, RequestDelegate next) { - context.User.SetOrleansContext(); + if (context.User.Identity?.IsAuthenticated == true) + { + // Store user claims in Orleans RequestContext as serializable dictionary + // Group claims by type to handle multiple values (like roles) + var claims = context.User.Claims + .GroupBy(c => c.Type) + .ToDictionary(g => g.Key, g => string.Join(",", g.Select(c => c.Value))); + RequestContext.Set("UserClaims", claims); + } + await next(context); } } \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Client/Middlewares/OrleansIdentityAuthenticationHandler.cs b/ManagedCode.Orleans.Identity.Client/Middlewares/OrleansIdentityAuthenticationHandler.cs deleted file mode 100644 index d28656c..0000000 --- a/ManagedCode.Orleans.Identity.Client/Middlewares/OrleansIdentityAuthenticationHandler.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Security.Claims; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using ManagedCode.Orleans.Identity.Core.Constants; -using ManagedCode.Orleans.Identity.Core.Extensions; -using ManagedCode.Orleans.Identity.Core.Interfaces; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Orleans; - -namespace ManagedCode.Orleans.Identity.Client.Middlewares; - -public class OrleansIdentityAuthenticationHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock, - IClusterClient client) : AuthenticationHandler(options, logger, encoder, clock) -{ - protected override async Task HandleAuthenticateAsync() - { - string sessionId; - if (!Request.Headers.TryGetValue(OrleansIdentityConstants.AUTH_TOKEN, out var values)) - { - if (Request.Headers.TryGetValue("Authorization", out var jwt)) - { - sessionId = jwt.ToString().Replace("Bearer", "").Trim(); - } - else if (Request.Query.TryGetValue(OrleansIdentityConstants.AUTH_TOKEN, out var queryValues)) - { - sessionId = queryValues.ToString().Trim(); - } - else - { - return AuthenticateResult.NoResult(); - } - } - else - { - sessionId = values.ToString().Trim(); - } - - if (string.IsNullOrEmpty(sessionId)) - { - return AuthenticateResult.NoResult(); - } - - try - { - var sessionGrain = client.GetGrain(sessionId); - var result = await sessionGrain.ValidateAndGetClaimsAsync(); - - if (result.IsSuccess) - { - ClaimsIdentity claimsIdentity = new(OrleansIdentityConstants.AUTHENTICATION_TYPE); - - foreach (var claim in result.Value!) - claimsIdentity.ParseClaims(claim.Key, claim.Value); - - var ticket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name); - return AuthenticateResult.Success(ticket); - } - } - catch (Exception e) - { - Logger.LogError(e, "HandleAuthenticateAsync Validation"); - } - - return AuthenticateResult.Fail($"Unauthorized request. SessionId: {sessionId};"); - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Client/Middlewares/SignalRAuthorizationFilter.cs b/ManagedCode.Orleans.Identity.Client/Middlewares/SignalRAuthorizationFilter.cs new file mode 100644 index 0000000..02fb67d --- /dev/null +++ b/ManagedCode.Orleans.Identity.Client/Middlewares/SignalRAuthorizationFilter.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using System.Linq; +using Microsoft.AspNetCore.SignalR; +using Orleans.Runtime; + +namespace ManagedCode.Orleans.Identity.Client.Middlewares; + +public class SignalRAuthorizationFilter : IHubFilter +{ + public async ValueTask InvokeMethodAsync( + HubInvocationContext invocationContext, + Func> next) + { + if (invocationContext.Context.User?.Identity?.IsAuthenticated == true) + { + // Store user claims in Orleans RequestContext as serializable dictionary + // Group claims by type to handle multiple values (like roles) + var claims = invocationContext.Context.User.Claims + .GroupBy(c => c.Type) + .ToDictionary(g => g.Key, g => string.Join(",", g.Select(c => c.Value))); + RequestContext.Set("UserClaims", claims); + } + + return await next(invocationContext); + } + + public Task OnConnectedAsync(HubLifetimeContext context, Func next) + { + if (context.Context.User?.Identity?.IsAuthenticated == true) + { + var claims = context.Context.User.Claims + .GroupBy(c => c.Type) + .ToDictionary(g => g.Key, g => string.Join(",", g.Select(c => c.Value))); + RequestContext.Set("UserClaims", claims); + } + + return next(context); + } + + public Task OnDisconnectedAsync(HubLifetimeContext context, Exception? exception, Func next) + { + return next(context, exception); + } +} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Core/Extensions/GrainExtensions.cs b/ManagedCode.Orleans.Identity.Core/Extensions/GrainExtensions.cs new file mode 100644 index 0000000..382f607 --- /dev/null +++ b/ManagedCode.Orleans.Identity.Core/Extensions/GrainExtensions.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Orleans; +using Orleans.Runtime; + +namespace ManagedCode.Orleans.Identity.Core.Extensions; + +public static class GrainExtensions +{ + public static ClaimsPrincipal GetCurrentUser(this Grain grain) + { + var requestContext = RequestContext.Get("UserClaims"); + if (requestContext is Dictionary claimsDict) + { + var claims = new List(); + foreach (var kvp in claimsDict) + { + // Handle comma-separated values (like roles) + var values = kvp.Value.Split(',', StringSplitOptions.RemoveEmptyEntries); + foreach (var value in values) + { + claims.Add(new Claim(kvp.Key, value.Trim())); + } + } + var identity = new ClaimsIdentity(claims, "JWT"); + return new ClaimsPrincipal(identity); + } + + return new ClaimsPrincipal(new ClaimsIdentity()); + } + + public static bool IsAuthorizationFailed(this Grain grain) + { + return RequestContext.Get("AuthorizationFailed") is bool failed && failed; + } + + public static string? GetAuthorizationMessage(this Grain grain) + { + return RequestContext.Get("AuthorizationMessage") as string; + } +} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Server/Constants/SessionGrainConstants.cs b/ManagedCode.Orleans.Identity.Server/Constants/SessionGrainConstants.cs deleted file mode 100644 index d61aa81..0000000 --- a/ManagedCode.Orleans.Identity.Server/Constants/SessionGrainConstants.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ManagedCode.Orleans.Identity.Server.Constants; - -public static class SessionGrainConstants -{ - public const string SESSION_LIFETIME_REMINDER_NAME = "SessionExpiredReminder"; -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Server/Constants/TokenGrainConstants.cs b/ManagedCode.Orleans.Identity.Server/Constants/TokenGrainConstants.cs deleted file mode 100644 index d18e638..0000000 --- a/ManagedCode.Orleans.Identity.Server/Constants/TokenGrainConstants.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ManagedCode.Orleans.Identity.Server.Constants -{ - public static class TokenGrainConstants - { - public const string EMAIL_VERIFICATION_TOKEN_REMINDER_NAME = "DisableEmailVerificationToken"; - public const string MAGIC_LINK_TOKEN_REMINDER_NAME = "DisableMagicLinkToken"; - public const string CODE_VERIFICATION_TOKEN_REMINDER_NAME = "DisableVerificationCodeToken"; - } -} diff --git a/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs b/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs index 31cc02b..bd87e93 100644 --- a/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs +++ b/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs @@ -2,79 +2,100 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Security.Claims; using System.Threading.Tasks; -using ManagedCode.Orleans.Identity.Core.Extensions; -using ManagedCode.Orleans.Identity.Core.Interfaces; using Microsoft.AspNetCore.Authorization; using Orleans; +using Orleans.Runtime; namespace ManagedCode.Orleans.Identity.Server.GrainCallFilter; -public class GrainAuthorizationIncomingFilter(IClusterClient client, IGrainFactory grainFactory) : IIncomingGrainCallFilter +public class GrainAuthorizationIncomingFilter : IIncomingGrainCallFilter { - private readonly IClusterClient _client = client; - public async Task Invoke(IIncomingGrainCallContext context) { if (IsGrainAuthorized(context.ImplementationMethod, out var attributes)) { - var isSessionExists = await IsAuthorized(); - if (isSessionExists) + var user = GetUserFromRequestContext(); + var isAuthorized = false; + + if (user?.Identity?.IsAuthenticated == true) { if (attributes.All(attribute => string.IsNullOrWhiteSpace(attribute.Roles))) { - await context.Invoke(); - return; + isAuthorized = true; } - - var roles = this.GetRoles().ToHashSet(); - foreach (var attribute in attributes) + else { - var intersect = attribute.Roles?.Split(',') ?? Array.Empty(); - if (intersect.Any(role => roles.Contains(role))) + var userRoles = user.FindAll(ClaimTypes.Role).Select(c => c.Value).ToHashSet(); + + foreach (var attribute in attributes) { - await context.Invoke(); - return; + var requiredRoles = attribute.Roles?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(); + if (requiredRoles.Any(role => userRoles.Contains(role.Trim()))) + { + isAuthorized = true; + break; + } } } } - throw new UnauthorizedAccessException(); + + if (!isAuthorized) + { + // Set authorization failure flag instead of throwing exception + RequestContext.Set("AuthorizationFailed", true); + RequestContext.Set("AuthorizationMessage", "Access denied. User is not authenticated or does not have required roles."); + } } await context.Invoke(); - } - private async Task IsAuthorized() + private static ClaimsPrincipal? GetUserFromRequestContext() { - var sessionId = this.GetSessionId(); - if (string.IsNullOrWhiteSpace(sessionId) is false) + var requestContext = RequestContext.Get("UserClaims"); + if (requestContext is Dictionary claimsDict) { - var sessionGrain = grainFactory.GetGrain(sessionId); - var result = await sessionGrain.ValidateAndGetClaimsAsync(); - return result.IsSuccess; + var claims = new List(); + foreach (var kvp in claimsDict) + { + // Handle comma-separated values (like roles) + var values = kvp.Value.Split(',', StringSplitOptions.RemoveEmptyEntries); + foreach (var value in values) + { + claims.Add(new Claim(kvp.Key, value.Trim())); + } + } + var identity = new ClaimsIdentity(claims, "JWT"); + return new ClaimsPrincipal(identity); } - return false; + + return null; } private static bool IsGrainAuthorized(MemberInfo methodInfo, out List attributes) { attributes = new List(); - // for method + // Check for AllowAnonymous on method if (Attribute.IsDefined(methodInfo, typeof(AllowAnonymousAttribute))) { return false; } + // Check for Authorize on class if (methodInfo.DeclaringType != null && Attribute.IsDefined(methodInfo.DeclaringType, typeof(AuthorizeAttribute))) { - attributes.AddRange(Attribute.GetCustomAttributes(methodInfo.DeclaringType, typeof(AuthorizeAttribute)).Select(s => (AuthorizeAttribute)s)); + attributes.AddRange(Attribute.GetCustomAttributes(methodInfo.DeclaringType, typeof(AuthorizeAttribute)) + .Cast()); } + // Check for Authorize on method if (Attribute.IsDefined(methodInfo, typeof(AuthorizeAttribute))) { - attributes.AddRange(Attribute.GetCustomAttributes(methodInfo, typeof(AuthorizeAttribute)).Select(s => (AuthorizeAttribute)s)); + attributes.AddRange(Attribute.GetCustomAttributes(methodInfo, typeof(AuthorizeAttribute)) + .Cast()); return true; } diff --git a/ManagedCode.Orleans.Identity.Server/Grains/SessionGrain.cs b/ManagedCode.Orleans.Identity.Server/Grains/SessionGrain.cs deleted file mode 100644 index 67c13a1..0000000 --- a/ManagedCode.Orleans.Identity.Server/Grains/SessionGrain.cs +++ /dev/null @@ -1,340 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ManagedCode.Communication; -using ManagedCode.Orleans.Identity.Core.Constants; -using ManagedCode.Orleans.Identity.Core.Enums; -using ManagedCode.Orleans.Identity.Core.Interfaces; -using ManagedCode.Orleans.Identity.Core.Models; -using ManagedCode.Orleans.Identity.Core.Options; -using ManagedCode.Orleans.Identity.Server.Constants; -using Orleans; -using Orleans.Runtime; - -namespace ManagedCode.Orleans.Identity.Server.Grains; - -public class SessionGrain : Grain, ISessionGrain, IRemindable -{ - private readonly SessionOption _sessionOption; - private readonly IPersistentState _sessionState; - - public SessionGrain( - [PersistentState("sessions", OrleansIdentityConstants.SESSION_STORAGE)] - IPersistentState sessionState, - SessionOption sessionOption) - { - _sessionState = sessionState; - _sessionOption = sessionOption; - } - - public ValueTask> GetSessionAsync() - { - if (_sessionState.RecordExists is false) - { - return Result.Fail().AsValueTask(); - } - - var result = GetSessionModel(); - - return Result.Succeed(result).AsValueTask(); - } - - public override async Task OnActivateAsync(CancellationToken cancellationToken) - { - if (_sessionState.RecordExists) - { - var timePassed = DateTime.UtcNow - _sessionState.State.LastAccess; - if (timePassed >= _sessionOption.SessionLifetime) - { - await CloseAsync(); - } - } - } - - public async Task> CreateAsync(CreateSessionModel model) - { - var date = DateTime.UtcNow; - - _sessionState.State = new SessionModel(this.GetPrimaryKeyString()) - { - IsActive = true, - UserGrainId = model.UserGrainId, - UserData = model.UserData ?? new (), - Status = SessionStatus.Active, - CreatedDate = date, - LastAccess = date - }; - - _sessionState.State.UserData.Add(OrleansIdentityConstants.SESSION_ID_CLAIM_NAME, new HashSet() - { - this.GetPrimaryKeyString() - }); - - await _sessionState.WriteStateAsync(); - - var result = GetSessionModel(); - - return Result.Succeed(result); - } - - public async Task ReceiveReminder(string reminderName, TickStatus status) - { - if (reminderName == SessionGrainConstants.SESSION_LIFETIME_REMINDER_NAME) - { - await CloseAsync(); - await UnregisterReminder(); - } - } - - public ValueTask>>> ValidateAndGetClaimsAsync() - { - if (_sessionState.RecordExists is false) - { - DeactivateOnIdle(); - return Result>>.Fail().AsValueTask(); - } - - if (_sessionState.State.IsActive is false) - { - DeactivateOnIdle(); - return Result>>.Fail().AsValueTask(); - } - - _sessionState.State.LastAccess = DateTime.UtcNow; - - return Result>>.Succeed(_sessionState.State.UserData.ToImmutableDictionary()).AsValueTask(); - } - - public async Task CloseAsync() - { - if (_sessionState.RecordExists is false) - { - return Result.Fail(); - } - - if (_sessionOption.ClearStateOnClose) - { - await _sessionState.ClearStateAsync(); - await UnregisterReminder(); - return Result.Succeed(); - } - - _sessionState.State.ClosedDate = DateTime.UtcNow; - _sessionState.State.Status = SessionStatus.Closed; - _sessionState.State.IsActive = false; - - await _sessionState.WriteStateAsync(); - - await UnregisterReminder(); - return Result.Succeed(); - } - - public ValueTask PauseSessionAsync() - { - if (_sessionState.RecordExists is false) - { - DeactivateOnIdle(); - return Result.Fail().AsValueTask(); - } - - _sessionState.State.LastAccess = DateTime.UtcNow; - _sessionState.State.Status = SessionStatus.Paused; - - return Result.Succeed().AsValueTask(); - } - - public ValueTask ResumeSessionAsync() - { - if (_sessionState.RecordExists is false) - { - DeactivateOnIdle(); - return Result.Fail().AsValueTask(); - } - - _sessionState.State.LastAccess = DateTime.UtcNow; - _sessionState.State.Status = SessionStatus.Active; - - return Result.Succeed().AsValueTask(); - } - - public ValueTask AddProperty(string key, string value) - { - if (_sessionState.RecordExists is false) - { - DeactivateOnIdle(); - return Result.Fail().AsValueTask(); - } - - if (_sessionState.State.UserData.ContainsKey(key)) - { - return Result.Fail().AsValueTask(); - } - - _sessionState.State.UserData[key] = new HashSet { value }; - return Result.Succeed().AsValueTask(); - } - - public ValueTask AddProperty(string key, List values) - { - if (_sessionState.RecordExists is false) - { - DeactivateOnIdle(); - return Result.Fail().AsValueTask(); - } - - if (_sessionState.State.UserData.ContainsKey(key)) - { - return Result.Fail().AsValueTask(); - } - - _sessionState.State.UserData[key] = values.ToHashSet(); - return Result.Succeed().AsValueTask(); - } - - public ValueTask ReplaceProperty(string key, string value) - { - if (_sessionState.RecordExists is false) - { - DeactivateOnIdle(); - return Result.Fail().AsValueTask(); - } - - if (_sessionState.State.UserData.ContainsKey(key) is false) - { - return Result.Fail().AsValueTask(); - } - - _sessionState.State.UserData[key] = new HashSet { value }; - return Result.Succeed().AsValueTask(); - } - - public ValueTask ReplaceProperty(string key, List values) - { - if (_sessionState.RecordExists is false) - { - DeactivateOnIdle(); - return Result.Fail().AsValueTask(); - } - - if (_sessionState.State.UserData.ContainsKey(key) is false) - { - return Result.Fail().AsValueTask(); - } - - _sessionState.State.UserData[key] = values.ToHashSet(); - return Result.Succeed().AsValueTask(); - } - - public ValueTask AddValueToProperty(string key, string value) - { - if (_sessionState.RecordExists is false) - { - DeactivateOnIdle(); - return Result.Fail().AsValueTask(); - } - - if (_sessionState.State.UserData.TryGetValue(key, out var hashset)) - { - return hashset.Add(value) ? Result.Succeed().AsValueTask() : Result.Fail().AsValueTask(); - } - - return Result.Fail().AsValueTask(); - } - - public ValueTask RemoveProperty(string key) - { - if (_sessionState.RecordExists is false) - { - DeactivateOnIdle(); - return Result.Fail().AsValueTask(); - } - - if (_sessionState.State.UserData.ContainsKey(key)) - { - _sessionState.State.UserData.Remove(key); - return Result.Succeed().AsValueTask(); - } - - return Result.Fail().AsValueTask(); - } - - public ValueTask RemoveValueFromProperty(string key, string value) - { - if (_sessionState.RecordExists is false) - { - DeactivateOnIdle(); - return Result.Fail().AsValueTask(); - } - - if (_sessionState.State.UserData.TryGetValue(key, out var hashset)) - { - return hashset.Remove(value) ? Result.Succeed().AsValueTask() : Result.Fail().AsValueTask(); - } - - return Result.Fail().AsValueTask(); - } - - public ValueTask ClearUserData() - { - if (_sessionState.RecordExists is false) - { - DeactivateOnIdle(); - return Result.Fail().AsValueTask(); - } - - _sessionState.State.UserData.Clear(); - return Result.Succeed().AsValueTask(); - } - - public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken) - { - if (_sessionState.RecordExists && _sessionState.State.Status != SessionStatus.Closed) - { - await _sessionState.WriteStateAsync(); - await SetSessionLifetimeReminder(); - } - else - { - await _sessionState.ClearStateAsync(); - } - } - - private SessionModel GetSessionModel() - { - return new SessionModel(_sessionState.State.Id) - { - IsActive = _sessionState.State.IsActive, - ClosedDate = _sessionState.State.ClosedDate, - CreatedDate = _sessionState.State.CreatedDate, - LastAccess = _sessionState.State.LastAccess, - Status = _sessionState.State.Status, - UserData = _sessionState.State.UserData - }; - } - - private async ValueTask SetSessionLifetimeReminder() - { - if (_sessionOption.SessionLifetime == TimeSpan.Zero || - _sessionOption.SessionLifetime < TimeSpan.FromMinutes(1)) - { - return; - } - - var reminder = await this.GetReminder(SessionGrainConstants.SESSION_LIFETIME_REMINDER_NAME); - if(reminder is not null) - return; - - await this.RegisterOrUpdateReminder(SessionGrainConstants.SESSION_LIFETIME_REMINDER_NAME, - _sessionOption.SessionLifetime, _sessionOption.SessionLifetime); - } - - private async ValueTask UnregisterReminder() - { - var reminder = await this.GetReminder(SessionGrainConstants.SESSION_LIFETIME_REMINDER_NAME); - if (reminder is not null) - await this.UnregisterReminder(reminder); - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Server/Grains/Tokens/Base/TokenGrain.cs b/ManagedCode.Orleans.Identity.Server/Grains/Tokens/Base/TokenGrain.cs deleted file mode 100644 index a913ea6..0000000 --- a/ManagedCode.Orleans.Identity.Server/Grains/Tokens/Base/TokenGrain.cs +++ /dev/null @@ -1,115 +0,0 @@ -using ManagedCode.Communication; -using ManagedCode.Orleans.Identity.Core.Extensions; -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Core.Models; -using ManagedCode.Orleans.Identity.Server.Constants; -using Orleans; -using Orleans.Runtime; -using System; -using System.Threading.Tasks; -using ManagedCode.Orleans.Identity.Core.Constants; - -namespace ManagedCode.Orleans.Identity.Server.Grains.Tokens.Base -{ - public abstract class TokenGrain : Grain, IBaseTokenGrain, IRemindable - { - private readonly string _reminderName; - protected readonly IPersistentState TokenState; - - private IDisposable? _timerReference; - - protected TokenGrain(IPersistentState tokenState, string reminderName) - { - TokenState = tokenState; - _reminderName = reminderName; - } - - protected abstract ValueTask CallUserGrainOnTokenExpired(); - protected abstract ValueTask CallUserGrainOnTokenValid(); - - private async Task OnTimerTicked(object args) - { - _timerReference?.Dispose(); - if (TokenState.RecordExists is false) - { - DeactivateOnIdle(); - return; - } - - await CallUserGrainOnTokenExpired(); - await TokenState.ClearStateAsync(); - DeactivateOnIdle(); - } - - public async ValueTask CreateAsync(CreateTokenModel createModel) - { - if (createModel.IsModelValid() is false) - { - DeactivateOnIdle(); - return Result.Fail(); - } - - TokenState.State = new TokenModel - { - Lifetime = createModel.Lifetime, - UserGrainId = createModel.UserGrainId, - Value = createModel.Value, - }; - - await TokenState.WriteStateAsync(); - - if (createModel.Lifetime < TimeSpan.FromMinutes(1)) - { - _timerReference = RegisterTimer(OnTimerTicked, null, createModel.Lifetime, createModel.Lifetime); - } - else - { - await this.RegisterOrUpdateReminder(_reminderName, TokenState.State.Lifetime, TokenState.State.Lifetime); - } - - return Result.Succeed(); - } - - - public async ValueTask VerifyAsync() - { - if (TokenState.RecordExists is false) - { - DeactivateOnIdle(); - return Result.Fail(); - } - - await CallUserGrainOnTokenValid(); - return Result.Succeed(); - } - - public ValueTask> GetTokenAsync() - { - if (TokenState.RecordExists is false) - { - DeactivateOnIdle(); - return Result.Fail().AsValueTask(); - } - - return Result.Succeed(TokenState.State).AsValueTask(); - } - - public async Task ReceiveReminder(string reminderName, TickStatus status) - { - if (TokenState.RecordExists is false) - { - DeactivateOnIdle(); - await this.UnregisterReminder(await this.GetReminder(reminderName)); - return; - } - - if (reminderName == _reminderName) - { - await CallUserGrainOnTokenExpired(); - await this.UnregisterReminder(await this.GetReminder(reminderName)); - await TokenState.ClearStateAsync(); - DeactivateOnIdle(); - } - } - } -} diff --git a/ManagedCode.Orleans.Identity.Server/Grains/Tokens/CodeVerificationTokenGrain.cs b/ManagedCode.Orleans.Identity.Server/Grains/Tokens/CodeVerificationTokenGrain.cs deleted file mode 100644 index ea69dbe..0000000 --- a/ManagedCode.Orleans.Identity.Server/Grains/Tokens/CodeVerificationTokenGrain.cs +++ /dev/null @@ -1,43 +0,0 @@ -using ManagedCode.Communication; -using ManagedCode.Orleans.Identity.Core.Constants; -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Core.Interfaces.UserGrains; -using ManagedCode.Orleans.Identity.Core.Models; -using ManagedCode.Orleans.Identity.Server.Constants; -using ManagedCode.Orleans.Identity.Server.Grains.Tokens.Base; -using Orleans.Runtime; -using System.Threading.Tasks; - -namespace ManagedCode.Orleans.Identity.Server.Grains.Tokens -{ - public class CodeVerificationTokenGrain : TokenGrain, ICodeVerificationTokenGrain - { - public CodeVerificationTokenGrain( - [PersistentState("verificationCodeToken", OrleansIdentityConstants.SESSION_STORAGE)] - IPersistentState tokenState) : base(tokenState, TokenGrainConstants.EMAIL_VERIFICATION_TOKEN_REMINDER_NAME) - { - } - - protected override async ValueTask CallUserGrainOnTokenExpired() - { - if (TokenState.State.UserGrainId.IsDefault) - { - return; - } - var parseResult = TokenState.State.UserGrainId.Key.ToString(); - var userGrain = GrainFactory.GetGrain(parseResult); - await userGrain.CodeVerificationTokenExpiredAsync(TokenState.State.Value); - } - - protected override async ValueTask CallUserGrainOnTokenValid() - { - if (TokenState.State.UserGrainId.IsDefault) - { - return; - } - var parseResult = TokenState.State.UserGrainId.Key.ToString(); - var userGrain = GrainFactory.GetGrain(parseResult); - await userGrain.CodeVerificationTokenValidAsync(TokenState.State.Value); - } - } -} diff --git a/ManagedCode.Orleans.Identity.Server/Grains/Tokens/EmailVerificationTokenGrain.cs b/ManagedCode.Orleans.Identity.Server/Grains/Tokens/EmailVerificationTokenGrain.cs deleted file mode 100644 index 6084328..0000000 --- a/ManagedCode.Orleans.Identity.Server/Grains/Tokens/EmailVerificationTokenGrain.cs +++ /dev/null @@ -1,43 +0,0 @@ - -using ManagedCode.Orleans.Identity.Core.Constants; -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Core.Interfaces.UserGrains; -using ManagedCode.Orleans.Identity.Core.Models; -using ManagedCode.Orleans.Identity.Server.Constants; -using ManagedCode.Orleans.Identity.Server.Grains.Tokens.Base; -using Orleans; -using Orleans.Runtime; -using System.Threading.Tasks; - -namespace ManagedCode.Orleans.Identity.Server.Grains.Tokens; - -public class EmailVerificationTokenGrain : TokenGrain, IEmailVerificationTokenGrain -{ - public EmailVerificationTokenGrain( - [PersistentState("emailVerificationToken", OrleansIdentityConstants.SESSION_STORAGE)] - IPersistentState tokenState) : base(tokenState, TokenGrainConstants.EMAIL_VERIFICATION_TOKEN_REMINDER_NAME) - { - } - - protected override async ValueTask CallUserGrainOnTokenExpired() - { - if (TokenState.State.UserGrainId.IsDefault) - { - return; - } - var parseResult = TokenState.State.UserGrainId.Key.ToString(); - var userGrain = GrainFactory.GetGrain(parseResult); - await userGrain.EmailVerificationTokenExpiredAsync(TokenState.State.Value); - } - - protected override async ValueTask CallUserGrainOnTokenValid() - { - if (TokenState.State.UserGrainId.IsDefault) - { - return; - } - var parseResult = TokenState.State.UserGrainId.Key.ToString(); - var userGrain = GrainFactory.GetGrain(parseResult); - await userGrain.EmailVerificationTokenValidAsync(TokenState.State.Value); - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Server/Grains/Tokens/MagicLinkTokenGrain.cs b/ManagedCode.Orleans.Identity.Server/Grains/Tokens/MagicLinkTokenGrain.cs deleted file mode 100644 index 1ec9e5d..0000000 --- a/ManagedCode.Orleans.Identity.Server/Grains/Tokens/MagicLinkTokenGrain.cs +++ /dev/null @@ -1,41 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Constants; -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Core.Interfaces.UserGrains; -using ManagedCode.Orleans.Identity.Core.Models; -using ManagedCode.Orleans.Identity.Server.Constants; -using ManagedCode.Orleans.Identity.Server.Grains.Tokens.Base; -using Orleans.Runtime; -using System.Threading.Tasks; - -namespace ManagedCode.Orleans.Identity.Server.Grains.Tokens; - -public class MagicLinkTokenGrain : TokenGrain, IMagicLinkTokenGrain -{ - public MagicLinkTokenGrain( - [PersistentState("magicLinkToken", OrleansIdentityConstants.SESSION_STORAGE)] - IPersistentState tokenState) : base(tokenState, TokenGrainConstants.MAGIC_LINK_TOKEN_REMINDER_NAME) - { - } - - protected override async ValueTask CallUserGrainOnTokenExpired() - { - if (TokenState.State.UserGrainId.IsDefault) - { - return; - } - var parseResult = TokenState.State.UserGrainId.Key.ToString(); - var userGrain = GrainFactory.GetGrain(parseResult); - await userGrain.MagicLinkTokenExpiredAsync(TokenState.State.Value); - } - - protected override async ValueTask CallUserGrainOnTokenValid() - { - if (TokenState.State.UserGrainId.IsDefault) - { - return; - } - var parseResult = TokenState.State.UserGrainId.Key.ToString(); - var userGrain = GrainFactory.GetGrain(parseResult); - await userGrain.MagicLinkTokenValidAsync(TokenState.State.Value); - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/IUserGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/IUserGrain.cs similarity index 68% rename from ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/IUserGrain.cs rename to ManagedCode.Orleans.Identity.Tests/Cluster/Grains/IUserGrain.cs index be33422..e302f5c 100644 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/IUserGrain.cs +++ b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/IUserGrain.cs @@ -1,9 +1,10 @@ -namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces; +namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains; public interface IUserGrain : IGrainWithStringKey { Task GetUser(); Task BanUser(); + Task GetAdminInfo(); Task GetPublicInfo(); Task ModifyUser(); Task AddToList(); diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/IModeratorGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/IModeratorGrain.cs deleted file mode 100644 index 068a04a..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/IModeratorGrain.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces -{ - public interface IModeratorGrain : IGrainWithStringKey - { - Task GetInfo(); - Task GetModerators(); - Task GetPublicInformation(); - } -} diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/IPublicGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/IPublicGrain.cs deleted file mode 100644 index 944cda4..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/IPublicGrain.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces; - -public interface IPublicGrain : IGrainWithStringKey -{ - Task CommonMethod(); - Task AuthorizedMethod(); - Task AdminOnly(); - Task ModeratorOnly(); -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/IBaseTestUserGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/IBaseTestUserGrain.cs deleted file mode 100644 index 0f72c4d..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/IBaseTestUserGrain.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ManagedCode.Communication; - -namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces.UserGrains; - -public interface IBaseTestUserGrain : IGrainWithStringKey -{ - ValueTask> IsTokenExpired(); - - ValueTask> IsTokenValid(); - - ValueTask> IsTokenInvalid(); - - ValueTask> GetTokenValue(); -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/ICodeUserGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/ICodeUserGrain.cs deleted file mode 100644 index adcad7c..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/ICodeUserGrain.cs +++ /dev/null @@ -1,9 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Interfaces.UserGrains; - -namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces.UserGrains -{ - public interface ICodeUserGrain : IBaseTestUserGrain, ICodeVerificationTokenUserGrain - { - - } -} diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/ILinkUserGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/ILinkUserGrain.cs deleted file mode 100644 index 5e84ef1..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/ILinkUserGrain.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Interfaces.UserGrains; - -namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces.UserGrains -{ - public interface ILinkUserGrain : IBaseTestUserGrain, IMagicLinkTokenUserGrain - { - } -} diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/ISocialUserGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/ISocialUserGrain.cs deleted file mode 100644 index 85da88d..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/Interfaces/UserGrains/ISocialUserGrain.cs +++ /dev/null @@ -1,7 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Interfaces.UserGrains; - -namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces.UserGrains; - -public interface ISocialUserGrain : IEmailVerificationTokenUserGrain, IBaseTestUserGrain -{ -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/ModeratorGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/ModeratorGrain.cs deleted file mode 100644 index a340200..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/ModeratorGrain.cs +++ /dev/null @@ -1,27 +0,0 @@ -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces; -using ManagedCode.Orleans.Identity.Tests.Constants; -using Microsoft.AspNetCore.Authorization; - -namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains -{ - [Authorize(Roles = TestRoles.MODERATOR)] - public class ModeratorGrain : Grain, IModeratorGrain - { - public Task GetInfo() - { - return Task.FromResult("info"); - } - - [Authorize] - public Task GetModerators() - { - return Task.FromResult("moderators"); - } - - [AllowAnonymous] - public Task GetPublicInformation() - { - return Task.FromResult("public info"); - } - } -} diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/PublicGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/PublicGrain.cs deleted file mode 100644 index 2450d84..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/PublicGrain.cs +++ /dev/null @@ -1,31 +0,0 @@ -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces; -using ManagedCode.Orleans.Identity.Tests.Constants; -using Microsoft.AspNetCore.Authorization; - -namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains; - -public class PublicGrain : Grain, IPublicGrain -{ - public Task CommonMethod() - { - return Task.FromResult("common"); - } - - [Authorize] - public Task AuthorizedMethod() - { - return Task.FromResult("authorized"); - } - - [Authorize(Roles = TestRoles.ADMIN)] - public Task AdminOnly() - { - return Task.FromResult("admin only"); - } - - [Authorize(Roles = TestRoles.MODERATOR)] - public Task ModeratorOnly() - { - return Task.FromResult("moderator only"); - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrain.cs index 9c98ee5..4d86a14 100644 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrain.cs +++ b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrain.cs @@ -1,4 +1,4 @@ -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces; +using ManagedCode.Orleans.Identity.Core.Extensions; using ManagedCode.Orleans.Identity.Tests.Constants; using Microsoft.AspNetCore.Authorization; @@ -10,29 +10,70 @@ public class UserGrain : Grain, IUserGrain [Authorize] public Task GetUser() { - return Task.FromResult("user"); + if (this.IsAuthorizationFailed()) + { + throw new UnauthorizedAccessException(this.GetAuthorizationMessage() ?? "Access denied"); + } + + var user = this.GetCurrentUser(); + var username = user.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "Unknown"; + return Task.FromResult($"Hello, {username}!"); } [Authorize(Roles = TestRoles.ADMIN)] public Task BanUser() { - return Task.FromResult("User is banned"); + if (this.IsAuthorizationFailed()) + { + throw new UnauthorizedAccessException(this.GetAuthorizationMessage() ?? "Access denied"); + } + + var user = this.GetCurrentUser(); + var username = user.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "Unknown"; + return Task.FromResult($"User {username} is banned"); + } + + [Authorize(Roles = TestRoles.ADMIN)] + public Task GetAdminInfo() + { + if (this.IsAuthorizationFailed()) + { + throw new UnauthorizedAccessException(this.GetAuthorizationMessage() ?? "Access denied"); + } + + var user = this.GetCurrentUser(); + var username = user.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "Unknown"; + return Task.FromResult($"Admin info for {username}: You have admin privileges"); } [AllowAnonymous] public Task GetPublicInfo() { - return Task.FromResult("public info"); + return Task.FromResult("This is public information - no authorization required"); } [Authorize(Roles = TestRoles.MODERATOR)] public Task ModifyUser() { - return Task.FromResult("user modified"); + if (this.IsAuthorizationFailed()) + { + throw new UnauthorizedAccessException(this.GetAuthorizationMessage() ?? "Access denied"); + } + + var user = this.GetCurrentUser(); + var username = user.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "Unknown"; + return Task.FromResult($"User {username} has been modified"); } public Task AddToList() { - return Task.FromResult("add to list"); + if (this.IsAuthorizationFailed()) + { + throw new UnauthorizedAccessException(this.GetAuthorizationMessage() ?? "Access denied"); + } + + var user = this.GetCurrentUser(); + var username = user.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "Unknown"; + return Task.FromResult($"User {username} added to list"); } } \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/BaseTestUserGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/BaseTestUserGrain.cs deleted file mode 100644 index ca62085..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/BaseTestUserGrain.cs +++ /dev/null @@ -1,32 +0,0 @@ -using ManagedCode.Communication; -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces.UserGrains; - -namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains.UserGrains; - -public abstract class BaseTestUserGrain : Grain, IBaseTestUserGrain -{ - protected bool _tokenInvalid; - protected bool _tokenExpired; - protected bool _tokenValid; - protected string _tokenValue; - - public ValueTask> IsTokenExpired() - { - return Result.Succeed(_tokenExpired).AsValueTask(); - } - - public ValueTask> IsTokenValid() - { - return Result.Succeed(_tokenValid).AsValueTask(); - } - - public ValueTask> IsTokenInvalid() - { - return Result.Succeed(_tokenInvalid).AsValueTask(); - } - - public ValueTask> GetTokenValue() - { - return Result.Succeed(_tokenValue).AsValueTask(); - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/CodeUserGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/CodeUserGrain.cs deleted file mode 100644 index 45ad778..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/CodeUserGrain.cs +++ /dev/null @@ -1,28 +0,0 @@ -using ManagedCode.Communication; -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces.UserGrains; - -namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains.UserGrains; - -public class CodeUserGrain : BaseTestUserGrain, ICodeUserGrain -{ - public ValueTask CodeVerificationTokenExpiredAsync(string token) - { - _tokenValue = token; - _tokenExpired = true; - return Result.Succeed().AsValueTask(); - } - - public ValueTask CodeVerificationTokenInvalidAsync(string token) - { - _tokenValue = token; - _tokenInvalid = true; - return Result.Succeed().AsValueTask(); - } - - public ValueTask CodeVerificationTokenValidAsync(string token) - { - _tokenValue = token; - _tokenValid = true; - return Result.Succeed().AsValueTask(); - } -} diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/LinkUserGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/LinkUserGrain.cs deleted file mode 100644 index cae7fb9..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/LinkUserGrain.cs +++ /dev/null @@ -1,30 +0,0 @@ -using ManagedCode.Communication; -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces.UserGrains; - -namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains.UserGrains -{ - public class LinkUserGrain : BaseTestUserGrain, ILinkUserGrain - { - - public ValueTask MagicLinkTokenExpiredAsync(string token) - { - _tokenValue = token; - _tokenExpired = true; - return Result.Succeed().AsValueTask(); - } - - public ValueTask MagicLinkTokenInvalidAsync(string token) - { - _tokenValue = token; - _tokenInvalid = true; - return Result.Succeed().AsValueTask(); - } - - public ValueTask MagicLinkTokenValidAsync(string token) - { - _tokenValue = token; - _tokenValid = true; - return Result.Succeed().AsValueTask(); - } - } -} diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/SocialUserGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/SocialUserGrain.cs deleted file mode 100644 index a33744d..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrains/SocialUserGrain.cs +++ /dev/null @@ -1,29 +0,0 @@ -using ManagedCode.Communication; -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces.UserGrains; - -namespace ManagedCode.Orleans.Identity.Tests.Cluster.Grains.UserGrains; - -public class SocialUserGrain : BaseTestUserGrain, ISocialUserGrain -{ - public ValueTask EmailVerificationTokenExpiredAsync(string token) - { - _tokenValue = token; - _tokenExpired = true; - return Result.Succeed().AsValueTask(); - } - - public ValueTask EmailVerificationTokenValidAsync(string token) - { - _tokenValue = token; - _tokenValid = true; - return Result.Succeed().AsValueTask(); - } - - public ValueTask EmailVerificationTokenInvalidAsync(string token) - { - _tokenValue = token; - _tokenInvalid = true; - return Result.Succeed().AsValueTask(); - } - -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/ShortLifetimeSilo/ShortLifetimeSiloConfiguration.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/ShortLifetimeSilo/ShortLifetimeSiloConfiguration.cs deleted file mode 100644 index 7a4caa6..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/ShortLifetimeSilo/ShortLifetimeSiloConfiguration.cs +++ /dev/null @@ -1,31 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Constants; -using ManagedCode.Orleans.Identity.Server.Extensions; -using Microsoft.Extensions.DependencyInjection; -using Orleans.Configuration; -using Orleans.Serialization; -using Orleans.TestingHost; - -namespace ManagedCode.Orleans.Identity.Tests.Cluster.ShortLifetimeSilo; - -public class ShortLifetimeSiloConfiguration : ISiloConfigurator -{ - public void Configure(ISiloBuilder siloBuilder) - { - // add OrleansIdentity - siloBuilder.AddOrleansIdentity(); - - - // For test purpose - siloBuilder.AddMemoryGrainStorage(OrleansIdentityConstants.SESSION_STORAGE); - siloBuilder.UseInMemoryReminderService(); - siloBuilder.Configure(options => - { - options.CollectionAge = TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(30)); - }); - - siloBuilder.ConfigureServices(services => - { - services.AddSingleton(ShortLifetimeSiloOptions.SessionOption); - }); - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/ShortLifetimeSilo/ShortLifetimeSiloTestApp.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/ShortLifetimeSilo/ShortLifetimeSiloTestApp.cs deleted file mode 100644 index 23bcb8a..0000000 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/ShortLifetimeSilo/ShortLifetimeSiloTestApp.cs +++ /dev/null @@ -1,55 +0,0 @@ -using ManagedCode.Orleans.Identity.Tests.TestApp; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Orleans.TestingHost; -using Xunit; - -namespace ManagedCode.Orleans.Identity.Tests.Cluster.ShortLifetimeSilo; - -[CollectionDefinition(nameof(ShortLifetimeSiloTestApp))] -public class ShortLifetimeSiloTestApp : WebApplicationFactory, ICollectionFixture -{ - public TestCluster Cluster { get; } - - public ShortLifetimeSiloTestApp() - { - var builder = new TestClusterBuilder(); - builder.AddSiloBuilderConfigurator(); - builder.AddClientBuilderConfigurator(); - Cluster = builder.Build(); - Cluster.Deploy(); - } - - protected override IHost CreateHost(IHostBuilder builder) - { - if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"))) - { - Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); - } - - builder.ConfigureServices(s => { s.AddSingleton(Cluster.Client); }); - return base.CreateHost(builder); - } - - public HubConnection CreateSignalRClient(string hubUrl, Action? configure = null) - { - var builder = new HubConnectionBuilder(); - configure?.Invoke(builder); - return builder.WithUrl(new Uri(Server.BaseAddress, hubUrl), o => o.HttpMessageHandlerFactory = _ => Server.CreateHandler()) - .Build(); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - Cluster.Dispose(); - } - - public override async ValueTask DisposeAsync() - { - await base.DisposeAsync(); - await Cluster.DisposeAsync(); - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/ControllerTests.cs b/ManagedCode.Orleans.Identity.Tests/ControllerTests.cs deleted file mode 100644 index 7c56823..0000000 --- a/ManagedCode.Orleans.Identity.Tests/ControllerTests.cs +++ /dev/null @@ -1,268 +0,0 @@ -using System.Net; -using System.Security.Claims; -using FluentAssertions; -using ManagedCode.Orleans.Identity.Core.Constants; -using ManagedCode.Orleans.Identity.Core.Interfaces; -using ManagedCode.Orleans.Identity.Core.Models; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using ManagedCode.Orleans.Identity.Tests.Constants; -using ManagedCode.Orleans.Identity.Tests.Helpers; -using Orleans.Runtime; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests; - -[Collection(nameof(TestClusterApplication))] -public class ControllerTests -{ - private readonly ITestOutputHelper _outputHelper; - private readonly TestClusterApplication _testApp; - - private Dictionary claimsForAdminController = new() - { - { ClaimTypes.Role, "Moderator" }, - { ClaimTypes.Email, "test2@gmail.com" } - }; - - public ControllerTests(TestClusterApplication testApp, ITestOutputHelper outputHelper) - { - _testApp = testApp; - _outputHelper = outputHelper; - } - - private async Task CreateSession(string sessionId, Dictionary> claims = null, bool replaceClaims = false) - { - var createSessionModel = SessionHelper.GetTestCreateSessionModel(sessionId, claims, replaceClaims); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(createSessionModel); - } - - private async Task CreateSession(string sessionId, CreateSessionModel createSessionModel) - { - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(createSessionModel); - } - - #region Authorized route tests - - [Fact] - public async Task LoginTest() - { - var client = _testApp.CreateClient(); - - var response = await client.GetAsync(TestControllerRoutes.AUTH_CONTROLLER_LOGIN+"?user=test"); - response.IsSuccessStatusCode.Should().BeTrue(); - - response = await client.GetAsync(TestControllerRoutes.AUTH_CONTROLLER_LOGOUT); - response.IsSuccessStatusCode.Should().BeTrue(); - } - - - [Fact] - public async Task SendRequestToUnauthorizedRoute_ReturnOk() - { - // Arrange - var client = _testApp.CreateClient(); - - // Act - var response = await client.GetAsync(TestControllerRoutes.ANONYMOUS_ROUTE); - - // Assert - response.IsSuccessStatusCode.Should().BeTrue(); - } - - [Fact] - public async Task SendRequestToAuthorizedRoute_WhenNotAuthorized_ReturnUnauthorizedCode() - { - // Arrange - var client = _testApp.CreateClient(); - - // Act - var response = await client.GetAsync(TestControllerRoutes.AUTHORIZE_ROUTE); - - // Assert - response.IsSuccessStatusCode.Should().BeFalse(); - response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); - } - - [Fact] - public async Task SendRequestToAuthorizedRoute_WhenAuthorized_ReturnOk() - { - // Arrange - var client = _testApp.CreateClient(); - var sessionId = Guid.NewGuid().ToString(); - await CreateSession(sessionId); - client.DefaultRequestHeaders.Add(OrleansIdentityConstants.AUTH_TOKEN, sessionId); - - // Act - var response = await client.GetAsync(TestControllerRoutes.AUTHORIZE_ROUTE); - - // Assert - response.IsSuccessStatusCode.Should().BeTrue(); - } - - [Fact] - public async Task SendRequestToAuthorizedRouteWithRole_WhenAuthorizedWithRole_ReturnOk() - { - // Arrange - var client = _testApp.CreateClient(); - var sessionId = Guid.NewGuid().ToString(); - await CreateSession(sessionId); - client.DefaultRequestHeaders.Add(OrleansIdentityConstants.AUTH_TOKEN, sessionId); - - // Act - var response = await client.GetAsync(TestControllerRoutes.ADMIN_ROUTE); - - // Assert - response.IsSuccessStatusCode.Should().BeTrue(); - } - - [Fact] - public async Task SendRequestToAuthorizedRouteWithRole_WhenAuthorizedWithoutRole_ReturnForbidden() - { - // Arrange - var client = _testApp.CreateClient(); - var sessionId = Guid.NewGuid().ToString(); - await CreateSession(sessionId); - client.DefaultRequestHeaders.Add(OrleansIdentityConstants.AUTH_TOKEN, sessionId); - - // Act - var response = await client.GetAsync(TestControllerRoutes.MODERATOR_ROUTE); - - // Assert - response.IsSuccessStatusCode.Should().BeFalse(); - response.StatusCode.Should().Be(HttpStatusCode.Forbidden); - } - - [Fact] - public async Task SendRequestToAuthorizedRouteWitheRoles_WhenAuthorizedWithRoles_ReturnOk() - { - // Arrange - var client = _testApp.CreateClient(); - var sessionId = Guid.NewGuid().ToString(); - CreateSessionModel createSessionModel = new CreateSessionModel(); - createSessionModel.AddUserGrainId(SessionHelper.GetTestUserGrainId()); - createSessionModel.AddProperty(ClaimTypes.Role, new List { TestRoles.ADMIN, TestRoles.MODERATOR }); - await CreateSession(sessionId, createSessionModel); - client.DefaultRequestHeaders.Add(OrleansIdentityConstants.AUTH_TOKEN, sessionId); - - // Act - var response = await client.GetAsync(TestControllerRoutes.ADMIN_CONTROLLER_EDIT_ADMINS); - - // Assert - response.IsSuccessStatusCode.Should().BeTrue(); - } - - [Fact] - public async Task SendRequestToAuthorizedRouteWitheRoles_WhenAuthorizedWithNotAllRoles_ReturnForbidden() - { - // Arrange - var client = _testApp.CreateClient(); - var sessionId = Guid.NewGuid().ToString(); - await CreateSession(sessionId); - client.DefaultRequestHeaders.Add(OrleansIdentityConstants.AUTH_TOKEN, sessionId); - - // Act - var response = await client.GetAsync(TestControllerRoutes.ADMIN_CONTROLLER_EDIT_ADMINS); - - // Assert - response.IsSuccessStatusCode.Should().BeFalse(); - response.StatusCode.Should().Be(HttpStatusCode.Forbidden); - } - - #endregion - - #region Authorized controller tests - - [Fact] - public async Task SendRequestToAuthorizedController_WhenHasRole_ReturnOk() - { - // Arrange - var client = _testApp.CreateClient(); - var sessionId = Guid.NewGuid().ToString(); - await CreateSession(sessionId); - client.DefaultRequestHeaders.Add(OrleansIdentityConstants.AUTH_TOKEN, sessionId); - - // Act - var response = await client.GetAsync(TestControllerRoutes.ADMIN_CONTROLLER_DEFAULT_ROUTE); - - // Assert - response.IsSuccessStatusCode.Should().BeTrue(); - } - - [Fact] - public async Task SendRequestToAuthorizedControllerToUnauthorizedRoute_WithoutRole_ReturnOk() - { - // Arrange - var client = _testApp.CreateClient(); - var sessionId = Guid.NewGuid().ToString(); - CreateSessionModel createSessionModel = new CreateSessionModel(); - createSessionModel.AddUserGrainId(SessionHelper.GetTestUserGrainId()); - createSessionModel.AddProperty(ClaimTypes.Role, new List { TestRoles.USER }); - await CreateSession(sessionId, createSessionModel); - client.DefaultRequestHeaders.Add(OrleansIdentityConstants.AUTH_TOKEN, sessionId); - - // Act - var response = await client.GetAsync(TestControllerRoutes.ADMIN_CONTROLLER_ADMINS_LIST); - - // Assert - response.IsSuccessStatusCode.Should().BeTrue(); - } - - [Fact] - public async Task SendRequestToAuthorizedControllerToUnauthorizedRoute_NotAuthorized_ReturnOk() - { - // Arrange - var client = _testApp.CreateClient(); - - // Act - var response = await client.GetAsync(TestControllerRoutes.ADMIN_CONTROLLER_ADMINS_LIST); - - // Assert - response.IsSuccessStatusCode.Should().BeTrue(); - } - - [Fact] - public async Task SendRequestToAuthorizedControllerToAuthorizedRoute_WhenAutorized_ReturnForbidden() - { - // Arrange - var client = _testApp.CreateClient(); - var sessionId = Guid.NewGuid().ToString(); - CreateSessionModel createSessionModel = new CreateSessionModel(); - createSessionModel.AddUserGrainId(SessionHelper.GetTestUserGrainId()); - createSessionModel.AddProperty(ClaimTypes.Role, new List { TestRoles.USER }); - await CreateSession(sessionId, createSessionModel); - client.DefaultRequestHeaders.Add(OrleansIdentityConstants.AUTH_TOKEN, sessionId); - - // Act - var response = await client.GetAsync(TestControllerRoutes.ADMIN_CONTROLLER_ADMIN_GET_ADMIN); - - // Assert - response.IsSuccessStatusCode.Should().BeFalse(); - response.StatusCode.Should().Be(HttpStatusCode.Forbidden); - } - - [Fact] - public async Task SendRequestToToAuthorizedControllerToAuthorizedRouteWithRole_WhenAutorizedWithRole_ReturnOk() - { - // Arrange - var client = _testApp.CreateClient(); - var sessionId = Guid.NewGuid().ToString(); - CreateSessionModel createSessionModel = new CreateSessionModel(); - createSessionModel.AddUserGrainId(SessionHelper.GetTestUserGrainId()); - createSessionModel.AddProperty(ClaimTypes.Role, new List { TestRoles.ADMIN, TestRoles.MODERATOR }); - await CreateSession(sessionId, createSessionModel); - - await CreateSession(sessionId, createSessionModel); - client.DefaultRequestHeaders.Add(OrleansIdentityConstants.AUTH_TOKEN, sessionId); - - // Act - var response = await client.GetAsync(TestControllerRoutes.ADMIN_CONTROLLER_ADMIN_GET_ADMIN); - - // Assert - response.IsSuccessStatusCode.Should().BeTrue(); - } - - #endregion -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/JwtControllerTests.cs b/ManagedCode.Orleans.Identity.Tests/JwtControllerTests.cs new file mode 100644 index 0000000..3d66a72 --- /dev/null +++ b/ManagedCode.Orleans.Identity.Tests/JwtControllerTests.cs @@ -0,0 +1,203 @@ +using System.Net; +using System.Net.Http.Json; +using System.Security.Claims; +using FluentAssertions; +using ManagedCode.Orleans.Identity.Tests.Cluster; +using Xunit; +using Xunit.Abstractions; + +namespace ManagedCode.Orleans.Identity.Tests; + +[Collection(nameof(TestClusterApplication))] +public class JwtControllerTests(TestClusterApplication testApp, ITestOutputHelper outputHelper) +{ + private readonly ITestOutputHelper _outputHelper = outputHelper; + + private async Task GetJwtToken(string username) + { + var client = testApp.CreateClient(); + var loginRequest = new { Username = username }; + var response = await client.PostAsJsonAsync("/auth/login", loginRequest); + + response.IsSuccessStatusCode.Should().BeTrue(); + var result = await response.Content.ReadFromJsonAsync(); + return result!.Token; + } + + private HttpClient CreateAuthenticatedClient(string token) + { + var client = testApp.CreateClient(); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + return client; + } + + [Fact] + public async Task LoginWithJwt_ShouldReturnToken() + { + // Arrange + var client = testApp.CreateClient(); + var loginRequest = new { Username = "user" }; + + // Act + var response = await client.PostAsJsonAsync("/auth/login", loginRequest); + + // Assert + response.IsSuccessStatusCode.Should().BeTrue(); + var result = await response.Content.ReadFromJsonAsync(); + result.Should().NotBeNull(); + result!.Token.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task GetCurrentUser_WhenAuthenticated_ShouldReturnUserInfo() + { + // Arrange + var token = await GetJwtToken("user"); + var client = CreateAuthenticatedClient(token); + + // Act + var response = await client.GetAsync("/auth/me"); + + // Assert + response.IsSuccessStatusCode.Should().BeTrue(); + var userInfo = await response.Content.ReadFromJsonAsync(); + userInfo.Should().NotBeNull(); + userInfo!.Username.Should().Be("user"); + userInfo.Roles.Should().Contain("user"); + } + + [Fact] + public async Task GetUser_WhenAuthenticated_ShouldReturnPersonalizedMessage() + { + // Arrange + var token = await GetJwtToken("user"); + var client = CreateAuthenticatedClient(token); + + // Act + var response = await client.GetAsync("/userController"); + + // Assert + response.IsSuccessStatusCode.Should().BeTrue(); + var result = await response.Content.ReadAsStringAsync(); + result.Should().Contain("Hello, user!"); + } + + [Fact] + public async Task GetUser_WhenNotAuthenticated_ShouldReturnUnauthorized() + { + // Arrange + var client = testApp.CreateClient(); + + // Act + var response = await client.GetAsync("/userController"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task BanUser_WhenAdmin_ShouldReturnSuccess() + { + // Arrange + var token = await GetJwtToken("admin"); + var client = CreateAuthenticatedClient(token); + + // Act + var response = await client.GetAsync("/userController/ban"); + + // Assert + response.IsSuccessStatusCode.Should().BeTrue(); + var result = await response.Content.ReadAsStringAsync(); + result.Should().Contain("admin"); + result.Should().Contain("banned"); + } + + [Fact] + public async Task BanUser_WhenNotAdmin_ShouldReturnForbidden() + { + // Arrange + var token = await GetJwtToken("user"); + var client = CreateAuthenticatedClient(token); + + // Act + var response = await client.GetAsync("/userController/ban"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task GetPublicInfo_WhenNotAuthenticated_ShouldReturnSuccess() + { + // Arrange + var client = testApp.CreateClient(); + + // Act + var response = await client.GetAsync("/userController/publicInfo"); + + // Assert + response.IsSuccessStatusCode.Should().BeTrue(); + var result = await response.Content.ReadAsStringAsync(); + result.Should().Contain("public information"); + } + + [Fact] + public async Task ModifyUser_WhenModerator_ShouldReturnSuccess() + { + // Arrange + var token = await GetJwtToken("moderator"); + var client = CreateAuthenticatedClient(token); + + // Act + var response = await client.GetAsync("/userController/modify"); + + // Assert + response.IsSuccessStatusCode.Should().BeTrue(); + var result = await response.Content.ReadAsStringAsync(); + result.Should().Contain("moderator"); + result.Should().Contain("modified"); + } + + [Fact] + public async Task ModifyUser_WhenNotModerator_ShouldReturnForbidden() + { + // Arrange + var token = await GetJwtToken("user"); + var client = CreateAuthenticatedClient(token); + + // Act + var response = await client.GetAsync("/userController/modify"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task AddToList_WhenAuthenticated_ShouldReturnSuccess() + { + // Arrange + var token = await GetJwtToken("user"); + var client = CreateAuthenticatedClient(token); + + // Act + var response = await client.GetAsync("/userController/addToList"); + + // Assert + response.IsSuccessStatusCode.Should().BeTrue(); + var result = await response.Content.ReadAsStringAsync(); + result.Should().Contain("user"); + result.Should().Contain("added to list"); + } +} + +public class LoginResponse +{ + public string Token { get; set; } = string.Empty; +} + +public class UserInfo +{ + public string UserId { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string[] Roles { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/JwtSignalRTests.cs b/ManagedCode.Orleans.Identity.Tests/JwtSignalRTests.cs new file mode 100644 index 0000000..96de80a --- /dev/null +++ b/ManagedCode.Orleans.Identity.Tests/JwtSignalRTests.cs @@ -0,0 +1,151 @@ +using System.Net; +using System.Net.Http.Json; +using FluentAssertions; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.SignalR.Client; +using ManagedCode.Orleans.Identity.Tests.Cluster; +using Xunit; +using Xunit.Abstractions; + +namespace ManagedCode.Orleans.Identity.Tests; + +[Collection(nameof(TestClusterApplication))] +public class JwtSignalRTests +{ + private readonly ITestOutputHelper _outputHelper; + private readonly TestClusterApplication _testApp; + + public JwtSignalRTests(TestClusterApplication testApp, ITestOutputHelper outputHelper) + { + _testApp = testApp; + _outputHelper = outputHelper; + } + + private async Task GetJwtToken(string username) + { + var client = _testApp.CreateClient(); + var loginRequest = new { Username = username }; + var response = await client.PostAsJsonAsync("/auth/login", loginRequest); + + response.IsSuccessStatusCode.Should().BeTrue(); + var result = await response.Content.ReadFromJsonAsync(); + return result!.Token; + } + + private async Task CreateSignalRConnection(string token) + { + var connection = _testApp.CreateSignalRClient("TestAuthorizeHub", builder => + { + builder.WithUrl($"{_testApp.Server.BaseAddress}TestAuthorizeHub", options => + { + options.AccessTokenProvider = () => Task.FromResult(token); + }); + }); + + await connection.StartAsync(); + return connection; + } + + [Fact] + public async Task SignalR_GetUserInfo_WhenAuthenticated_ShouldReturnPersonalizedMessage() + { + // Arrange + var token = await GetJwtToken("user"); + var connection = await CreateSignalRConnection(token); + + try + { + // Act + var result = await connection.InvokeAsync("GetUserInfo"); + + // Assert + result.Should().NotBeNullOrEmpty(); + result.Should().Contain("Hello, user!"); + } + finally + { + await connection.DisposeAsync(); + } + } + + [Fact] + public async Task SignalR_GetPublicInfo_WhenAuthenticated_ShouldReturnPublicInfo() + { + // Arrange + var token = await GetJwtToken("user"); + var connection = await CreateSignalRConnection(token); + + try + { + // Act + var result = await connection.InvokeAsync("GetPublicInfo"); + + // Assert + result.Should().NotBeNullOrEmpty(); + result.Should().Contain("public information"); + } + finally + { + await connection.DisposeAsync(); + } + } + + [Fact] + public async Task SignalR_GetAdminInfo_WhenAdmin_ShouldReturnSuccess() + { + // Arrange + var token = await GetJwtToken("admin"); + var connection = await CreateSignalRConnection(token); + + try + { + // Act + var result = await connection.InvokeAsync("GetAdminInfo"); + + // Assert + result.Should().NotBeNullOrEmpty(); + result.Should().Contain("admin"); + result.Should().Contain("admin privileges"); + } + finally + { + await connection.DisposeAsync(); + } + } + + [Fact] + public async Task SignalR_GetAdminInfo_WhenNotAdmin_ShouldThrowException() + { + // Arrange + var token = await GetJwtToken("user"); + var connection = await CreateSignalRConnection(token); + + try + { + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + connection.InvokeAsync("GetAdminInfo")); + + exception.Message.Should().Contain("Failed to invoke 'GetAdminInfo' because user is unauthorized"); + } + finally + { + await connection.DisposeAsync(); + } + } + + [Fact] + public async Task SignalR_ConnectWithoutToken_ShouldFail() + { + // Arrange + var connection = new HubConnectionBuilder() + .WithUrl($"{_testApp.Server.BaseAddress}TestAuthorizeHub") + .Build(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + connection.StartAsync()); + + await connection.DisposeAsync(); + } +} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/ManagedCode.Orleans.Identity.Tests.csproj b/ManagedCode.Orleans.Identity.Tests/ManagedCode.Orleans.Identity.Tests.csproj index 6aae240..a3558b9 100644 --- a/ManagedCode.Orleans.Identity.Tests/ManagedCode.Orleans.Identity.Tests.csproj +++ b/ManagedCode.Orleans.Identity.Tests/ManagedCode.Orleans.Identity.Tests.csproj @@ -11,6 +11,7 @@ + @@ -20,6 +21,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ManagedCode.Orleans.Identity.Tests/SessionGrainReminderTests.cs b/ManagedCode.Orleans.Identity.Tests/SessionGrainReminderTests.cs deleted file mode 100644 index 45a0326..0000000 --- a/ManagedCode.Orleans.Identity.Tests/SessionGrainReminderTests.cs +++ /dev/null @@ -1,149 +0,0 @@ -using FluentAssertions; -using ManagedCode.Orleans.Identity.Core.Enums; -using ManagedCode.Orleans.Identity.Core.Interfaces; -using ManagedCode.Orleans.Identity.Core.Models; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using ManagedCode.Orleans.Identity.Tests.Cluster.ShortLifetimeSilo; -using ManagedCode.Orleans.Identity.Tests.Helpers; -using Orleans.Runtime; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests; - -[Collection(nameof(ShortLifetimeSiloTestApp))] -public class SessionGrainReminderTests -{ - private readonly ITestOutputHelper _outputHelper; - private readonly ShortLifetimeSiloTestApp _testApp; - - private CreateSessionModel GetTestCreateSessionModel(string sessionId) - { - var userId = Guid.NewGuid().ToString(); - - var userGrainId = GrainId.Create("UserGrain", userId); - - var createSessionModel = new CreateSessionModel - { - UserData = SessionHelper.SetTestClaims(sessionId), - UserGrainId = userGrainId - }; - - return createSessionModel; - } - - public SessionGrainReminderTests(ITestOutputHelper outputHelper, ShortLifetimeSiloTestApp testApp) - { - _outputHelper = outputHelper; - _testApp = testApp; - } - - [Fact] - public async Task DeactivateGrain_RegisterReminder_CloseSession() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var sessionCreateModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.GrainFactory.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - await Task.Delay(TimeSpan.FromMinutes(5)); - var result = await sessionGrain.GetSessionAsync(); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public async Task DeactivateGrain_WhenClearSessionOnCloseIsFalse_RegisterReminder_CloseSession() - { - // Arrange - ShortLifetimeSiloOptions.SessionOption.ClearStateOnClose = false; - var sessionId = Guid.NewGuid().ToString(); - var sessionCreateModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.GrainFactory.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - await Task.Delay(TimeSpan.FromMinutes(5)); - var result = await sessionGrain.GetSessionAsync(); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Status.Should().Be(SessionStatus.Closed); - result.Value.ClosedDate.Should().NotBeNull(); - ShortLifetimeSiloOptions.SessionOption.ClearStateOnClose = true; - } - - [Fact] - public async Task DeactivateGrain_WhenSessionDoesntExists_DoNotRegisterReminder() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var sessionGrain = _testApp.Cluster.GrainFactory.GetGrain(sessionId); - - // Act - var result = await sessionGrain.PauseSessionAsync(); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public async Task DeactivateGrain_RegisterReminder_CloseSession_UnregisterReminder() - { - // Arrange - ShortLifetimeSiloOptions.SessionOption.SessionLifetime = TimeSpan.FromMinutes(4); - var sessionId = Guid.NewGuid().ToString(); - var sessionCreateModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.GrainFactory.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - await sessionGrain.GetSessionAsync(); - await Task.Delay(TimeSpan.FromMinutes(2)); - var result = await sessionGrain.CloseAsync(); - - // Assert - result.IsSuccess.Should().BeTrue(); - ShortLifetimeSiloOptions.SessionOption.SessionLifetime = TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(40)); - } - - [Fact] - public async Task DeactivateGrain_ReactivateGrainWhenSessionLifetimeIsExpired_CloseSession_UnregisterReminder() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var sessionCreateModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.GrainFactory.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - await Task.Delay(TimeSpan.FromMinutes(3)); - var result = await sessionGrain.GetSessionAsync(); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public async Task DeactivateGrain_ReactivateGrainWhenSessionLifetimeIsExpiredAndClearStateOnClose_CloseSession_UnregisterReminder_ReturnClosedSession() - { - // Arrange - ShortLifetimeSiloOptions.SessionOption.ClearStateOnClose = false; - var sessionId = Guid.NewGuid().ToString(); - var sessionCreateModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.GrainFactory.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - await Task.Delay(TimeSpan.FromMinutes(3)); - var result = await sessionGrain.GetSessionAsync(); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Status.Should().Be(SessionStatus.Closed); - ShortLifetimeSiloOptions.SessionOption.ClearStateOnClose = true; - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/SessionGrainTests.cs b/ManagedCode.Orleans.Identity.Tests/SessionGrainTests.cs deleted file mode 100644 index c91d261..0000000 --- a/ManagedCode.Orleans.Identity.Tests/SessionGrainTests.cs +++ /dev/null @@ -1,703 +0,0 @@ -using System.Security.Claims; -using FluentAssertions; -using ManagedCode.Orleans.Identity.Core.Enums; -using ManagedCode.Orleans.Identity.Core.Interfaces; -using ManagedCode.Orleans.Identity.Core.Models; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using ManagedCode.Orleans.Identity.Tests.Helpers; -using Orleans.Runtime; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests; - -[Collection(nameof(TestClusterApplication))] -public class SessionGrainTests -{ - private readonly ITestOutputHelper _outputHelper; - private readonly TestClusterApplication _testApp; - - public SessionGrainTests(TestClusterApplication testApp, ITestOutputHelper outputHelper) - { - _testApp = testApp; - _outputHelper = outputHelper; - } - - private CreateSessionModel GetTestCreateSessionModel(string sessionId) - { - var userId = Guid.NewGuid().ToString(); - - var userGrainId = GrainId.Create("UserGrain", userId); - - var createSessionModel = new CreateSessionModel - { - UserData = SessionHelper.SetTestClaims(sessionId), - UserGrainId = userGrainId - }; - - return createSessionModel; - } - - #region CreateSession - - [Fact] - public async Task CreateSessionAsync_ReturnCreatedSession() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - - // Act - var result = await sessionGrain.CreateAsync(createSessionModel); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().NotBeNull(); - } - - #endregion - - #region ValidateSessionAndGetClaimsAsync - - [Fact] - public async Task ValidateSessionAndGetClaimsAsync_ReturnClaims() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(createSessionModel); - - // Act - var result = await sessionGrain.ValidateAndGetClaimsAsync(); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().NotBeNull(); - result.Value.Count.Should().Be(createSessionModel.UserData.Count + 1); - } - - [Fact] - public async Task ValidateSessionAndGetClaimsAsync_WhenSessionStateIsNotExists_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - - // Act - var result = await sessionGrain.ValidateAndGetClaimsAsync(); - - // Assert - result.IsFailed.Should().BeTrue(); - result.Value.Should().BeNull(); - } - - [Fact] - public async Task ValidateSessionAndGetClaimsAsync_WhenSessionIsNotActive_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - TestSiloOptions.SessionOption.ClearStateOnClose = false; - await sessionGrain.CreateAsync(createSessionModel); - await sessionGrain.CloseAsync(); - - // Act - var result = await sessionGrain.ValidateAndGetClaimsAsync(); - - // Assert - result.IsFailed.Should().BeTrue(); - result.Value.Should().BeNull(); - TestSiloOptions.SessionOption.ClearStateOnClose = false; - } - - #endregion - - #region PauseSessionAsync - - [Fact] - public async Task PauseSessionAsync_ReturnSuccess() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(createSessionModel); - - // Act - var result = await sessionGrain.PauseSessionAsync(); - - // Assert - var session = await sessionGrain.GetSessionAsync(); - result.IsSuccess.Should().BeTrue(); - session.Value.Status.Should().Be(SessionStatus.Paused); - } - - [Fact] - public async Task PauseSessionAsync_WhenSessionStateIsNotExists_ReturnFailed() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - - // Act - var result = await sessionGrain.PauseSessionAsync(); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - #endregion - - #region CloseSessionAsync - - [Fact] - public async Task CloseSessionAsync_WhenSessionExistsAndClearStateIsTrue_ReturnSuccessAndClearState() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(createSessionModel); - - // Act - var result = await sessionGrain.CloseAsync(); - var session = await sessionGrain.GetSessionAsync(); - - // Arrange - result.IsSuccess.Should().BeTrue(); - session.IsFailed.Should().BeTrue(); - } - - [Fact] - public async Task CloseSessionAsync_WhenSessionExistsAndClearStateIsFalse_ReturnSuccess() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(createSessionModel); - TestSiloOptions.SessionOption.ClearStateOnClose = false; - - // Act - var result = await sessionGrain.CloseAsync(); - var session = await sessionGrain.GetSessionAsync(); - - // Arrange - result.IsSuccess.Should().BeTrue(); - session.IsSuccess.Should().BeTrue(); - session.Value.Should().NotBeNull(); - session.Value.Status.Should().Be(SessionStatus.Closed); - - TestSiloOptions.SessionOption.ClearStateOnClose = true; - } - - [Fact] - public async Task CloseSessionAsync_WhenSessionIsNotExists_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - - // Act - var result = await sessionGrain.CloseAsync(); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - #endregion - - #region ResumeSessionAsync - - [Fact] - public async Task ResumeSessionAsync_WhenSessionExists_ReturnSuccess() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(createSessionModel); - await sessionGrain.PauseSessionAsync(); - - // Act - var result = await sessionGrain.ResumeSessionAsync(); - - // Assert - var session = await sessionGrain.GetSessionAsync(); - result.IsSuccess.Should().BeTrue(); - session.IsSuccess.Should().BeTrue(); - session.Value.Status.Should().Be(SessionStatus.Active); - } - - [Fact] - public async Task ResumeSessionAsync_WhenSessionIsNotExists_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - - // Act - var result = await sessionGrain.ResumeSessionAsync(); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - #endregion - - #region RemoveProperty - - [Fact] - public async Task RemoveProperty_WhenPropertyExists_ReturnSuccess() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(createSessionModel); - - // Act - var result = await sessionGrain.RemoveProperty(ClaimTypes.MobilePhone); - - // Assert - var claims = await sessionGrain.ValidateAndGetClaimsAsync(); - result.IsSuccess.Should().BeTrue(); - claims.IsSuccess.Should().BeTrue(); - claims.Value.Should().NotContainKey(ClaimTypes.MobilePhone); - } - - [Fact] - public async Task RemoveProperty_WhenPropertyIsNotExists_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(createSessionModel); - - // Act - var result = await sessionGrain.RemoveProperty(ClaimTypes.WindowsDeviceGroup); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public async Task RemoveProperty_WhenSessionIsNotExists_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - - // Act - var result = await sessionGrain.RemoveProperty(ClaimTypes.MobilePhone); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - #endregion - - #region AddProperty - - [Fact] - public async Task AddProperty_WhenSessionExist_ReturnSuccess() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var propertyValue = "Name"; - var sessionCreateModel = SessionHelper.GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - var result = await sessionGrain.AddProperty(ClaimTypes.Name, propertyValue); - - // Assert - var userData = await sessionGrain.ValidateAndGetClaimsAsync(); - result.IsSuccess.Should().BeTrue(); - userData.IsSuccess.Should().BeTrue(); - userData.Value.Should().ContainKey(ClaimTypes.Name).WhoseValue.Should().BeEquivalentTo(propertyValue); - } - - [Fact] - public async Task AddProperty_WhenSessionNotExist_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var propertyValue = "Name"; - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - - // Act - var result = await sessionGrain.AddProperty(ClaimTypes.Name, propertyValue); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public async Task AddProperty_WhenPropertyAlreadyExist_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var propertyValue = "Name"; - var sessionCreateModel = SessionHelper.GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - var result = await sessionGrain.AddProperty(ClaimTypes.Email, propertyValue); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public async Task AddProperty_WithValues_WhenSessionExist_ReturnSuccess() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var propertyValues = new List { "one", "two" }; - var sessionCreateModel = SessionHelper.GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - var result = await sessionGrain.AddProperty(ClaimTypes.System, propertyValues); - - // Assert - var userData = await sessionGrain.ValidateAndGetClaimsAsync(); - result.IsSuccess.Should().BeTrue(); - userData.IsSuccess.Should().BeTrue(); - userData.Value.Should().ContainKey(ClaimTypes.System).WhoseValue.Should().BeEquivalentTo(propertyValues); - } - - [Fact] - public async Task AddProperty_WithValuesThatHaveDuplicates_WhenSessionExist_ReturnSuccess() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var propertyValues = new List { "one", "two", "two" }; - var expectedpropertyValues = new List { "one", "two" }; - var sessionCreateModel = SessionHelper.GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - var result = await sessionGrain.AddProperty(ClaimTypes.System, propertyValues); - - // Assert - var userData = await sessionGrain.ValidateAndGetClaimsAsync(); - result.IsSuccess.Should().BeTrue(); - userData.IsSuccess.Should().BeTrue(); - userData.Value.Should().ContainKey(ClaimTypes.System).WhoseValue.Should().BeEquivalentTo(expectedpropertyValues); - } - - #endregion - - #region ReplaceProperty - - [Fact] - public async Task ReplaceProperty_WhenPropertyExists_ReturnSuccess() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var newPropertyValue = "test22@gmail.com"; - var sessionCreateModel = SessionHelper.GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - var result = await sessionGrain.ReplaceProperty(ClaimTypes.Email, newPropertyValue); - - // Assert - var userData = await sessionGrain.ValidateAndGetClaimsAsync(); - result.IsSuccess.Should().BeTrue(); - userData.IsSuccess.Should().BeTrue(); - userData.Value.Should().ContainKey(ClaimTypes.Email).WhoseValue.Should().BeEquivalentTo(newPropertyValue); - } - - [Fact] - public async Task ReplaceProperty_WhenPropertyNotExist_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var newPropertyValue = "new name"; - var sessionCreateModel = SessionHelper.GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - var result = await sessionGrain.ReplaceProperty(ClaimTypes.Name, newPropertyValue); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public async Task ReplaceProperty_WhenSessionNotExists_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var newPropertyValue = "test22@gmail.com"; - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - - // Act - var result = await sessionGrain.ReplaceProperty(ClaimTypes.Email, newPropertyValue); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public async Task ReplaceProperty_WithValues_WhenPropertyExists_ReturnSuccess() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var newPropertyValues = new List { "admin", "moderator" }; - var sessionCreateModel = SessionHelper.GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - var result = await sessionGrain.ReplaceProperty(ClaimTypes.Role, newPropertyValues); - - // Assert - var userData = await sessionGrain.ValidateAndGetClaimsAsync(); - result.IsSuccess.Should().BeTrue(); - userData.IsSuccess.Should().BeTrue(); - userData.Value.Should().ContainKey(ClaimTypes.Role).WhoseValue.Should().BeEquivalentTo(newPropertyValues); - } - - [Fact] - public async Task ReplaceProperty_WithValuesThatHaveDuplicates_WhenPropertyExists_ReturnSuccess() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var newPropertyValues = new List { "admin", "moderator", "moderator" }; - var expectedPropertyValues = new List { "admin", "moderator" }; - var sessionCreateModel = SessionHelper.GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - var result = await sessionGrain.ReplaceProperty(ClaimTypes.Role, newPropertyValues); - - // Assert - var userData = await sessionGrain.ValidateAndGetClaimsAsync(); - result.IsSuccess.Should().BeTrue(); - userData.IsSuccess.Should().BeTrue(); - userData.Value.Should().ContainKey(ClaimTypes.Role).WhoseValue.Should().BeEquivalentTo(expectedPropertyValues); - } - - [Fact] - public async Task ReplaceProperty_WithValues_WhenPropertyIsNotExists_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var newPropertyValues = new List { "admin", "moderator" }; - var sessionCreateModel = SessionHelper.GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - var result = await sessionGrain.ReplaceProperty(ClaimTypes.Authentication, newPropertyValues); - - result.IsFailed.Should().BeTrue(); - } - - - #endregion - - #region RemoveValueFromProperty - - [Fact] - public async Task RemoveValueFromProperty_WhenPropertyExists_ReturnSuccess() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var valueToRemove = "moderator"; - var rolesClaims = new HashSet { "admin", valueToRemove }; - var sessionCreateModel = - SessionHelper.GetTestCreateSessionModel(sessionId, new Dictionary> { { ClaimTypes.Role, rolesClaims } }, true); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - var result = await sessionGrain.RemoveValueFromProperty(ClaimTypes.Role, valueToRemove); - - // Assert - var userData = await sessionGrain.ValidateAndGetClaimsAsync(); - result.IsSuccess.Should().BeTrue(); - userData.IsSuccess.Should().BeTrue(); - userData.Value.Should().ContainKey(ClaimTypes.Role).WhoseValue.Should().NotContain(valueToRemove); - } - - [Fact] - public async Task RemoveValueFromProperty_WhenPropertyIsNotExists_ReturnFailture() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var valueToRemove = "c#@gmail.com"; - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - var sessionCreateModel = SessionHelper.GetTestCreateSessionModel(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - var result = await sessionGrain.RemoveValueFromProperty(ClaimTypes.Name, valueToRemove); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public async Task RemoveValueFromProperty_WhenSessionIsNotExists_ReturnFailture() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var valueToRemove = "c#@gmail.com"; - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - - // Act - var result = await sessionGrain.RemoveValueFromProperty(ClaimTypes.Role, valueToRemove); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public async Task RemoveValueFromProperty_WhenValueNotExists_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var valueToRemove = "moderator"; - var rolesClaims = new HashSet { "admin" }; - var sessionCreateModel = - SessionHelper.GetTestCreateSessionModel(sessionId, new Dictionary> { { ClaimTypes.Role, rolesClaims } }, true); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(sessionCreateModel); - - // Act - var result = await sessionGrain.RemoveValueFromProperty(ClaimTypes.Role, valueToRemove); - - // Assert - var userData = await sessionGrain.ValidateAndGetClaimsAsync(); - result.IsFailed.Should().BeTrue(); - } - - #endregion - - #region AddValueToProperty - - [Fact] - public async Task AddValueToProperty_WhenPropertyExists_ReturnSuccess() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = SessionHelper.GetTestCreateSessionModel(sessionId); - string valueToAdd = "moderator"; - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(createSessionModel); - - // Act - var result = await sessionGrain.AddValueToProperty(ClaimTypes.Role, valueToAdd); - - // Assert - var userData = await sessionGrain.ValidateAndGetClaimsAsync(); - result.IsSuccess.Should().BeTrue(); - userData.IsSuccess.Should().BeTrue(); - userData.Value.Should().ContainKey(ClaimTypes.Role).WhoseValue.Should().Contain(valueToAdd); - } - - [Fact] - public async Task AddValueToProperty_WhenPropertyNotExists_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = SessionHelper.GetTestCreateSessionModel(sessionId); - string valueToAdd = "moderator"; - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(createSessionModel); - - // Act - var result = await sessionGrain.AddValueToProperty(ClaimTypes.Country, valueToAdd); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public async Task AddValueToProperty_WhenValueAlreadyExists_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = SessionHelper.GetTestCreateSessionModel(sessionId); - string valueToAdd = "admin"; - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(createSessionModel); - - // Act - var result = await sessionGrain.AddValueToProperty(ClaimTypes.Role, valueToAdd); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public async Task AddValueToProperty_WhenSessionIsNotExsist_ReturFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - string valueToAdd = "moderator"; - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - - // Act - var result = await sessionGrain.AddValueToProperty(ClaimTypes.Role, valueToAdd); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - #endregion - - #region ClearUserData - - [Fact] - public async Task ClearUserData_WhenUserDataIsNotEmpty_ReturnSuccess() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var createSessionModel = SessionHelper.GetTestCreateSessionModel(sessionId); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - await sessionGrain.CreateAsync(createSessionModel); - - // Act - var result = await sessionGrain.ClearUserData(); - - // Assert - var userData = await sessionGrain.ValidateAndGetClaimsAsync(); - result.IsSuccess.Should().BeTrue(); - userData.IsSuccess.Should().BeTrue(); - userData.Value.Should().BeEmpty(); - } - - [Fact] - public async Task ClearUserData_WhenSesionIsNotExsist_ReturnFail() - { - // Arrange - var sessionId = Guid.NewGuid().ToString(); - var sessionGrain = _testApp.Cluster.Client.GetGrain(sessionId); - - // Act - var result = await sessionGrain.ClearUserData(); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - #endregion -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/SomeTest.cs b/ManagedCode.Orleans.Identity.Tests/SomeTest.cs deleted file mode 100644 index e71a24b..0000000 --- a/ManagedCode.Orleans.Identity.Tests/SomeTest.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Net; -using FluentAssertions; -using ManagedCode.Orleans.Identity.Core.Interfaces; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using ManagedCode.Orleans.Identity.Tests.TestApp; -using Microsoft.AspNetCore.SignalR.Client; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests; - -[Collection(nameof(TestClusterApplication))] -public class SomeTest -{ - private readonly ITestOutputHelper _outputHelper; - private readonly TestClusterApplication _testApp; - - public SomeTest(TestClusterApplication testApp, ITestOutputHelper outputHelper) - { - _testApp = testApp; - _outputHelper = outputHelper; - } - - [Fact] - public async Task OneRequest() - { - var gr = _testApp.Cluster.Client.GetGrain("123"); - //await gr.AddClaimAsync(new Claim("1", "2")); - var xx = await gr.GetSessionAsync(); - - var anonymous = await _testApp.CreateClient().GetAsync("/anonymous"); - anonymous.StatusCode.Should().Be(HttpStatusCode.OK); - - var authorize = await _testApp.CreateClient().GetAsync("/authorize"); - var content = await authorize.Content.ReadAsStringAsync(); - authorize.StatusCode.Should().Be(HttpStatusCode.Unauthorized); - } - - [Fact] - public async Task OneSignalR() - { - var anonymousHub = _testApp.CreateSignalRClient(nameof(TestAnonymousHub)); - await anonymousHub.StartAsync(); - anonymousHub.State.Should().Be(HubConnectionState.Connected); - - var authorizeHub = _testApp.CreateSignalRClient(nameof(TestAuthorizeHub)); - await Assert.ThrowsAsync(() => authorizeHub.StartAsync()); - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/AdminController.cs b/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/AdminController.cs deleted file mode 100644 index b958e5b..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/AdminController.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace ManagedCode.Orleans.Identity.Tests.TestApp.Controllers; - -[Authorize(Roles = "admin")] -[Route("adminController")] -public class AdminController : ControllerBase -{ - [HttpGet] - public ActionResult AdminsOnly() - { - return "Admins only"; - } - - [HttpGet("adminsList")] - [AllowAnonymous] - public ActionResult AdminsList() - { - return "adminsList"; - } - - [HttpGet("getAdmin")] - [Authorize] - public ActionResult GetAdmin() - { - return "admin"; - } - - [HttpGet("editAdmin")] - [Authorize(Roles = "moderator")] - public ActionResult EditAdmins() - { - return "edits admins"; - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/AuthController.cs b/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/AuthController.cs index da9f3ae..c18a702 100644 --- a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/AuthController.cs +++ b/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/AuthController.cs @@ -1,38 +1,61 @@ -using ManagedCode.Orleans.Identity.Tests.TestApp.Models; +using ManagedCode.Orleans.Identity.Tests.Constants; +using ManagedCode.Orleans.Identity.Tests.TestApp.Services; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; namespace ManagedCode.Orleans.Identity.Tests.TestApp.Controllers; -[AllowAnonymous] [Route("auth")] public class AuthController : ControllerBase { - private readonly SignInManager _signInManager; - private readonly UserManager _userManager; + private readonly IJwtService _jwtService; - public AuthController(SignInManager signInManager, UserManager userManager) + public AuthController(IJwtService jwtService) { - _signInManager = signInManager; - _userManager = userManager; + _jwtService = jwtService; } [AllowAnonymous] - [HttpGet("login")] - public async Task Login([FromQuery]string user) + [HttpPost("login")] + public IActionResult Login([FromBody] LoginRequest request) { - var testUser = new TestUser(user); - var identityResult = await _userManager.CreateAsync(testUser); - await _signInManager.SignInAsync(testUser, true); - return Ok(); + // Simple authentication - in real app you'd validate against database + if (string.IsNullOrEmpty(request.Username)) + { + return BadRequest("Username is required"); + } + + var roles = request.Username.ToLower() switch + { + "admin" => new[] { TestRoles.USER, TestRoles.ADMIN }, + "moderator" => new[] { TestRoles.USER, TestRoles.MODERATOR }, + "user" => new[] { TestRoles.USER }, + _ => new[] { TestRoles.USER } + }; + + var token = _jwtService.GenerateToken(request.Username, request.Username, roles); + + return Ok(new { token }); } - + + [HttpGet("me")] [Authorize] - [HttpGet("logout")] - public async Task Logout() + public IActionResult GetCurrentUser() { - await _signInManager.SignOutAsync(); - return Ok(); + var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + var username = User.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value; + var roles = User.FindAll(System.Security.Claims.ClaimTypes.Role).Select(c => c.Value); + + return Ok(new + { + UserId = userId, + Username = username, + Roles = roles + }); } +} + +public class LoginRequest +{ + public string Username { get; set; } = string.Empty; } \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/ModeratorController.cs b/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/ModeratorController.cs deleted file mode 100644 index 437dc8b..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/ModeratorController.cs +++ /dev/null @@ -1,43 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Extensions; -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace ManagedCode.Orleans.Identity.Tests.TestApp.Controllers -{ - [Authorize(Roles = "admin, moderator")] - [Route("moderatorController")] - public class ModeratorController : ControllerBase - { - private readonly IClusterClient _clusterClient; - - public ModeratorController(IClusterClient clusterClient) - { - _clusterClient = clusterClient; - } - - [HttpGet] - public async Task> GetInfo() - { - var userId = User.GetGrainId(); - var moderatorGrain = _clusterClient.GetGrain(userId); - return await moderatorGrain.GetInfo(); - } - - [HttpGet("getModerators")] - public async Task> GetModerators() - { - var userId = User.GetGrainId(); - var moderatorGrain = _clusterClient.GetGrain(userId); - return await moderatorGrain.GetModerators(); - } - - [HttpGet("publicInfo")] - public async Task> GetPublicInfo() - { - var userId = User.GetGrainId(); - var moderatorGrain = _clusterClient.GetGrain(userId); - return await moderatorGrain.GetPublicInformation(); - } - } -} diff --git a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/PublicController.cs b/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/PublicController.cs deleted file mode 100644 index ff0174a..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/PublicController.cs +++ /dev/null @@ -1,50 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Extensions; -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace ManagedCode.Orleans.Identity.Tests.TestApp.Controllers; - -[AllowAnonymous] -[Route("publicController")] -public class PublicController : ControllerBase -{ - private readonly IClusterClient _clusterClient; - - public PublicController(IClusterClient clusterClient) - { - _clusterClient = clusterClient; - } - - [HttpGet] - public async Task> CallCommonMethod() - { - var userId = User.GetGrainId(); - var publicGrain = _clusterClient.GetGrain(userId); - return await publicGrain.CommonMethod(); - } - - [HttpGet("authorizedMethod")] - public async Task> CallAuthorizedMethod() - { - var userId = User.GetGrainId(); - var publicGrain = _clusterClient.GetGrain(userId); - return await publicGrain.AuthorizedMethod(); - } - - [HttpGet("adminMethod")] - public async Task> CallAdminMethod() - { - var userId = User.GetGrainId(); - var publicGrain = _clusterClient.GetGrain(userId); - return await publicGrain.AdminOnly(); - } - - [HttpGet("moderatorMethod")] - public async Task> CallModeratorMethod() - { - var userId = User.GetGrainId(); - var publicGrain = _clusterClient.GetGrain(userId); - return await publicGrain.ModeratorOnly(); - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/TestController.cs b/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/TestController.cs deleted file mode 100644 index 0f1e00c..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/TestController.cs +++ /dev/null @@ -1,44 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Extensions; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace ManagedCode.Orleans.Identity.Tests.TestApp.Controllers; - -[Authorize] -public class TestController : ControllerBase -{ - [HttpGet("authorize")] - public ActionResult Authorize() - { - User.SetOrleansContext(); - return "Authorize"; - } - - [AllowAnonymous] - [HttpGet("anonymous")] - public ActionResult Anonymous() - { - return "Anonymous"; - } - - [Authorize(Roles = "admin")] - [HttpGet("admin")] - public ActionResult Admin() - { - return "admin"; - } - - [Authorize(Roles = "moderator")] - [HttpGet("moderator")] - public ActionResult Moderator() - { - return "moderator"; - } - - //[Authorize(Roles = "admin, moderator")] - [HttpGet("common")] - public ActionResult Common() - { - return "common"; - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/UserController.cs b/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/UserController.cs index e1c7da0..fb2fad7 100644 --- a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/UserController.cs +++ b/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/UserController.cs @@ -1,5 +1,5 @@ using ManagedCode.Orleans.Identity.Core.Extensions; -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces; +using ManagedCode.Orleans.Identity.Tests.Cluster.Grains; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -19,52 +19,96 @@ public UserController(IClusterClient clusterClient) [HttpGet] public async Task> GetUser() { - var userId = User.GetGrainId(); - var userGrain = _clusterClient.GetGrain(userId); - return await userGrain.GetUser(); + try + { + var userId = User.GetGrainId(); + var userGrain = _clusterClient.GetGrain(userId); + return await userGrain.GetUser(); + } + catch (UnauthorizedAccessException) + { + return Forbid(); + } } [HttpGet("anonymous")] [AllowAnonymous] public async Task> TryGetUser() { - var result = await GetUser(); - return result; + try + { + var result = await GetUser(); + return result; + } + catch (UnauthorizedAccessException) + { + return Forbid(); + } } [HttpGet("ban")] public async Task> BanUser() { - var userId = User.GetGrainId(); - var userGrain = _clusterClient.GetGrain(userId); - return await userGrain.BanUser(); + try + { + var userId = User.GetGrainId(); + var userGrain = _clusterClient.GetGrain(userId); + return await userGrain.BanUser(); + } + catch (UnauthorizedAccessException) + { + return Forbid(); + } } [HttpGet("publicInfo")] [AllowAnonymous] public async Task> GetPublicInfo() { - var userId = User.GetGrainId(); - var userGrain = _clusterClient.GetGrain(userId); - var result = await userGrain.GetPublicInfo(); - return result; + try + { + // For public endpoints, use a default grain ID since user might not be authenticated + var userId = User.Identity?.IsAuthenticated == true ? User.GetGrainId() : "public"; + var userGrain = _clusterClient.GetGrain(userId); + var result = await userGrain.GetPublicInfo(); + return result; + } + catch (UnauthorizedAccessException) + { + return Forbid(); + } } [HttpGet("modify")] public async Task> ModifyUser() { - var userId = User.GetGrainId(); - var userGrain = _clusterClient.GetGrain(userId); - var result = await userGrain.ModifyUser(); - return result; + try + { + var userId = User.GetGrainId(); + var userGrain = _clusterClient.GetGrain(userId); + var result = await userGrain.ModifyUser(); + return result; + } + catch (UnauthorizedAccessException) + { + return Forbid(); + } } [HttpGet("addToList")] [AllowAnonymous] public async Task> AddToList() { - var userId = User.GetGrainId(); - var userGrain = _clusterClient.GetGrain(userId); - return await userGrain.AddToList(); + try + { + // For anonymous endpoints, use a default grain ID since user might not be authenticated + var userId = User.Identity?.IsAuthenticated == true ? User.GetGrainId() : "anonymous"; + var userGrain = _clusterClient.GetGrain(userId); + return await userGrain.AddToList(); + } + catch (UnauthorizedAccessException) + { + return Forbid(); + } } } \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TestApp/HttpHostProgram.cs b/ManagedCode.Orleans.Identity.Tests/TestApp/HttpHostProgram.cs index 573c03b..f1b5092 100644 --- a/ManagedCode.Orleans.Identity.Tests/TestApp/HttpHostProgram.cs +++ b/ManagedCode.Orleans.Identity.Tests/TestApp/HttpHostProgram.cs @@ -1,14 +1,8 @@ -using ManagedCode.Orleans.Identity.Client; using ManagedCode.Orleans.Identity.Client.Extensions; -using ManagedCode.Orleans.Identity.Core.Constants; -using ManagedCode.Orleans.Identity.Tests.TestApp.Models; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; +using ManagedCode.Orleans.Identity.Tests.TestApp.Services; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using System.Text; namespace ManagedCode.Orleans.Identity.Tests.TestApp; @@ -21,27 +15,54 @@ public static void Main(string[] args) builder.Services.AddControllers(); builder.Services.AddSignalR(); - - // Add services to the container. - builder.Services.AddDbContext(options => - options.UseInMemoryDatabase("InMemoryDbForTesting")); - - builder.Services.AddIdentity() - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); - - // AddProperty it for using Orleans Identity + // Add Orleans Identity builder.Services.AddOrleansIdentity(); - - + + // Add JWT Authentication + builder.Services.AddAuthentication("Bearer") + .AddJwtBearer("Bearer", options => + { + options.TokenValidationParameters = new() + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = "Orleans.Identity.Test", + ValidAudience = "Orleans.Identity.Test", + IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey( + Encoding.UTF8.GetBytes("your-super-secret-key-with-at-least-32-characters")) + }; + + options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + var path = context.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/TestAuthorizeHub")) + { + context.Token = accessToken; + } + return Task.CompletedTask; + } + }; + }); + + builder.Services.AddAuthorization(); + + // Add JWT service + builder.Services.AddScoped(); var app = builder.Build(); - // AddProperty it for using Orleans Identity - // Authentication and Authorization already use - app.UseAuthenticationAndOrleansIdentity(); - - + // Configure the HTTP request pipeline. + app.UseAuthentication(); + app.UseAuthorization(); + + // Add Orleans Identity middleware + app.UseOrleansIdentity(); + app.MapControllers(); app.MapHub(nameof(TestAnonymousHub)); app.MapHub(nameof(TestAuthorizeHub)); diff --git a/ManagedCode.Orleans.Identity.Tests/TestApp/Models/TestUser.cs b/ManagedCode.Orleans.Identity.Tests/TestApp/Models/TestUser.cs deleted file mode 100644 index baa3d19..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TestApp/Models/TestUser.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; - -namespace ManagedCode.Orleans.Identity.Tests.TestApp.Models; - -public class TestUser : IdentityUser -{ - public TestUser() - { - } - - public TestUser(string login) - { - this.UserName = login; - } -} - -public class TestUserIdentityDbContext : IdentityDbContext -{ - public TestUserIdentityDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); - - // Add custom configurations for TestUser - builder.Entity().ToTable("TestUsers"); - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TestApp/Services/IJwtService.cs b/ManagedCode.Orleans.Identity.Tests/TestApp/Services/IJwtService.cs new file mode 100644 index 0000000..173c2c2 --- /dev/null +++ b/ManagedCode.Orleans.Identity.Tests/TestApp/Services/IJwtService.cs @@ -0,0 +1,9 @@ +using System.Security.Claims; + +namespace ManagedCode.Orleans.Identity.Tests.TestApp.Services; + +public interface IJwtService +{ + string GenerateToken(string userId, string username, IEnumerable roles); + ClaimsPrincipal? ValidateToken(string token); +} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TestApp/Services/JwtService.cs b/ManagedCode.Orleans.Identity.Tests/TestApp/Services/JwtService.cs new file mode 100644 index 0000000..052999f --- /dev/null +++ b/ManagedCode.Orleans.Identity.Tests/TestApp/Services/JwtService.cs @@ -0,0 +1,69 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace ManagedCode.Orleans.Identity.Tests.TestApp.Services; + +public class JwtService : IJwtService +{ + private readonly string _secretKey = "your-super-secret-key-with-at-least-32-characters"; + private readonly string _issuer = "Orleans.Identity.Test"; + private readonly string _audience = "Orleans.Identity.Test"; + + public string GenerateToken(string userId, string username, IEnumerable roles) + { + var claims = new List + { + new(ClaimTypes.NameIdentifier, userId), + new(ClaimTypes.Name, username), + new(ClaimTypes.Actor, userId) // For grain ID + }; + + foreach (var role in roles) + { + claims.Add(new Claim(ClaimTypes.Role, role)); + } + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey)); + var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: _issuer, + audience: _audience, + claims: claims, + expires: DateTime.UtcNow.AddHours(1), + signingCredentials: credentials + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + public ClaimsPrincipal? ValidateToken(string token) + { + try + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.UTF8.GetBytes(_secretKey); + + var tokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = true, + ValidIssuer = _issuer, + ValidateAudience = true, + ValidAudience = _audience, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; + + var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out _); + return principal; + } + catch + { + return null; + } + } +} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TestApp/TestAuthorizeHub.cs b/ManagedCode.Orleans.Identity.Tests/TestApp/TestAuthorizeHub.cs index b643c16..c0046d0 100644 --- a/ManagedCode.Orleans.Identity.Tests/TestApp/TestAuthorizeHub.cs +++ b/ManagedCode.Orleans.Identity.Tests/TestApp/TestAuthorizeHub.cs @@ -1,13 +1,53 @@ +using ManagedCode.Orleans.Identity.Tests.Cluster.Grains; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; +using Orleans; namespace ManagedCode.Orleans.Identity.Tests.TestApp; [Authorize] public class TestAuthorizeHub : Hub { - public Task DoTest() + private readonly IClusterClient _clusterClient; + + public TestAuthorizeHub(IClusterClient clusterClient) + { + _clusterClient = clusterClient; + } + + public async Task GetUserInfo() + { + try + { + var userId = Context.User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "anonymous"; + var userGrain = _clusterClient.GetGrain(userId); + return await userGrain.GetUser(); + } + catch (UnauthorizedAccessException) + { + throw new HubException("Forbidden: Access denied"); + } + } + + [Authorize(Roles = "admin")] + public async Task GetAdminInfo() + { + var userId = Context.User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "anonymous"; + var userGrain = _clusterClient.GetGrain(userId); + return await userGrain.GetAdminInfo(); + } + + public async Task GetPublicInfo() { - return Task.FromResult(new Random().Next()); + try + { + var userId = Context.User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "anonymous"; + var userGrain = _clusterClient.GetGrain(userId); + return await userGrain.GetPublicInfo(); + } + catch (UnauthorizedAccessException) + { + throw new HubException("Forbidden: Access denied"); + } } } \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/BaseTokenGrainTests.cs b/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/BaseTokenGrainTests.cs deleted file mode 100644 index 1c94dd8..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/BaseTokenGrainTests.cs +++ /dev/null @@ -1,188 +0,0 @@ -using FluentAssertions; -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Core.Models; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using ManagedCode.Orleans.Identity.Tests.Helpers; -using Orleans.Runtime; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests.TokenGrainTests -{ - [Collection(nameof(TestClusterApplication))] - public abstract class BaseTokenGrainTests - where TGrain : IBaseTokenGrain - { - protected readonly ITestOutputHelper _outputHelper; - protected readonly TestClusterApplication _testApp; - - protected BaseTokenGrainTests(TestClusterApplication testApp, ITestOutputHelper outputHelper) - { - _outputHelper = outputHelper; - _testApp = testApp; - } - - #region CreateToken - - [Fact] - public virtual async Task CreateToken_WhenValueIsValid_ReturnSuccess() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(); - var tokenGrain = _testApp.Cluster.Client.GetGrain(createTokenModel.Value); - - // Act - var result = await tokenGrain.CreateAsync(createTokenModel); - - // Assert - var token = await tokenGrain.GetTokenAsync(); - result.IsSuccess.Should().BeTrue(); - token.IsSuccess.Should().BeTrue(); - token.Value.Should().NotBeNull(); - token.Value.Value.Should().Be(createTokenModel.Value); - } - - [Fact] - public virtual async Task CreateToken_WhenTokensLifetimeLessThanMinute_ReturnSuccess() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(TimeSpan.FromSeconds(30)); - var tokenGrain = _testApp.Cluster.Client.GetGrain(createTokenModel.Value); - - // Act - var result = await tokenGrain.CreateAsync(createTokenModel); - - // Assert - var token = await tokenGrain.GetTokenAsync(); - result.IsSuccess.Should().BeTrue(); - token.IsSuccess.Should().BeTrue(); - token.Value.Should().NotBeNull(); - token.Value.Value.Should().Be(createTokenModel.Value); - } - - [Fact] - public virtual async Task CreateToken_WhenTokensValueIsEmpty_ReturnFail() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(string.Empty); - var tokenGrain = _testApp.Cluster.Client.GetGrain(createTokenModel.Value); - - // Act - var result = await tokenGrain.CreateAsync(createTokenModel); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public virtual async Task CreateToken_WhenTokensValueIsNull_ReturnFail() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(null); - var tokenGrain = _testApp.Cluster.Client.GetGrain(createTokenModel.Value); - - // Act - var result = await tokenGrain.CreateAsync(createTokenModel); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public virtual async Task CreateToken_WhenTokensValueIsWhiteSpace_ReturnFail() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(" "); - var tokenGrain = _testApp.Cluster.Client.GetGrain(createTokenModel.Value); - - // Act - var result = await tokenGrain.CreateAsync(createTokenModel); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - [Fact] - public virtual async Task CreateToken_WhenTokensLifetimeIsZero_ReturnFail() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(TimeSpan.Zero); - var tokenGrain = _testApp.Cluster.Client.GetGrain(createTokenModel.Value); - - // Act - var result = await tokenGrain.CreateAsync(createTokenModel); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - #endregion - - #region GetToken - - [Fact] - public async Task GetToken_WhenTokenExists_ReturnSuccess() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(); - var tokenGrain = _testApp.Cluster.Client.GetGrain(createTokenModel.Value); - await tokenGrain.CreateAsync(createTokenModel); - - // Act - var result = await tokenGrain.GetTokenAsync(); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Value.Should().Be(createTokenModel.Value); - } - - [Fact] - public async Task GetToken_WhenTokenDoesntExist_ReturnFail() - { - // Arrange - var randValue = Guid.NewGuid().ToString(); - var tokenGrain = _testApp.Cluster.Client.GetGrain(randValue); - - // Act - var result = await tokenGrain.GetTokenAsync(); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - #endregion - - #region VerifyToken - - [Fact] - public async Task VerifyToken_WhenTokenExists_ReturnSuccess() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(); - var tokenGrain = _testApp.Cluster.Client.GetGrain(createTokenModel.Value); - await tokenGrain.CreateAsync(createTokenModel); - - // Act - var result = await tokenGrain.VerifyAsync(); - - // Arrange - result.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task VerifyToken_WhenTokenDoesNotExists_ReturnFail() - { - // Arrange - var randomValue = Guid.NewGuid().ToString(); - var tokenGrain = _testApp.Cluster.Client.GetGrain(randomValue); - - // Act - var result = await tokenGrain.VerifyAsync(); - - // Assert - result.IsFailed.Should().BeTrue(); - } - - #endregion - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/CodeVerificationTokenGrainTests.cs b/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/CodeVerificationTokenGrainTests.cs deleted file mode 100644 index dc4489c..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/CodeVerificationTokenGrainTests.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests.TokenGrainTests; - -[Collection(nameof(TestClusterApplication))] -public class CodeVerificationTokenGrainTests : BaseTokenGrainTests -{ - public CodeVerificationTokenGrainTests(TestClusterApplication testApp, ITestOutputHelper outputHelper) : base(testApp, outputHelper) - { - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/EmailVerificationTokenGrainTests.cs b/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/EmailVerificationTokenGrainTests.cs deleted file mode 100644 index 5d90ae1..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/EmailVerificationTokenGrainTests.cs +++ /dev/null @@ -1,15 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests.TokenGrainTests -{ - [Collection(nameof(TestClusterApplication))] - public class EmailVerificationTokenGrainTests : BaseTokenGrainTests - { - public EmailVerificationTokenGrainTests(TestClusterApplication testApp, ITestOutputHelper outputHelper) : base(testApp, outputHelper) - { - } - } -} diff --git a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/MagicLinkTokenGrainTests.cs b/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/MagicLinkTokenGrainTests.cs deleted file mode 100644 index 4dbb5ae..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/MagicLinkTokenGrainTests.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests.TokenGrainTests; - -[Collection(nameof(TestClusterApplication))] -public class MagicLinkTokenGrainTests : BaseTokenGrainTests -{ - public MagicLinkTokenGrainTests(TestClusterApplication testApp, ITestOutputHelper outputHelper) : base(testApp, outputHelper) - { - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/RemainderTests/BaseTokenGrainReminderTests.cs b/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/RemainderTests/BaseTokenGrainReminderTests.cs deleted file mode 100644 index bc8d9a5..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/RemainderTests/BaseTokenGrainReminderTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using FluentAssertions; -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using ManagedCode.Orleans.Identity.Tests.Helpers; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests.TokenGrainTests.RemainderTests; - -[Collection(nameof(TestClusterApplication))] -public abstract class BaseTokenGrainReminderTests - where TGrain : IBaseTokenGrain -{ - protected readonly ITestOutputHelper _outputHelper; - protected readonly TestClusterApplication _testApp; - - protected BaseTokenGrainReminderTests(TestClusterApplication testApp, ITestOutputHelper outputHelper) - { - _outputHelper = outputHelper; - _testApp = testApp; - } - - #region Reminder - - [Fact] - public async Task ExecuteReminder_WhenTokenExists_DeleteToken() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(); - var tokenGrain = _testApp.Cluster.Client.GetGrain(createTokenModel.Value); - await tokenGrain.CreateAsync(createTokenModel); - - // Act - await Task.Delay(TimeSpan.FromMinutes(3)); - - // Assert - var result = await tokenGrain.VerifyAsync(); - result.IsFailed.Should().BeTrue(); - } - - #endregion - - #region Timer - - [Fact] - public async Task ExecuteTimer_WhenTokenExists_DeleteToken() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(TimeSpan.FromSeconds(30)); - var tokenGrain = _testApp.Cluster.Client.GetGrain(createTokenModel.Value); - await tokenGrain.CreateAsync(createTokenModel); - - // Act - await Task.Delay(TimeSpan.FromMinutes(2)); - - // Assert - var result = await tokenGrain.VerifyAsync(); - result.IsFailed.Should().BeTrue(); - } - - #endregion -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/RemainderTests/EmailVerificationGrainReminderTests.cs b/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/RemainderTests/EmailVerificationGrainReminderTests.cs deleted file mode 100644 index 866e585..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/RemainderTests/EmailVerificationGrainReminderTests.cs +++ /dev/null @@ -1,15 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests.TokenGrainTests.RemainderTests; - -[Collection(nameof(TestClusterApplication))] -public class EmailVerificationGrainReminderTests : BaseTokenGrainReminderTests -{ - public EmailVerificationGrainReminderTests(TestClusterApplication testApp, ITestOutputHelper outputHelper) - : base(testApp, outputHelper) - { - } -} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/BaseUserGrainsTests.cs b/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/BaseUserGrainsTests.cs deleted file mode 100644 index a68281a..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/BaseUserGrainsTests.cs +++ /dev/null @@ -1,191 +0,0 @@ -using FluentAssertions; -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Core.Models; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces.UserGrains; -using ManagedCode.Orleans.Identity.Tests.Helpers; -using Orleans.Runtime; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests.TokenGrainTests.UserGrainTests -{ - [Collection(nameof(TestClusterApplication))] - public abstract class BaseUserGrainsTests - where TTokenGrain : IBaseTokenGrain - where TUserGrain : IBaseTestUserGrain - { - protected readonly ITestOutputHelper _outputHelper; - protected readonly TestClusterApplication _testApp; - - public BaseUserGrainsTests(ITestOutputHelper outputHelper, TestClusterApplication testApp) - { - _outputHelper = outputHelper; - _testApp = testApp; - } - - #region Create TokenGrain methods - - private async ValueTask CreateAndGetTokenGrainAsync(CreateTokenModel createTokenModel) - { - var tokenGrain = _testApp.Cluster.Client.GetGrain(createTokenModel.Value); - await tokenGrain.CreateAsync(createTokenModel); - return tokenGrain; - } - - protected async Task CreateTokenAsync(CreateTokenModel createTokenModel) - { - var tokenGrain = await CreateAndGetTokenGrainAsync(createTokenModel); - return tokenGrain; - } - - - #endregion - - #region CreateToken_VerifyToken - - [Fact] - public virtual async Task VerifyToken_WhenTokenIsNotExpired_ReturnSuccess() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(); - var tokenGrain = await CreateTokenAsync(createTokenModel); - var userGrainId = createTokenModel.UserGrainId.Key.ToString(); - await tokenGrain.VerifyAsync(); - var userGrain = _testApp.Cluster.Client.GetGrain(userGrainId); - - // Act - var result = await userGrain.IsTokenValid(); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().BeTrue(); - } - - [Fact] - public virtual async Task VerifyToken_WhenTokenExpired_ReturnTrue() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(TimeSpan.FromSeconds(40)); - var tokenGrain = await CreateTokenAsync(createTokenModel); - var userGrainId = createTokenModel.UserGrainId.Key.ToString(); - var userGrain = _testApp.Cluster.Client.GetGrain(userGrainId); - - await Task.Delay(TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(20))); - - // Act - var result = await userGrain.IsTokenExpired(); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().BeTrue(); - } - - [Fact] - public virtual async Task VerifyToken_WhenTokenDoesNotExists_ReturnFail() - { - // Arrange - var tokenValue = Guid.NewGuid().ToString(); - var userId = Guid.NewGuid().ToString(); - var tokenGrain = _testApp.Cluster.GrainFactory.GetGrain(tokenValue); - var userGrain = _testApp.Cluster.GrainFactory.GetGrain(userId); - - // Act - await tokenGrain.VerifyAsync(); - var result = await userGrain.IsTokenValid(); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().BeFalse(); - } - - [Fact] - public virtual async Task VerifyToken_WhenUserGrainIsDefault_ReturnSuccess() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(); - createTokenModel.UserGrainId = new GrainId(); - var tokenGrain = await CreateTokenAsync(createTokenModel); - - // Act - var result = await tokenGrain.VerifyAsync(); - - // Assert - result.IsSuccess.Should().BeTrue(); - } - - #endregion - - #region NotifyUserGrain_WhenTokenIsExpired - - [Fact] - public virtual async Task NotifyUserGrain_WhenTokenExpiredWithReminder_ReturnSuccess() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(30))); - var tokenGrain = await CreateTokenAsync(createTokenModel); - var userGrain = _testApp.Cluster.GrainFactory.GetGrain(createTokenModel.UserGrainId.Key.ToString()); - - // Act - await Task.Delay(TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(34))); - var result = await userGrain.IsTokenExpired(); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().BeTrue(); - } - - [Fact] - public virtual async Task NotifyUserGrain_WhenTokenExpiredWithTimer_ReturnSuccess() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(TimeSpan.FromSeconds(30)); - var tokenGrain = await CreateTokenAsync(createTokenModel); - var userGrain = _testApp.Cluster.GrainFactory.GetGrain(createTokenModel.UserGrainId.Key.ToString()); - - // Act - await Task.Delay(TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(10))); - var result = await userGrain.IsTokenExpired(); - - // Assert - result.IsSuccess.Should().BeTrue(); - result.Value.Should().BeTrue(); - } - - [Fact] - public async Task NotifyUserGrain_WhenTokenExpiredAndUserGrainIdIsDefault_ReturnSuccess() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(); - createTokenModel.UserGrainId = new GrainId(); - var tokenGrain = await CreateTokenAsync(createTokenModel); - - // Act - await Task.Delay(TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(10))); - var result = await tokenGrain.VerifyAsync(); - - // Assert - result.IsSuccess.Should().BeTrue(); - } - - [Fact] - public virtual async Task NotifyUserGrainWithTimer_WhenTokenExpiredAndUserGrainIdIsDefault_ReturnFail() - { - // Arrange - var createTokenModel = TokenHelper.GenerateCreateTestTokenModel(TimeSpan.FromSeconds(30)); - createTokenModel.UserGrainId = new GrainId(); - var tokenGrain = await CreateTokenAsync(createTokenModel); - - // Act - await Task.Delay(TimeSpan.FromMinutes(1).Add(TimeSpan.FromSeconds(10))); - var result = await tokenGrain.VerifyAsync(); - - // Assert - result.IsSuccess.Should().BeFalse(); - result.IsFailed.Should().BeTrue(); - } - - #endregion - } - -} diff --git a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/CodeVerificationTokenUserGrainTests.cs b/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/CodeVerificationTokenUserGrainTests.cs deleted file mode 100644 index b6c4a89..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/CodeVerificationTokenUserGrainTests.cs +++ /dev/null @@ -1,16 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces.UserGrains; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests.TokenGrainTests.UserGrainTests -{ - [Collection(nameof(TestClusterApplication))] - public class CodeVerificationTokenUserGrainTests : BaseUserGrainsTests - { - public CodeVerificationTokenUserGrainTests(ITestOutputHelper outputHelper, TestClusterApplication testApp) : base(outputHelper, testApp) - { - } - } -} diff --git a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/EmailVerificationUserGrainTests.cs b/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/EmailVerificationUserGrainTests.cs deleted file mode 100644 index 36696b1..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/EmailVerificationUserGrainTests.cs +++ /dev/null @@ -1,17 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Core.Interfaces.UserGrains; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces.UserGrains; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests.TokenGrainTests.UserGrainTests -{ - [Collection(nameof(TestClusterApplication))] - public class EmailVerificationUserGrainTests : BaseUserGrainsTests - { - public EmailVerificationUserGrainTests(ITestOutputHelper outputHelper, TestClusterApplication testApp) : base(outputHelper, testApp) - { - } - } -} diff --git a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/MagicLinkTokenUserGrainTests.cs b/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/MagicLinkTokenUserGrainTests.cs deleted file mode 100644 index b1093b2..0000000 --- a/ManagedCode.Orleans.Identity.Tests/TokenGrainTests/UserGrainTests/MagicLinkTokenUserGrainTests.cs +++ /dev/null @@ -1,15 +0,0 @@ -using ManagedCode.Orleans.Identity.Core.Interfaces.TokenGrains; -using ManagedCode.Orleans.Identity.Tests.Cluster; -using ManagedCode.Orleans.Identity.Tests.Cluster.Grains.Interfaces.UserGrains; -using Xunit; -using Xunit.Abstractions; - -namespace ManagedCode.Orleans.Identity.Tests.TokenGrainTests.UserGrainTests; - -[Collection(nameof(TestClusterApplication))] -public class MagicLinkTokenUserGrainTests : BaseUserGrainsTests -{ - public MagicLinkTokenUserGrainTests(ITestOutputHelper outputHelper, TestClusterApplication testApp) : base(outputHelper, testApp) - { - } -} \ No newline at end of file diff --git a/README.md b/README.md index f1c5da9..5896372 100644 --- a/README.md +++ b/README.md @@ -2,43 +2,124 @@ # Orleans.Identity -[![.NET](https://github.com/managed-code-hub/Identity/actions/workflows/dotnet.yml/badge.svg)](https://github.com/managed-code-hub/Identity/actions/workflows/dotnet.yml) -[![nuget](https://github.com/managed-code-hub/Identity/actions/workflows/nuget.yml/badge.svg?branch=main)](https://github.com/managed-code-hub/Identity/actions/workflows/nuget.yml) -[![CodeQL](https://github.com/managed-code-hub/Identity/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](https://github.com/managed-code-hub/Identity/actions/workflows/codeql-analysis.yml) +A simplified Orleans library for handling authorization context propagation from ASP.NET Core controllers and SignalR hubs to Orleans grains. -| Version | Package | Description | -|--------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|-------------| -| [![NuGet Package](https://img.shields.io/nuget/v/ManagedCode.Identity.Core.svg)](https://www.nuget.org/packages/ManagedCode.Identity.Core) | [ManagedCode.Identity.Core](https://www.nuget.org/packages/ManagedCode.Identity.Core) | Core | +## Overview -## Orleans.Identity +This library provides a simple way to pass user authorization context from your ASP.NET Core application to Orleans grains, allowing you to implement authorization at the grain level using standard ASP.NET Core authorization attributes. -Orleans.Identity is a library for managing authorization and authentication in ASP.NET Identity applications using -Orleans. -It provides a simple, easy-to-use interface for managing user accounts and securing access to your application's -resources. +## Features -With Orleans.Identity, you can easily add support for user registration, login, logout, and password management to your -ASP.NET Identity applications. Additionally, Orleans.Identity provides support for managing user sessions, ensuring that -user data is kept secure and accessed only by authorized users. +- **JWT-based authentication**: Works with standard JWT tokens +- **Controller authorization**: Automatically passes user claims to grains called from controllers +- **SignalR authorization**: Supports authorization in SignalR hubs +- **Grain-level authorization**: Use `[Authorize]` and `[Authorize(Roles = "RoleName")]` attributes on grains +- **Simple grain extension**: Use `this.GetCurrentUser().Claims` to access user claims in grains -## Features +## Quick Start + +### 1. Orleans Cluster Setup + +```csharp +var builder = Host.CreateDefaultBuilder(args) + .UseOrleans(siloBuilder => + { + siloBuilder + .UseLocalhostClustering() + .AddOrleansIdentity() // Add the authorization filter + .ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(YourGrain).Assembly)); + }); +``` + +### 2. ASP.NET Core API Setup + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Add Orleans client +builder.Services.AddOrleansClient(client => +{ + client.UseLocalhostClustering(); +}); + +// Add Orleans Identity +builder.Services.AddOrleansIdentity(); + +// Add JWT authentication +builder.Services.AddAuthentication("Bearer") + .AddJwtBearer("Bearer", options => { /* JWT configuration */ }); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); +app.UseOrleansIdentity(); // Add the middleware -- Easy integration with ASP.NET Identity applications -- Support for user registration, login, logout, and password management -- Secure session management +app.MapControllers(); +app.MapHub("/hub"); +``` + +### 3. Using in Grains + +```csharp +[Authorize] +public class MyGrain : Grain, IMyGrain +{ + [AllowAnonymous] + public Task GetPublicInfo() + { + return Task.FromResult("Public info"); + } + + [Authorize] + public Task GetUserInfo() + { + var user = this.GetCurrentUser(); + var username = user.FindFirst(ClaimTypes.Name)?.Value; + return Task.FromResult($"Hello, {username}!"); + } + + [Authorize(Roles = "Admin")] + public Task GetAdminInfo() + { + return Task.FromResult("Admin only info"); + } +} +``` + +## Testing + +The library includes comprehensive integration tests in the `ManagedCode.Orleans.Identity.Tests` project that demonstrate: + +- JWT token generation and validation +- Controller → Grain authorization flow +- SignalR → Grain authorization flow - Role-based access control -- Support for multi-tenancy +- Grain authorization with user claims + +### Running Tests + +```bash +dotnet test +``` + +### Test Structure + +The tests use the existing integration test infrastructure with: +- **TestApp**: ASP.NET Core application with controllers and SignalR hubs +- **Cluster**: Orleans test cluster with grains +- **Integration Tests**: Comprehensive tests covering all scenarios -## Getting Started +## Architecture -To use Orleans.Identity in your ASP.NET Identity application, follow these steps: +The library works by: -Install the ```ManagedCode.Orleans.Identity``` NuGet package in your project: +1. **Middleware**: Extracts user claims from JWT tokens and stores them in Orleans `RequestContext` +2. **SignalR Filter**: Handles authorization in SignalR hubs and stores claims in `RequestContext` +3. **Grain Filter**: Intercepts grain calls and validates authorization based on `[Authorize]` attributes +4. **Grain Extension**: Provides `this.GetCurrentUser()` method to access claims in grains -## Motivation +## License -The motivation for creating Orleans.Identity is to provide a scalable and performant solution for managing user sessions -and authentication in ASP.NET applications. Orleans provides a powerful actor model that makes it easy to implement -concurrency and scalability in distributed systems. By leveraging the power of Orleans, Orleans.Identity enables you to -easily add authentication and authorization to your ASP.NET applications without sacrificing performance. +MIT License From bfb71cb1d849b2cfee8bb7aa84b9514434a6b827 Mon Sep 17 00:00:00 2001 From: VladKovtun99 Date: Wed, 30 Jul 2025 13:48:29 +0200 Subject: [PATCH 2/5] Added fixes. Data flow now with ClaimPrincipal, direct exceptions, no string literals --- .../Extensions/OrleansIdentityExtensions.cs | 3 - .../Middlewares/OrleansContextMiddleware.cs | 9 +-- .../Middlewares/SignalRAuthorizationFilter.cs | 14 +---- .../Constants/OrleansIdentityConstants.cs | 1 + .../Extensions/GrainExtensions.cs | 33 +--------- .../Serializations/ClaimSurrogate.cs | 27 +++----- .../Serializations/ClaimSurrogateConverter.cs | 4 +- .../ClaimsPrincipalSurrogate.cs | 17 +++++ .../ClaimsPrincipalSurrogateConverter.cs | 26 ++++++++ .../GrainAuthorizationIncomingFilter.cs | 62 +++++++------------ .../Cluster/Grains/UserGrain.cs | 25 -------- 11 files changed, 83 insertions(+), 138 deletions(-) create mode 100644 ManagedCode.Orleans.Identity.Core/Serializations/ClaimsPrincipalSurrogate.cs create mode 100644 ManagedCode.Orleans.Identity.Core/Serializations/ClaimsPrincipalSurrogateConverter.cs diff --git a/ManagedCode.Orleans.Identity.Client/Extensions/OrleansIdentityExtensions.cs b/ManagedCode.Orleans.Identity.Client/Extensions/OrleansIdentityExtensions.cs index e107870..7268b96 100644 --- a/ManagedCode.Orleans.Identity.Client/Extensions/OrleansIdentityExtensions.cs +++ b/ManagedCode.Orleans.Identity.Client/Extensions/OrleansIdentityExtensions.cs @@ -9,10 +9,8 @@ public static class OrleansIdentityExtensions { public static IServiceCollection AddOrleansIdentity(this IServiceCollection services) { - // Add middleware for storing claims in RequestContext services.AddScoped(); - // Add SignalR filter for authorization services.AddSignalR(options => { options.AddFilter(); @@ -23,7 +21,6 @@ public static IServiceCollection AddOrleansIdentity(this IServiceCollection serv public static IApplicationBuilder UseOrleansIdentity(this IApplicationBuilder app) { - // Add middleware to the pipeline app.UseMiddleware(); return app; diff --git a/ManagedCode.Orleans.Identity.Client/Middlewares/OrleansContextMiddleware.cs b/ManagedCode.Orleans.Identity.Client/Middlewares/OrleansContextMiddleware.cs index 443f966..efa4539 100644 --- a/ManagedCode.Orleans.Identity.Client/Middlewares/OrleansContextMiddleware.cs +++ b/ManagedCode.Orleans.Identity.Client/Middlewares/OrleansContextMiddleware.cs @@ -1,8 +1,8 @@ using System.Security.Claims; using System.Threading.Tasks; -using System.Linq; using Microsoft.AspNetCore.Http; using Orleans.Runtime; +using ManagedCode.Orleans.Identity.Core.Constants; namespace ManagedCode.Orleans.Identity.Client.Middlewares; @@ -12,12 +12,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) { if (context.User.Identity?.IsAuthenticated == true) { - // Store user claims in Orleans RequestContext as serializable dictionary - // Group claims by type to handle multiple values (like roles) - var claims = context.User.Claims - .GroupBy(c => c.Type) - .ToDictionary(g => g.Key, g => string.Join(",", g.Select(c => c.Value))); - RequestContext.Set("UserClaims", claims); + RequestContext.Set(OrleansIdentityConstants.USER_CLAIMS, context.User); } await next(context); diff --git a/ManagedCode.Orleans.Identity.Client/Middlewares/SignalRAuthorizationFilter.cs b/ManagedCode.Orleans.Identity.Client/Middlewares/SignalRAuthorizationFilter.cs index 02fb67d..f826c67 100644 --- a/ManagedCode.Orleans.Identity.Client/Middlewares/SignalRAuthorizationFilter.cs +++ b/ManagedCode.Orleans.Identity.Client/Middlewares/SignalRAuthorizationFilter.cs @@ -1,8 +1,8 @@ using System; using System.Threading.Tasks; -using System.Linq; using Microsoft.AspNetCore.SignalR; using Orleans.Runtime; +using ManagedCode.Orleans.Identity.Core.Constants; namespace ManagedCode.Orleans.Identity.Client.Middlewares; @@ -14,12 +14,7 @@ public class SignalRAuthorizationFilter : IHubFilter { if (invocationContext.Context.User?.Identity?.IsAuthenticated == true) { - // Store user claims in Orleans RequestContext as serializable dictionary - // Group claims by type to handle multiple values (like roles) - var claims = invocationContext.Context.User.Claims - .GroupBy(c => c.Type) - .ToDictionary(g => g.Key, g => string.Join(",", g.Select(c => c.Value))); - RequestContext.Set("UserClaims", claims); + RequestContext.Set(OrleansIdentityConstants.USER_CLAIMS, invocationContext.Context.User); } return await next(invocationContext); @@ -29,10 +24,7 @@ public Task OnConnectedAsync(HubLifetimeContext context, Func c.Type) - .ToDictionary(g => g.Key, g => string.Join(",", g.Select(c => c.Value))); - RequestContext.Set("UserClaims", claims); + RequestContext.Set(OrleansIdentityConstants.USER_CLAIMS, context.Context.User); } return next(context); diff --git a/ManagedCode.Orleans.Identity.Core/Constants/OrleansIdentityConstants.cs b/ManagedCode.Orleans.Identity.Core/Constants/OrleansIdentityConstants.cs index d220d43..27f6870 100644 --- a/ManagedCode.Orleans.Identity.Core/Constants/OrleansIdentityConstants.cs +++ b/ManagedCode.Orleans.Identity.Core/Constants/OrleansIdentityConstants.cs @@ -6,4 +6,5 @@ public static class OrleansIdentityConstants public static string AUTH_TOKEN = "AUTH-TOKEN"; public static string AUTHENTICATION_TYPE = "MC-OrleansIdentity"; public const string SESSION_ID_CLAIM_NAME = "SessionId"; + public const string USER_CLAIMS = "UserClaims"; } \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Core/Extensions/GrainExtensions.cs b/ManagedCode.Orleans.Identity.Core/Extensions/GrainExtensions.cs index 382f607..f8fbada 100644 --- a/ManagedCode.Orleans.Identity.Core/Extensions/GrainExtensions.cs +++ b/ManagedCode.Orleans.Identity.Core/Extensions/GrainExtensions.cs @@ -1,9 +1,8 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Claims; using Orleans; using Orleans.Runtime; +using ManagedCode.Orleans.Identity.Core.Constants; namespace ManagedCode.Orleans.Identity.Core.Extensions; @@ -11,33 +10,7 @@ public static class GrainExtensions { public static ClaimsPrincipal GetCurrentUser(this Grain grain) { - var requestContext = RequestContext.Get("UserClaims"); - if (requestContext is Dictionary claimsDict) - { - var claims = new List(); - foreach (var kvp in claimsDict) - { - // Handle comma-separated values (like roles) - var values = kvp.Value.Split(',', StringSplitOptions.RemoveEmptyEntries); - foreach (var value in values) - { - claims.Add(new Claim(kvp.Key, value.Trim())); - } - } - var identity = new ClaimsIdentity(claims, "JWT"); - return new ClaimsPrincipal(identity); - } - - return new ClaimsPrincipal(new ClaimsIdentity()); - } - - public static bool IsAuthorizationFailed(this Grain grain) - { - return RequestContext.Get("AuthorizationFailed") is bool failed && failed; - } - - public static string? GetAuthorizationMessage(this Grain grain) - { - return RequestContext.Get("AuthorizationMessage") as string; + var requestContext = RequestContext.Get(OrleansIdentityConstants.USER_CLAIMS); + return requestContext as ClaimsPrincipal ?? new ClaimsPrincipal(new ClaimsIdentity()); } } \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Core/Serializations/ClaimSurrogate.cs b/ManagedCode.Orleans.Identity.Core/Serializations/ClaimSurrogate.cs index 293280a..40c46d0 100644 --- a/ManagedCode.Orleans.Identity.Core/Serializations/ClaimSurrogate.cs +++ b/ManagedCode.Orleans.Identity.Core/Serializations/ClaimSurrogate.cs @@ -4,37 +4,24 @@ namespace ManagedCode.Orleans.Identity.Core.Serializations; // This is the surrogate which will act as a stand-in for the foreign type. -// Surrogates should use plain fields instead of properties for better perfomance. +// Surrogates should use plain fields instead of properties for better performance. [GenerateSerializer] -public struct ClaimSurrogate +public struct ClaimSurrogate(string type, string value, string valueType, string issuer, string originalIssuer) { - public ClaimSurrogate(string type, string value, string valueType, string issuer, string originalIssuer, ClaimsIdentity? subject) - { - Issuer = issuer; - OriginalIssuer = originalIssuer; - Subject = subject; - Type = type; - Value = value; - ValueType = valueType; - } - [Id(0)] - public string Issuer { get; set; } + public string Issuer { get; set; } = issuer; [Id(1)] - public string OriginalIssuer { get; set; } + public string OriginalIssuer { get; set; } = originalIssuer; [Id(2)] - public ClaimsIdentity? Subject { get; set; } + public string Type { get; set; } = type; [Id(3)] - public string Type { get; set; } + public string Value { get; set; } = value; [Id(4)] - public string Value { get; set; } - - [Id(5)] - public string ValueType { get; set; } + public string ValueType { get; set; } = valueType; } // This is a converter which converts between the surrogate and the foreign type. \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Core/Serializations/ClaimSurrogateConverter.cs b/ManagedCode.Orleans.Identity.Core/Serializations/ClaimSurrogateConverter.cs index 8b4a705..ad7aaeb 100644 --- a/ManagedCode.Orleans.Identity.Core/Serializations/ClaimSurrogateConverter.cs +++ b/ManagedCode.Orleans.Identity.Core/Serializations/ClaimSurrogateConverter.cs @@ -8,11 +8,11 @@ public sealed class ClaimSurrogateConverter : IConverter { public Claim ConvertFromSurrogate(in ClaimSurrogate surrogate) { - return new Claim(surrogate.Type, surrogate.Value, surrogate.ValueType, surrogate.Issuer, surrogate.OriginalIssuer, surrogate.Subject); + return new Claim(surrogate.Type, surrogate.Value, surrogate.ValueType, surrogate.Issuer, surrogate.OriginalIssuer); } public ClaimSurrogate ConvertToSurrogate(in Claim value) { - return new ClaimSurrogate(value.Type, value.Value, value.ValueType, value.Issuer, value.OriginalIssuer, value.Subject); + return new ClaimSurrogate(value.Type, value.Value, value.ValueType, value.Issuer, value.OriginalIssuer); } } \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Core/Serializations/ClaimsPrincipalSurrogate.cs b/ManagedCode.Orleans.Identity.Core/Serializations/ClaimsPrincipalSurrogate.cs new file mode 100644 index 0000000..4c131ef --- /dev/null +++ b/ManagedCode.Orleans.Identity.Core/Serializations/ClaimsPrincipalSurrogate.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Security.Claims; +using Orleans; + +namespace ManagedCode.Orleans.Identity.Core.Serializations; + +// This is the surrogate which will act as a stand-in for the foreign type. +// Surrogates should use plain fields instead of properties for better performance. +[GenerateSerializer] +public struct ClaimsPrincipalSurrogate(List? identities, string? primaryIdentityType) +{ + [Id(0)] + public List? Identities { get; set; } = identities; + + [Id(1)] + public string? PrimaryIdentityType { get; set; } = primaryIdentityType; +} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Core/Serializations/ClaimsPrincipalSurrogateConverter.cs b/ManagedCode.Orleans.Identity.Core/Serializations/ClaimsPrincipalSurrogateConverter.cs new file mode 100644 index 0000000..d89b7b5 --- /dev/null +++ b/ManagedCode.Orleans.Identity.Core/Serializations/ClaimsPrincipalSurrogateConverter.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Orleans; + +namespace ManagedCode.Orleans.Identity.Core.Serializations; + +[RegisterConverter] +public sealed class ClaimsPrincipalSurrogateConverter : IConverter +{ + public ClaimsPrincipal ConvertFromSurrogate(in ClaimsPrincipalSurrogate surrogate) + { + if (surrogate.Identities == null || surrogate.Identities.Count == 0) + { + return new ClaimsPrincipal(); + } + + return new ClaimsPrincipal(surrogate.Identities); + } + + public ClaimsPrincipalSurrogate ConvertToSurrogate(in ClaimsPrincipal value) + { + var identities = value.Identities?.ToList(); + return new ClaimsPrincipalSurrogate(identities, value.Identity?.AuthenticationType); + } +} \ No newline at end of file diff --git a/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs b/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs index bd87e93..7aa0719 100644 --- a/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs +++ b/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Authorization; using Orleans; using Orleans.Runtime; +using ManagedCode.Orleans.Identity.Core.Constants; namespace ManagedCode.Orleans.Identity.Server.GrainCallFilter; @@ -19,33 +20,33 @@ public async Task Invoke(IIncomingGrainCallContext context) var user = GetUserFromRequestContext(); var isAuthorized = false; - if (user?.Identity?.IsAuthenticated == true) + if (user?.Identity?.IsAuthenticated == false) { - if (attributes.All(attribute => string.IsNullOrWhiteSpace(attribute.Roles))) - { - isAuthorized = true; - } - else - { - var userRoles = user.FindAll(ClaimTypes.Role).Select(c => c.Value).ToHashSet(); + throw new UnauthorizedAccessException("Access denied. User is not authenticated or does not have required roles."); + } + + if (attributes.All(attribute => string.IsNullOrWhiteSpace(attribute.Roles))) + { + isAuthorized = true; + } + else + { + var userRoles = user.FindAll(ClaimTypes.Role).Select(c => c.Value).ToHashSet(); - foreach (var attribute in attributes) + foreach (var attribute in attributes) + { + var requiredRoles = attribute.Roles?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(); + if (requiredRoles.Any(role => userRoles.Contains(role.Trim()))) { - var requiredRoles = attribute.Roles?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(); - if (requiredRoles.Any(role => userRoles.Contains(role.Trim()))) - { - isAuthorized = true; - break; - } + isAuthorized = true; + break; } } } if (!isAuthorized) { - // Set authorization failure flag instead of throwing exception - RequestContext.Set("AuthorizationFailed", true); - RequestContext.Set("AuthorizationMessage", "Access denied. User is not authenticated or does not have required roles."); + throw new UnauthorizedAccessException("Access denied. User is not authenticated or does not have required roles."); } } @@ -54,44 +55,25 @@ public async Task Invoke(IIncomingGrainCallContext context) private static ClaimsPrincipal? GetUserFromRequestContext() { - var requestContext = RequestContext.Get("UserClaims"); - if (requestContext is Dictionary claimsDict) - { - var claims = new List(); - foreach (var kvp in claimsDict) - { - // Handle comma-separated values (like roles) - var values = kvp.Value.Split(',', StringSplitOptions.RemoveEmptyEntries); - foreach (var value in values) - { - claims.Add(new Claim(kvp.Key, value.Trim())); - } - } - var identity = new ClaimsIdentity(claims, "JWT"); - return new ClaimsPrincipal(identity); - } - - return null; + var requestContext = RequestContext.Get(OrleansIdentityConstants.USER_CLAIMS); + return requestContext as ClaimsPrincipal; } private static bool IsGrainAuthorized(MemberInfo methodInfo, out List attributes) { - attributes = new List(); + attributes = []; - // Check for AllowAnonymous on method if (Attribute.IsDefined(methodInfo, typeof(AllowAnonymousAttribute))) { return false; } - // Check for Authorize on class if (methodInfo.DeclaringType != null && Attribute.IsDefined(methodInfo.DeclaringType, typeof(AuthorizeAttribute))) { attributes.AddRange(Attribute.GetCustomAttributes(methodInfo.DeclaringType, typeof(AuthorizeAttribute)) .Cast()); } - // Check for Authorize on method if (Attribute.IsDefined(methodInfo, typeof(AuthorizeAttribute))) { attributes.AddRange(Attribute.GetCustomAttributes(methodInfo, typeof(AuthorizeAttribute)) diff --git a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrain.cs b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrain.cs index 4d86a14..3114f84 100644 --- a/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrain.cs +++ b/ManagedCode.Orleans.Identity.Tests/Cluster/Grains/UserGrain.cs @@ -10,11 +10,6 @@ public class UserGrain : Grain, IUserGrain [Authorize] public Task GetUser() { - if (this.IsAuthorizationFailed()) - { - throw new UnauthorizedAccessException(this.GetAuthorizationMessage() ?? "Access denied"); - } - var user = this.GetCurrentUser(); var username = user.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "Unknown"; return Task.FromResult($"Hello, {username}!"); @@ -23,11 +18,6 @@ public Task GetUser() [Authorize(Roles = TestRoles.ADMIN)] public Task BanUser() { - if (this.IsAuthorizationFailed()) - { - throw new UnauthorizedAccessException(this.GetAuthorizationMessage() ?? "Access denied"); - } - var user = this.GetCurrentUser(); var username = user.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "Unknown"; return Task.FromResult($"User {username} is banned"); @@ -36,11 +26,6 @@ public Task BanUser() [Authorize(Roles = TestRoles.ADMIN)] public Task GetAdminInfo() { - if (this.IsAuthorizationFailed()) - { - throw new UnauthorizedAccessException(this.GetAuthorizationMessage() ?? "Access denied"); - } - var user = this.GetCurrentUser(); var username = user.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "Unknown"; return Task.FromResult($"Admin info for {username}: You have admin privileges"); @@ -55,11 +40,6 @@ public Task GetPublicInfo() [Authorize(Roles = TestRoles.MODERATOR)] public Task ModifyUser() { - if (this.IsAuthorizationFailed()) - { - throw new UnauthorizedAccessException(this.GetAuthorizationMessage() ?? "Access denied"); - } - var user = this.GetCurrentUser(); var username = user.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "Unknown"; return Task.FromResult($"User {username} has been modified"); @@ -67,11 +47,6 @@ public Task ModifyUser() public Task AddToList() { - if (this.IsAuthorizationFailed()) - { - throw new UnauthorizedAccessException(this.GetAuthorizationMessage() ?? "Access denied"); - } - var user = this.GetCurrentUser(); var username = user.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value ?? "Unknown"; return Task.FromResult($"User {username} added to list"); From 9f62ae67298aa467c9711ec2ad099ff54d017e41 Mon Sep 17 00:00:00 2001 From: VladKovtun99 Date: Wed, 30 Jul 2025 14:54:36 +0200 Subject: [PATCH 3/5] Added fixes from copilot --- .../Serializations/ClaimsIdentitySurrogate.cs | 2 +- .../GrainAuthorizationIncomingFilter.cs | 36 +++++++++++-------- .../TestApp/Controllers/UserController.cs | 10 +++--- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/ManagedCode.Orleans.Identity.Core/Serializations/ClaimsIdentitySurrogate.cs b/ManagedCode.Orleans.Identity.Core/Serializations/ClaimsIdentitySurrogate.cs index 73ec22d..eebfa4b 100644 --- a/ManagedCode.Orleans.Identity.Core/Serializations/ClaimsIdentitySurrogate.cs +++ b/ManagedCode.Orleans.Identity.Core/Serializations/ClaimsIdentitySurrogate.cs @@ -5,7 +5,7 @@ namespace ManagedCode.Orleans.Identity.Core.Serializations; // This is the surrogate which will act as a stand-in for the foreign type. -// Surrogates should use plain fields instead of properties for better perfomance. +// Surrogates should use plain fields instead of properties for better performance. [GenerateSerializer] public struct ClaimsIdentitySurrogate { diff --git a/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs b/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs index 7aa0719..8ffe1f4 100644 --- a/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs +++ b/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs @@ -19,12 +19,18 @@ public async Task Invoke(IIncomingGrainCallContext context) { var user = GetUserFromRequestContext(); var isAuthorized = false; - - if (user?.Identity?.IsAuthenticated == false) + + if (user == null) { - throw new UnauthorizedAccessException("Access denied. User is not authenticated or does not have required roles."); + throw new UnauthorizedAccessException("Access denied. User is missing."); } - + + if (user.Identity?.IsAuthenticated == false) + { + throw new UnauthorizedAccessException( + "Access denied. User is not authenticated or does not have required roles."); + } + if (attributes.All(attribute => string.IsNullOrWhiteSpace(attribute.Roles))) { isAuthorized = true; @@ -32,21 +38,22 @@ public async Task Invoke(IIncomingGrainCallContext context) else { var userRoles = user.FindAll(ClaimTypes.Role).Select(c => c.Value).ToHashSet(); - + foreach (var attribute in attributes) { - var requiredRoles = attribute.Roles?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(); - if (requiredRoles.Any(role => userRoles.Contains(role.Trim()))) - { - isAuthorized = true; - break; - } + var requiredRoles = attribute.Roles?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? []; + + if (!requiredRoles.Any(role => userRoles.Contains(role.Trim()))) continue; + + isAuthorized = true; + break; } } - + if (!isAuthorized) { - throw new UnauthorizedAccessException("Access denied. User is not authenticated or does not have required roles."); + throw new UnauthorizedAccessException( + "Access denied. User is not authenticated or does not have required roles."); } } @@ -68,7 +75,8 @@ private static bool IsGrainAuthorized(MemberInfo methodInfo, out List()); diff --git a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/UserController.cs b/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/UserController.cs index fb2fad7..750a1af 100644 --- a/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/UserController.cs +++ b/ManagedCode.Orleans.Identity.Tests/TestApp/Controllers/UserController.cs @@ -67,8 +67,9 @@ public async Task> GetPublicInfo() { try { - // For public endpoints, use a default grain ID since user might not be authenticated - var userId = User.Identity?.IsAuthenticated == true ? User.GetGrainId() : "public"; + // For public endpoints, use a unique grain ID for unauthenticated users + var userId = User.Identity?.IsAuthenticated == true ? User.GetGrainId() : Guid.NewGuid().ToString(); + var userGrain = _clusterClient.GetGrain(userId); var result = await userGrain.GetPublicInfo(); return result; @@ -101,8 +102,9 @@ public async Task> AddToList() { try { - // For anonymous endpoints, use a default grain ID since user might not be authenticated - var userId = User.Identity?.IsAuthenticated == true ? User.GetGrainId() : "anonymous"; + // For anonymous endpoints, generate a unique grain ID for unauthenticated users + var userId = User.Identity?.IsAuthenticated == true ? User.GetGrainId() : Guid.NewGuid().ToString(); + var userGrain = _clusterClient.GetGrain(userId); return await userGrain.AddToList(); } From 0f67a27dd8945bd36e02d0252d58833a3b70b6e9 Mon Sep 17 00:00:00 2001 From: VladKovtun99 Date: Wed, 30 Jul 2025 16:15:49 +0200 Subject: [PATCH 4/5] Small fix of foreach loop. --- .../GrainAuthorizationIncomingFilter.cs | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs b/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs index 8ffe1f4..7c76019 100644 --- a/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs +++ b/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs @@ -31,26 +31,15 @@ public async Task Invoke(IIncomingGrainCallContext context) "Access denied. User is not authenticated or does not have required roles."); } - if (attributes.All(attribute => string.IsNullOrWhiteSpace(attribute.Roles))) - { - isAuthorized = true; - } - else - { - var userRoles = user.FindAll(ClaimTypes.Role).Select(c => c.Value).ToHashSet(); - foreach (var attribute in attributes) - { - var requiredRoles = attribute.Roles?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? []; - - if (!requiredRoles.Any(role => userRoles.Contains(role.Trim()))) continue; - - isAuthorized = true; - break; - } - } + var userRoles = user.FindAll(ClaimTypes.Role).Select(c => c.Value).ToHashSet(); - if (!isAuthorized) + if (!(attributes.All(attribute => string.IsNullOrWhiteSpace(attribute.Roles)) || + attributes.Any(attribute => + { + var requiredRoles = attribute.Roles?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? []; + return requiredRoles.Any(role => userRoles.Contains(role.Trim())); + }))) { throw new UnauthorizedAccessException( "Access denied. User is not authenticated or does not have required roles."); From ca46b472a43f672f84deb6f2e565e723bfcedb55 Mon Sep 17 00:00:00 2001 From: VladKovtun99 Date: Wed, 30 Jul 2025 18:13:01 +0200 Subject: [PATCH 5/5] Removed unnecessary variable --- .../GrainCallFilter/GrainAuthorizationIncomingFilter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs b/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs index 7c76019..96fd728 100644 --- a/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs +++ b/ManagedCode.Orleans.Identity.Server/GrainCallFilter/GrainAuthorizationIncomingFilter.cs @@ -18,7 +18,6 @@ public async Task Invoke(IIncomingGrainCallContext context) if (IsGrainAuthorized(context.ImplementationMethod, out var attributes)) { var user = GetUserFromRequestContext(); - var isAuthorized = false; if (user == null) {