diff --git a/SecurityService.BusinessLogic/Oidc/OidcCommands.cs b/SecurityService.BusinessLogic/Oidc/OidcCommands.cs new file mode 100644 index 00000000..d139f7d0 --- /dev/null +++ b/SecurityService.BusinessLogic/Oidc/OidcCommands.cs @@ -0,0 +1,13 @@ +using MediatR; +using Microsoft.AspNetCore.Http; +using SimpleResults; + +namespace SecurityService.BusinessLogic.Oidc; + +public static class OidcCommands +{ + public sealed record AuthorizeCommand(HttpContext HttpContext) : IRequest>; + public sealed record TokenCommand(HttpContext HttpContext) : IRequest>; + public sealed record LogoutCommand(HttpContext HttpContext) : IRequest>; + public sealed record UserInfoCommand(HttpContext HttpContext) : IRequest>; +} diff --git a/SecurityService/Oidc/OidcHelpers.cs b/SecurityService.BusinessLogic/Oidc/OidcHelpers.cs similarity index 85% rename from SecurityService/Oidc/OidcHelpers.cs rename to SecurityService.BusinessLogic/Oidc/OidcHelpers.cs index 0cf8894a..93f8a8df 100644 --- a/SecurityService/Oidc/OidcHelpers.cs +++ b/SecurityService.BusinessLogic/Oidc/OidcHelpers.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Primitives; @@ -9,7 +10,7 @@ using SecurityService.Database.Entities; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace SecurityService.Oidc; +namespace SecurityService.BusinessLogic.Oidc; public static class OidcHelpers { @@ -38,7 +39,7 @@ public static string AppendQueryValues(string url, string name, IEnumerable ReadMultiValue(IQueryCollection query, string key) => query.TryGetValue(key, out StringValues values) ? values.Where(value => value is not null).Cast().ToArray() : Array.Empty(); - public static async Task CreatePrincipalAsync( + public static async Task CreatePrincipal( ApplicationUser user, UserManager userManager, IEnumerable scopes, @@ -116,15 +117,15 @@ public static IEnumerable GetDestinations(Claim claim) }; } - public static async Task<(IReadOnlyCollection IdentityScopes, IReadOnlyCollection ApiScopes)> BuildScopeDisplayAsync( + public static async Task<(IReadOnlyCollection IdentityScopes, IReadOnlyCollection ApiScopes)> BuildScopeDisplay( OpenIddictRequest request, SecurityServiceDbContext dbContext, CancellationToken cancellationToken) { - return await BuildScopeDisplayAsync(request.GetScopes(), dbContext, cancellationToken); + return await BuildScopeDisplay(request.GetScopes(), dbContext, cancellationToken); } - public static async Task<(IReadOnlyCollection IdentityScopes, IReadOnlyCollection ApiScopes)> BuildScopeDisplayAsync( + public static async Task<(IReadOnlyCollection IdentityScopes, IReadOnlyCollection ApiScopes)> BuildScopeDisplay( IEnumerable scopeNames, SecurityServiceDbContext dbContext, CancellationToken cancellationToken) @@ -156,6 +157,25 @@ public static IEnumerable GetDestinations(Claim claim) return (identityScopes, apiScopes); } + + public static async Task> ResolveClientCredentialsScopes( + OpenIddictRequest request, + SecurityServiceDbContext dbContext, + CancellationToken cancellationToken) + { + IReadOnlyCollection grantedScopes = request.GetScopes().ToArray(); + if (grantedScopes.Count > 0) + { + return grantedScopes; + } + + var clientDefinition = await dbContext.ClientDefinitions + .SingleOrDefaultAsync(client => client.ClientId == request.ClientId, cancellationToken); + + return clientDefinition is null + ? Array.Empty() + : JsonListSerializer.Deserialize(clientDefinition.AllowedScopesJson); + } } public sealed record ScopeDisplayItem(string Name, string DisplayName, string? Description, bool Required, bool Emphasize); diff --git a/SecurityService.BusinessLogic/Oidc/OidcRequestHandler.cs b/SecurityService.BusinessLogic/Oidc/OidcRequestHandler.cs new file mode 100644 index 00000000..bb967483 --- /dev/null +++ b/SecurityService.BusinessLogic/Oidc/OidcRequestHandler.cs @@ -0,0 +1,357 @@ +using System.Collections.Immutable; +using System.Security.Claims; +using MediatR; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; +using OpenIddict.Validation.AspNetCore; +using SecurityService.Database; +using SecurityService.Database.DbContexts; +using SimpleResults; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace SecurityService.BusinessLogic.Oidc; + +public sealed class OidcRequestHandler : + IRequestHandler>, + IRequestHandler>, + IRequestHandler>, + IRequestHandler> +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictAuthorizationManager _authorizationManager; + private readonly IOpenIddictScopeManager _scopeManager; + private readonly SecurityServiceDbContext _dbContext; + + public OidcRequestHandler( + UserManager userManager, + SignInManager signInManager, + IOpenIddictApplicationManager applicationManager, + IOpenIddictAuthorizationManager authorizationManager, + IOpenIddictScopeManager scopeManager, + SecurityServiceDbContext dbContext) + { + this._userManager = userManager; + this._signInManager = signInManager; + this._applicationManager = applicationManager; + this._authorizationManager = authorizationManager; + this._scopeManager = scopeManager; + this._dbContext = dbContext; + } + + public async Task> Handle(OidcCommands.AuthorizeCommand command, CancellationToken cancellationToken) + { + var context = command.HttpContext; + var request = context.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + var currentRequestUrl = OidcHelpers.BuildCurrentRequestUrl(context.Request); + + var authenticationResult = await context.AuthenticateAsync(IdentityConstants.ApplicationScheme); + if (authenticationResult.Succeeded == false) + { + return Result.Success(HandleAuthenticationFailed(request, currentRequestUrl)); + } + + var user = await this._userManager.GetUserAsync(authenticationResult.Principal); + if (user is null) + { + return Result.Success(AuthorizeForbidServer(Errors.LoginRequired, "The user account could not be resolved.")); + } + + var application = await this._applicationManager.FindByClientIdAsync(request.ClientId!, cancellationToken); + if (application is null) + { + return Result.Success(new AuthorizeBadRequestResult(new { error = "invalid_client", error_description = "The client application could not be found." })); + } + + if (context.Request.Query.TryGetValue("consent", out var consentDecision)) + { + return Result.Success(await this.HandleConsentDecision(user, request, application, context, consentDecision!, cancellationToken)); + } + + return Result.Success(await this.HandleConsentType(user, request, application, currentRequestUrl, cancellationToken)); + } + + public async Task> Handle(OidcCommands.TokenCommand command, CancellationToken cancellationToken) + { + var context = command.HttpContext; + var request = context.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + + if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType() || request.IsDeviceCodeGrantType()) + { + return Result.Success(await this.HandleCodeOrRefreshToken(context, cancellationToken)); + } + + if (request.IsClientCredentialsGrantType()) + { + return Result.Success(await this.HandleClientCredentials(request, cancellationToken)); + } + + if (request.IsPasswordGrantType()) + { + return Result.Success(await this.HandlePasswordGrant(request, cancellationToken)); + } + + return Result.Success(new TokenBadRequestResult(new { error = "unsupported_grant_type", error_description = "The specified grant type is not supported by this service." })); + } + + public Task> Handle(OidcCommands.LogoutCommand command, CancellationToken cancellationToken) + { + var context = command.HttpContext; + var currentRequestUrl = OidcHelpers.BuildCurrentRequestUrl(context.Request); + var confirmed = string.Equals(context.Request.Query["logout"], "confirmed", StringComparison.OrdinalIgnoreCase); + if (confirmed) + { + LogoutCommandResult signOut = new LogoutSignOutResult( + new AuthenticationProperties { RedirectUri = "/Account/Logout/LoggedOut" }, + [IdentityConstants.ApplicationScheme, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme]); + return Task.FromResult(Result.Success(signOut)); + } + + LogoutCommandResult redirect = new LogoutRedirectResult($"/Account/Logout?returnUrl={Uri.EscapeDataString(currentRequestUrl)}"); + return Task.FromResult(Result.Success(redirect)); + } + + public async Task> Handle(OidcCommands.UserInfoCommand command, CancellationToken cancellationToken) + { + var context = command.HttpContext; + var authenticationResult = await context.AuthenticateAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + if (authenticationResult.Succeeded == false || authenticationResult.Principal is null) + { + return Result.Success(InvalidToken("The access token is missing or invalid.")); + } + + var subject = authenticationResult.Principal.GetClaim(Claims.Subject); + if (string.IsNullOrWhiteSpace(subject)) + { + return Result.Success(InvalidToken("The access token does not contain a subject identifier.")); + } + + var user = await this._userManager.FindByIdAsync(subject); + if (user is null) + { + return Result.Success(InvalidToken("The user associated with the token could not be found.")); + } + + var scopes = authenticationResult.Principal.GetScopes().ToHashSet(StringComparer.OrdinalIgnoreCase); + var response = await this.BuildUserInfoResponse(user, scopes); + return Result.Success(new UserInfoJsonResult(response.Where(pair => pair.Value is not null).ToDictionary(pair => pair.Key, pair => pair.Value))); + } + + private static AuthorizeCommandResult HandleAuthenticationFailed(OpenIddictRequest request, string currentRequestUrl) + { + if (request.HasPromptValue(PromptValues.None)) + { + return AuthorizeForbidServer(Errors.LoginRequired, "The user is not logged in."); + } + + return new AuthorizeChallengeResult(new AuthenticationProperties { RedirectUri = currentRequestUrl }, [IdentityConstants.ApplicationScheme]); + } + + private async Task HandleConsentDecision( + ApplicationUser user, + OpenIddictRequest request, + object application, + HttpContext context, + string consentDecision, + CancellationToken cancellationToken) + { + if (string.Equals(consentDecision, "denied", StringComparison.OrdinalIgnoreCase)) + { + return AuthorizeForbidServer(Errors.AccessDenied, "The authorization request was denied."); + } + + if (string.Equals(consentDecision, "accepted", StringComparison.OrdinalIgnoreCase)) + { + return await this.CompleteAuthorization(user, request, application, cancellationToken, OidcHelpers.ReadMultiValue(context.Request.Query, "granted_scope")); + } + + return await this.HandleConsentType(user, request, application, OidcHelpers.BuildCurrentRequestUrl(context.Request), cancellationToken); + } + + private async Task HandleConsentType( + ApplicationUser user, + OpenIddictRequest request, + object application, + string currentRequestUrl, + CancellationToken cancellationToken) + { + var consentType = await this._applicationManager.GetConsentTypeAsync(application, cancellationToken) ?? ConsentTypes.Implicit; + var existingAuthorizations = await this._authorizationManager.FindAsync( + subject: user.Id, + client: await this._applicationManager.GetIdAsync(application, cancellationToken), + status: Statuses.Valid, + type: AuthorizationTypes.Permanent, + scopes: ImmutableArray.CreateRange(request.GetScopes()), + cancellationToken: cancellationToken).ToListAsync(cancellationToken); + + if (string.Equals(consentType, ConsentTypes.Explicit, StringComparison.OrdinalIgnoreCase) && existingAuthorizations.Count == 0) + { + if (request.HasPromptValue(PromptValues.None)) + { + return AuthorizeForbidServer(Errors.ConsentRequired, "Interactive user consent is required."); + } + + return new AuthorizeRedirectResult($"/Consent?returnUrl={Uri.EscapeDataString(currentRequestUrl)}"); + } + + return await this.CompleteAuthorization(user, request, application, cancellationToken, request.GetScopes()); + } + + private async Task HandleCodeOrRefreshToken(HttpContext context, CancellationToken cancellationToken) + { + var authenticationResult = await context.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + if (authenticationResult.Succeeded == false || authenticationResult.Principal is null) + { + return InvalidGrant("The specified token is invalid."); + } + + var subject = authenticationResult.Principal.GetClaim(Claims.Subject); + if (string.IsNullOrWhiteSpace(subject)) + { + return new TokenSignInResult(authenticationResult.Principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + var user = await this._userManager.FindByIdAsync(subject); + if (user is null || await this._signInManager.CanSignInAsync(user) == false) + { + return InvalidGrant("The token is no longer valid."); + } + + var scopes = authenticationResult.Principal.GetScopes(); + var resources = await this._scopeManager.ListResourcesAsync(ImmutableArray.CreateRange(scopes), cancellationToken).ToListAsync(cancellationToken); + var principal = await OidcHelpers.CreatePrincipal(user, this._userManager, scopes, resources, authenticationResult.Principal.GetAuthorizationId()); + return new TokenSignInResult(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + private async Task HandleClientCredentials(OpenIddictRequest request, CancellationToken cancellationToken) + { + var grantedScopes = await OidcHelpers.ResolveClientCredentialsScopes(request, this._dbContext, cancellationToken); + var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType, Claims.Name, Claims.Role); + identity.SetClaim(Claims.Subject, request.ClientId!) + .SetClaim(Claims.Name, request.ClientId!); + identity.SetScopes(grantedScopes); + identity.SetResources(await this._scopeManager.ListResourcesAsync(ImmutableArray.CreateRange(grantedScopes), cancellationToken).ToListAsync(cancellationToken)); + identity.SetDestinations(OidcHelpers.GetDestinations); + return new TokenSignInResult(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + private async Task HandlePasswordGrant(OpenIddictRequest request, CancellationToken cancellationToken) + { + var user = await this._userManager.FindByNameAsync(request.Username!); + if (user is null) + { + return InvalidGrant(); + } + + var signInResult = await this._signInManager.CheckPasswordSignInAsync(user, request.Password!, lockoutOnFailure: true); + if (signInResult.Succeeded == false) + { + return InvalidGrant(); + } + + var grantedScopes = await OidcHelpers.ResolveClientCredentialsScopes(request, this._dbContext, cancellationToken); + var resources = await this._scopeManager.ListResourcesAsync(ImmutableArray.CreateRange(grantedScopes), cancellationToken).ToListAsync(cancellationToken); + var principal = await OidcHelpers.CreatePrincipal(user, this._userManager, grantedScopes, resources, authorizationId: null); + return new TokenSignInResult(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + private async Task> BuildUserInfoResponse(ApplicationUser user, HashSet scopes) + { + var response = new Dictionary { [Claims.Subject] = user.Id }; + + if (scopes.Contains(Scopes.Profile)) + { + response[Claims.Name] = user.UserName; + response[Claims.PreferredUsername] = user.UserName; + response[Claims.GivenName] = user.GivenName; + response[Claims.FamilyName] = user.FamilyName; + response["middle_name"] = user.MiddleName; + } + + if (scopes.Contains(Scopes.Email)) + { + response[Claims.Email] = user.Email; + response[Claims.EmailVerified] = user.EmailConfirmed; + } + + if (scopes.Contains(Scopes.Roles)) + { + response[Claims.Role] = (await this._userManager.GetRolesAsync(user)).ToArray(); + } + + foreach (var claim in await this._userManager.GetClaimsAsync(user)) + { + if (response.ContainsKey(claim.Type) == false) + { + response[claim.Type] = claim.Value; + } + } + + return response; + } + + private static AuthorizeForbidResult AuthorizeForbidServer(string error, string description) => + new AuthorizeForbidResult( + new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = error, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = description + }), + [OpenIddictServerAspNetCoreDefaults.AuthenticationScheme]); + + private async Task CompleteAuthorization( + ApplicationUser user, + OpenIddictRequest request, + object application, + CancellationToken cancellationToken, + IReadOnlyCollection grantedScopes) + { + var resources = await this._scopeManager.ListResourcesAsync(ImmutableArray.CreateRange(grantedScopes), cancellationToken).ToListAsync(cancellationToken); + var authorizations = await this._authorizationManager.FindAsync( + subject: user.Id, + client: await this._applicationManager.GetIdAsync(application, cancellationToken), + status: Statuses.Valid, + type: AuthorizationTypes.Permanent, + scopes: ImmutableArray.CreateRange(grantedScopes), + cancellationToken: cancellationToken).ToListAsync(cancellationToken); + + var authorization = authorizations.LastOrDefault(); + var principal = await OidcHelpers.CreatePrincipal(user, this._userManager, grantedScopes, resources, authorizationId: null); + + authorization ??= await this._authorizationManager.CreateAsync( + principal: principal, + subject: user.Id, + client: (await this._applicationManager.GetIdAsync(application, cancellationToken))!, + type: AuthorizationTypes.Permanent, + scopes: ImmutableArray.CreateRange(grantedScopes), + cancellationToken: cancellationToken); + + principal.SetAuthorizationId(await this._authorizationManager.GetIdAsync(authorization, cancellationToken)); + return new AuthorizeSignInResult(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + private static TokenForbidResult InvalidGrant(string description = "The username/password couple is invalid.") => + new TokenForbidResult( + new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = description + }), + [OpenIddictServerAspNetCoreDefaults.AuthenticationScheme]); + + private static UserInfoChallengeResult InvalidToken(string description) => + new UserInfoChallengeResult( + new AuthenticationProperties(new Dictionary + { + [OpenIddictValidationAspNetCoreConstants.Properties.Error] = Errors.InvalidToken, + [OpenIddictValidationAspNetCoreConstants.Properties.ErrorDescription] = description + }), + [OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme]); +} diff --git a/SecurityService.BusinessLogic/Oidc/OidcResults.cs b/SecurityService.BusinessLogic/Oidc/OidcResults.cs new file mode 100644 index 00000000..722bbdee --- /dev/null +++ b/SecurityService.BusinessLogic/Oidc/OidcResults.cs @@ -0,0 +1,44 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; + +namespace SecurityService.BusinessLogic.Oidc; + +// ---- Authorize endpoint ---- + +public abstract record AuthorizeCommandResult; + +public sealed record AuthorizeSignInResult(ClaimsPrincipal Principal, string AuthenticationScheme) : AuthorizeCommandResult; + +public sealed record AuthorizeRedirectResult(string Url) : AuthorizeCommandResult; + +public sealed record AuthorizeForbidResult(AuthenticationProperties Properties, IList AuthenticationSchemes) : AuthorizeCommandResult; + +public sealed record AuthorizeChallengeResult(AuthenticationProperties Properties, IList AuthenticationSchemes) : AuthorizeCommandResult; + +public sealed record AuthorizeBadRequestResult(object Error) : AuthorizeCommandResult; + +// ---- Token endpoint ---- + +public abstract record TokenCommandResult; + +public sealed record TokenSignInResult(ClaimsPrincipal Principal, string AuthenticationScheme) : TokenCommandResult; + +public sealed record TokenForbidResult(AuthenticationProperties Properties, IList AuthenticationSchemes) : TokenCommandResult; + +public sealed record TokenBadRequestResult(object Error) : TokenCommandResult; + +// ---- Logout endpoint ---- + +public abstract record LogoutCommandResult; + +public sealed record LogoutSignOutResult(AuthenticationProperties Properties, IList AuthenticationSchemes) : LogoutCommandResult; + +public sealed record LogoutRedirectResult(string Url) : LogoutCommandResult; + +// ---- UserInfo endpoint ---- + +public abstract record UserInfoCommandResult; + +public sealed record UserInfoJsonResult(Dictionary Data) : UserInfoCommandResult; + +public sealed record UserInfoChallengeResult(AuthenticationProperties Properties, IList AuthenticationSchemes) : UserInfoCommandResult; diff --git a/SecurityService.BusinessLogic/SecurityService.BusinessLogic.csproj b/SecurityService.BusinessLogic/SecurityService.BusinessLogic.csproj index cb1e0812..eb781937 100644 --- a/SecurityService.BusinessLogic/SecurityService.BusinessLogic.csproj +++ b/SecurityService.BusinessLogic/SecurityService.BusinessLogic.csproj @@ -11,6 +11,8 @@ + + diff --git a/SecurityService.UnitTests/Oidc/OidcEndpointTests.cs b/SecurityService.UnitTests/Oidc/OidcEndpointTests.cs index 82a3fea2..aee7b456 100644 --- a/SecurityService.UnitTests/Oidc/OidcEndpointTests.cs +++ b/SecurityService.UnitTests/Oidc/OidcEndpointTests.cs @@ -1,14 +1,14 @@ using System.Security.Claims; -using System.Text; +using MediatR; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Moq; using OpenIddict.Validation.AspNetCore; +using SecurityService.BusinessLogic.Oidc; using SecurityService.Database; using SecurityService.Database.DbContexts; using SecurityService.Database.Entities; -using SecurityService.Oidc; using SecurityService.UnitTests.Infrastructure; using Shouldly; @@ -44,27 +44,39 @@ public async Task UserInfoAsync_WithValidPrincipal_ReturnsJsonPayload() var context = new DefaultHttpContext { - RequestServices = services.BuildServiceProvider(), - Response = - { - Body = new MemoryStream() - } + RequestServices = services.BuildServiceProvider() }; - var result = await OidcEndpoints.UserInfoAsync(context, userManager.Object); - await result.ExecuteAsync(context); + var signInManager = IdentityMocks.CreateSignInManager(userManager); + var appManager = new Mock(); + var authManager = new Mock(); + var scopeManager = new Mock(); + using var provider = TestServiceProviderFactory.Create(nameof(this.UserInfoAsync_WithValidPrincipal_ReturnsJsonPayload)); + using var scope = provider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); - context.Response.StatusCode.ShouldBe(StatusCodes.Status200OK); - context.Response.Body.Position = 0; - var payload = await new StreamReader(context.Response.Body, Encoding.UTF8).ReadToEndAsync(); - payload.ShouldContain("\"sub\":\"user-1\""); - payload.ShouldContain("\"email\":\"alice@example.com\""); + var handler = new OidcRequestHandler( + userManager.Object, + signInManager.Object, + appManager.Object, + authManager.Object, + scopeManager.Object, + dbContext); + + var result = await handler.Handle(new OidcCommands.UserInfoCommand(context), CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + var jsonResult = result.Data.ShouldBeOfType(); + jsonResult.Data.ShouldContainKey("sub"); + jsonResult.Data["sub"].ShouldBe("user-1"); + jsonResult.Data.ShouldContainKey("email"); + jsonResult.Data["email"].ShouldBe("alice@example.com"); } [Fact] - public async Task TokenAsync_ClientCredentialsWithoutRequestedScopes_FallsBackToConfiguredClientScopes() + public async Task ResolveClientCredentialsScopes_WithoutRequestedScopes_FallsBackToConfiguredClientScopes() { - using var provider = TestServiceProviderFactory.Create(nameof(this.TokenAsync_ClientCredentialsWithoutRequestedScopes_FallsBackToConfiguredClientScopes)); + using var provider = TestServiceProviderFactory.Create(nameof(this.ResolveClientCredentialsScopes_WithoutRequestedScopes_FallsBackToConfiguredClientScopes)); using var scope = provider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); @@ -88,7 +100,7 @@ public async Task TokenAsync_ClientCredentialsWithoutRequestedScopes_FallsBackTo ClientId = "serviceClient" }; - var scopes = await OidcEndpoints.ResolveClientCredentialsScopesAsync( + var scopes = await OidcHelpers.ResolveClientCredentialsScopes( request, dbContext, CancellationToken.None); @@ -97,7 +109,7 @@ public async Task TokenAsync_ClientCredentialsWithoutRequestedScopes_FallsBackTo } [Fact] - public async Task CreatePrincipalAsync_WhenUserClaimsDuplicateBuiltInClaims_DoesNotDuplicateValues() + public async Task CreatePrincipal_WhenUserClaimsDuplicateBuiltInClaims_DoesNotDuplicateValues() { var userManager = IdentityMocks.CreateUserManager(); var user = new ApplicationUser @@ -118,7 +130,7 @@ public async Task CreatePrincipalAsync_WhenUserClaimsDuplicateBuiltInClaims_Does new Claim(OpenIddictConstants.Claims.Role, "Merchant") ]); - var principal = await OidcHelpers.CreatePrincipalAsync( + var principal = await OidcHelpers.CreatePrincipal( user, userManager.Object, [OpenIddictConstants.Scopes.Email, OpenIddictConstants.Scopes.Profile, OpenIddictConstants.Scopes.Roles], diff --git a/SecurityService/Factories/OidcResponseFactory.cs b/SecurityService/Factories/OidcResponseFactory.cs new file mode 100644 index 00000000..71db600d --- /dev/null +++ b/SecurityService/Factories/OidcResponseFactory.cs @@ -0,0 +1,105 @@ +using Microsoft.AspNetCore.Authentication; +using SecurityService.BusinessLogic.Oidc; +using SimpleResults; +using System.Net; +using System.Security.Claims; +using Shared.Results.Web; + +namespace SecurityService.Factories; + +public static class OidcResponseFactory +{ + public static IResult FromResult(Result result) + { + if (result.IsSuccess == false) { + return TranslateResultStatus(result); + } + + return result.Data switch + { + AuthorizeSignInResult r => Results.SignIn(r.Principal, properties: null, authenticationScheme: r.AuthenticationScheme), + AuthorizeRedirectResult r => Results.Redirect(r.Url), + AuthorizeForbidResult r => Results.Forbid(r.Properties, r.AuthenticationSchemes), + AuthorizeChallengeResult r => Results.Challenge(r.Properties, r.AuthenticationSchemes), + AuthorizeBadRequestResult r => Results.BadRequest(r.Error), + TokenSignInResult r => Results.SignIn(r.Principal, properties: null, authenticationScheme: r.AuthenticationScheme), + TokenForbidResult r => Results.Forbid(r.Properties, r.AuthenticationSchemes), + TokenBadRequestResult r => Results.BadRequest(r.Error), + LogoutSignOutResult r => Results.SignOut(r.Properties, r.AuthenticationSchemes), + LogoutRedirectResult r => Results.Redirect(r.Url), + UserInfoJsonResult r => Results.Json(r.Data), + UserInfoChallengeResult r => Results.Challenge(r.Properties, r.AuthenticationSchemes), + _ => Results.StatusCode(500) + }; + } + + internal static IResult TranslateResultStatus(ResultBase result) + { + ErrorResponse errorResponse = new() + { + Errors = result.Errors.Any() switch + { + true => result.Errors.ToList(), + _ => [result.Message], + } + }; + + return result.Status switch + { + ResultStatus.Invalid => Microsoft.AspNetCore.Http.Results.BadRequest(errorResponse), + ResultStatus.NotFound => Microsoft.AspNetCore.Http.Results.NotFound(errorResponse), + ResultStatus.Unauthorized => Microsoft.AspNetCore.Http.Results.Unauthorized(), + ResultStatus.Conflict => Microsoft.AspNetCore.Http.Results.Conflict(errorResponse), + ResultStatus.Failure => Microsoft.AspNetCore.Http.Results.InternalServerError(errorResponse), + ResultStatus.CriticalError => Microsoft.AspNetCore.Http.Results.InternalServerError(errorResponse), + ResultStatus.Forbidden => Microsoft.AspNetCore.Http.Results.Forbid(), + _ => Microsoft.AspNetCore.Http.Results.StatusCode((int)HttpStatusCode.NotImplemented) + }; + } + + //public static IResult FromResult(Result result) + //{ + // if (result.IsSuccess == false) + // { + // return TranslateResultStatus(result); + // } + + // return result.Data switch + // { + // TokenSignInResult r => Results.SignIn(r.Principal, properties: null, authenticationScheme: r.AuthenticationScheme), + // TokenForbidResult r => Results.Forbid(r.Properties, r.AuthenticationSchemes), + // TokenBadRequestResult r => Results.BadRequest(r.Error), + // _ => Results.StatusCode(500) + // }; + //} + + //public static IResult FromResult(Result result) + //{ + // if (result.IsSuccess == false) + // { + // return TranslateResultStatus(result); + // } + + // return result.Data switch + // { + // LogoutSignOutResult r => Results.SignOut(r.Properties, r.AuthenticationSchemes), + // LogoutRedirectResult r => Results.Redirect(r.Url), + // _ => Results.StatusCode(500) + // }; + //} + + //public static IResult FromResult(Result result) + //{ + // if (result.IsSuccess == false) + // { + // return TranslateResultStatus(result); + // } + + // return result.Data switch + // { + // UserInfoJsonResult r => Results.Json(r.Data), + // UserInfoChallengeResult r => Results.Challenge(r.Properties, r.AuthenticationSchemes), + // _ => Results.StatusCode(500) + // }; + //} +} diff --git a/SecurityService/Oidc/Handlers/AuthorizeHandler.cs b/SecurityService/Oidc/Handlers/AuthorizeHandler.cs new file mode 100644 index 00000000..0334b608 --- /dev/null +++ b/SecurityService/Oidc/Handlers/AuthorizeHandler.cs @@ -0,0 +1,11 @@ +using MediatR; +using SecurityService.BusinessLogic.Oidc; +using SecurityService.Factories; + +namespace SecurityService.Oidc.Handlers; + +public static class AuthorizeHandler +{ + public static async Task AuthorizeAsync(HttpContext context, IMediator mediator, CancellationToken cancellationToken) + => OidcResponseFactory.FromResult(await mediator.Send(new OidcCommands.AuthorizeCommand(context), cancellationToken)); +} diff --git a/SecurityService/Oidc/Handlers/LogoutHandler.cs b/SecurityService/Oidc/Handlers/LogoutHandler.cs new file mode 100644 index 00000000..efc34f87 --- /dev/null +++ b/SecurityService/Oidc/Handlers/LogoutHandler.cs @@ -0,0 +1,11 @@ +using MediatR; +using SecurityService.BusinessLogic.Oidc; +using SecurityService.Factories; + +namespace SecurityService.Oidc.Handlers; + +public static class LogoutHandler +{ + public static async Task LogoutAsync(HttpContext context, IMediator mediator, CancellationToken cancellationToken) + => OidcResponseFactory.FromResult(await mediator.Send(new OidcCommands.LogoutCommand(context), cancellationToken)); +} diff --git a/SecurityService/Oidc/Handlers/TokenHandler.cs b/SecurityService/Oidc/Handlers/TokenHandler.cs new file mode 100644 index 00000000..bdba6eb6 --- /dev/null +++ b/SecurityService/Oidc/Handlers/TokenHandler.cs @@ -0,0 +1,11 @@ +using MediatR; +using SecurityService.BusinessLogic.Oidc; +using SecurityService.Factories; + +namespace SecurityService.Oidc.Handlers; + +public static class TokenHandler +{ + public static async Task TokenAsync(HttpContext context, IMediator mediator, CancellationToken cancellationToken) + => OidcResponseFactory.FromResult(await mediator.Send(new OidcCommands.TokenCommand(context), cancellationToken)); +} diff --git a/SecurityService/Oidc/Handlers/UserInfoHandler.cs b/SecurityService/Oidc/Handlers/UserInfoHandler.cs new file mode 100644 index 00000000..672742e4 --- /dev/null +++ b/SecurityService/Oidc/Handlers/UserInfoHandler.cs @@ -0,0 +1,11 @@ +using MediatR; +using SecurityService.BusinessLogic.Oidc; +using SecurityService.Factories; + +namespace SecurityService.Oidc.Handlers; + +public static class UserInfoHandler +{ + public static async Task UserInfoAsync(HttpContext context, IMediator mediator, CancellationToken cancellationToken) + => OidcResponseFactory.FromResult(await mediator.Send(new OidcCommands.UserInfoCommand(context), cancellationToken)); +} diff --git a/SecurityService/Oidc/OidcEndpoints.cs b/SecurityService/Oidc/OidcEndpoints.cs index 818cf461..fdc89aba 100644 --- a/SecurityService/Oidc/OidcEndpoints.cs +++ b/SecurityService/Oidc/OidcEndpoints.cs @@ -1,323 +1,12 @@ -using System.Collections.Immutable; -using System.Security.Claims; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.Tokens; -using OpenIddict.Abstractions; -using OpenIddict.Server.AspNetCore; -using OpenIddict.Validation.AspNetCore; -using SecurityService.Database; -using SecurityService.Database.DbContexts; -using static OpenIddict.Abstractions.OpenIddictConstants; - namespace SecurityService.Oidc; public static class OidcEndpoints { public static void MapOidcEndpoints(this IEndpointRouteBuilder endpoints) { - endpoints.MapMethods("/connect/authorize", [HttpMethods.Get, HttpMethods.Post], (Delegate)AuthorizeAsync); - endpoints.MapPost("/connect/token", (Delegate)TokenAsync); - endpoints.MapMethods("/connect/logout", [HttpMethods.Get, HttpMethods.Post], (Delegate)LogoutAsync); - endpoints.MapMethods("/connect/userinfo", [HttpMethods.Get, HttpMethods.Post], (Delegate)UserInfoAsync); - } - - public static async Task AuthorizeAsync( - HttpContext context, - UserManager userManager, - IOpenIddictApplicationManager applicationManager, - IOpenIddictAuthorizationManager authorizationManager, - IOpenIddictScopeManager scopeManager, - CancellationToken cancellationToken) - { - var request = context.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); - var currentRequestUrl = OidcHelpers.BuildCurrentRequestUrl(context.Request); - - var authenticationResult = await context.AuthenticateAsync(IdentityConstants.ApplicationScheme); - if (authenticationResult.Succeeded == false) - { - if (request.HasPromptValue(PromptValues.None)) - { - return Results.Forbid(new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in." - }), [OpenIddictServerAspNetCoreDefaults.AuthenticationScheme]); - } - - return Results.Challenge(new AuthenticationProperties { RedirectUri = currentRequestUrl }, [IdentityConstants.ApplicationScheme]); - } - - var user = await userManager.GetUserAsync(authenticationResult.Principal); - if (user is null) - { - return Results.Forbid(new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user account could not be resolved." - }), [OpenIddictServerAspNetCoreDefaults.AuthenticationScheme]); - } - - var application = await applicationManager.FindByClientIdAsync(request.ClientId!, cancellationToken); - if (application is null) - { - return Results.BadRequest(new { error = "invalid_client", error_description = "The client application could not be found." }); - } - - if (context.Request.Query.TryGetValue("consent", out var consentDecision)) - { - if (string.Equals(consentDecision, "denied", StringComparison.OrdinalIgnoreCase)) - { - return Results.Forbid(new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.AccessDenied, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The authorization request was denied." - }), [OpenIddictServerAspNetCoreDefaults.AuthenticationScheme]); - } - - if (string.Equals(consentDecision, "accepted", StringComparison.OrdinalIgnoreCase)) - { - return await CompleteAuthorizationAsync(context, user, request, application, authorizationManager, userManager, scopeManager, cancellationToken, OidcHelpers.ReadMultiValue(context.Request.Query, "granted_scope")); - } - } - - var consentType = await applicationManager.GetConsentTypeAsync(application, cancellationToken) ?? ConsentTypes.Implicit; - var existingAuthorizations = await authorizationManager.FindAsync( - subject: user.Id, - client: await applicationManager.GetIdAsync(application, cancellationToken), - status: Statuses.Valid, - type: AuthorizationTypes.Permanent, - scopes: ImmutableArray.CreateRange(request.GetScopes()), - cancellationToken: cancellationToken).ToListAsync(cancellationToken); - - if (string.Equals(consentType, ConsentTypes.Explicit, StringComparison.OrdinalIgnoreCase) && existingAuthorizations.Count == 0) - { - if (request.HasPromptValue(PromptValues.None)) - { - return Results.Forbid(new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Interactive user consent is required." - }), [OpenIddictServerAspNetCoreDefaults.AuthenticationScheme]); - } - - return Results.Redirect($"/Consent?returnUrl={Uri.EscapeDataString(currentRequestUrl)}"); - } - - return await CompleteAuthorizationAsync(context, user, request, application, authorizationManager, userManager, scopeManager, cancellationToken, request.GetScopes()); - } - - public static async Task TokenAsync( - HttpContext context, - UserManager userManager, - SignInManager signInManager, - SecurityServiceDbContext dbContext, - IOpenIddictScopeManager scopeManager, - CancellationToken cancellationToken) - { - var request = context.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); - - if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType() || request.IsDeviceCodeGrantType()) - { - var authenticationResult = await context.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - if (authenticationResult.Succeeded == false || authenticationResult.Principal is null) - { - return InvalidGrant("The specified token is invalid."); - } - - var subject = authenticationResult.Principal.GetClaim(Claims.Subject); - if (string.IsNullOrWhiteSpace(subject)) - { - return Results.SignIn(authenticationResult.Principal, authenticationScheme: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } - - var user = await userManager.FindByIdAsync(subject); - if (user is null || await signInManager.CanSignInAsync(user) == false) - { - return InvalidGrant("The token is no longer valid."); - } - - var principal = await OidcHelpers.CreatePrincipalAsync( - user, - userManager, - authenticationResult.Principal.GetScopes(), - await scopeManager.ListResourcesAsync(ImmutableArray.CreateRange(authenticationResult.Principal.GetScopes()), cancellationToken).ToListAsync(cancellationToken), - authenticationResult.Principal.GetAuthorizationId()); - return Results.SignIn(principal, authenticationScheme: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } - - if (request.IsClientCredentialsGrantType()) - { - var grantedScopes = await ResolveClientCredentialsScopesAsync(request, dbContext, cancellationToken); - - var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType, Claims.Name, Claims.Role); - identity.SetClaim(Claims.Subject, request.ClientId!) - .SetClaim(Claims.Name, request.ClientId!); - identity.SetScopes(grantedScopes); - identity.SetResources(await scopeManager.ListResourcesAsync(ImmutableArray.CreateRange(grantedScopes), cancellationToken).ToListAsync(cancellationToken)); - identity.SetDestinations(OidcHelpers.GetDestinations); - return Results.SignIn(new ClaimsPrincipal(identity), authenticationScheme: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } - - if (request.IsPasswordGrantType()) - { - var user = await userManager.FindByNameAsync(request.Username!); - if (user is null) - { - return InvalidGrant(); - } - - var signInResult = await signInManager.CheckPasswordSignInAsync(user, request.Password!, lockoutOnFailure: true); - if (signInResult.Succeeded == false) - { - return InvalidGrant(); - } - - var grantedScopes = await ResolveClientCredentialsScopesAsync(request, dbContext, cancellationToken); - - var resources = await scopeManager.ListResourcesAsync(ImmutableArray.CreateRange(grantedScopes), cancellationToken).ToListAsync(cancellationToken); - var principal = await OidcHelpers.CreatePrincipalAsync(user, userManager, grantedScopes, resources, authorizationId: null); - return Results.SignIn(principal, authenticationScheme: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } - - return Results.BadRequest(new { error = "unsupported_grant_type", error_description = "The specified grant type is not supported by this service." }); - - static IResult InvalidGrant(string description = "The username/password couple is invalid.") => Results.Forbid(new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = description - }), [OpenIddictServerAspNetCoreDefaults.AuthenticationScheme]); - } - - public static async Task> ResolveClientCredentialsScopesAsync( - OpenIddictRequest request, - SecurityServiceDbContext dbContext, - CancellationToken cancellationToken) - { - IReadOnlyCollection grantedScopes = request.GetScopes().ToArray(); - if (grantedScopes.Count > 0) - { - return grantedScopes; - } - - var clientDefinition = await dbContext.ClientDefinitions - .SingleOrDefaultAsync(client => client.ClientId == request.ClientId, cancellationToken); - - return clientDefinition is null - ? Array.Empty() - : JsonListSerializer.Deserialize(clientDefinition.AllowedScopesJson); - } - - public static Task LogoutAsync(HttpContext context) - { - var currentRequestUrl = OidcHelpers.BuildCurrentRequestUrl(context.Request); - var confirmed = string.Equals(context.Request.Query["logout"], "confirmed", StringComparison.OrdinalIgnoreCase); - if (confirmed) - { - return Task.FromResult(Results.SignOut(new AuthenticationProperties { RedirectUri = "/Account/Logout/LoggedOut" }, [IdentityConstants.ApplicationScheme, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme])); - } - - return Task.FromResult(Results.Redirect($"/Account/Logout?returnUrl={Uri.EscapeDataString(currentRequestUrl)}")); - } - - public static async Task UserInfoAsync(HttpContext context, UserManager userManager) - { - var authenticationResult = await context.AuthenticateAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); - if (authenticationResult.Succeeded == false || authenticationResult.Principal is null) - { - return InvalidToken("The access token is missing or invalid."); - } - - var subject = authenticationResult.Principal.GetClaim(Claims.Subject); - if (string.IsNullOrWhiteSpace(subject)) - { - return InvalidToken("The access token does not contain a subject identifier."); - } - - var user = await userManager.FindByIdAsync(subject); - if (user is null) - { - return InvalidToken("The user associated with the token could not be found."); - } - - var scopes = authenticationResult.Principal.GetScopes().ToHashSet(StringComparer.OrdinalIgnoreCase); - var response = new Dictionary - { - [Claims.Subject] = user.Id - }; - - if (scopes.Contains(Scopes.Profile)) - { - response[Claims.Name] = user.UserName; - response[Claims.PreferredUsername] = user.UserName; - response[Claims.GivenName] = user.GivenName; - response[Claims.FamilyName] = user.FamilyName; - response["middle_name"] = user.MiddleName; - } - - if (scopes.Contains(Scopes.Email)) - { - response[Claims.Email] = user.Email; - response[Claims.EmailVerified] = user.EmailConfirmed; - } - - if (scopes.Contains(Scopes.Roles)) - { - response[Claims.Role] = (await userManager.GetRolesAsync(user)).ToArray(); - } - - foreach (var claim in await userManager.GetClaimsAsync(user)) - { - if (response.ContainsKey(claim.Type) == false) - { - response[claim.Type] = claim.Value; - } - } - - return Results.Json(response.Where(pair => pair.Value is not null).ToDictionary(pair => pair.Key, pair => pair.Value)); - - static IResult InvalidToken(string description) => Results.Challenge(new AuthenticationProperties(new Dictionary - { - [OpenIddictValidationAspNetCoreConstants.Properties.Error] = Errors.InvalidToken, - [OpenIddictValidationAspNetCoreConstants.Properties.ErrorDescription] = description - }), [OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme]); - } - - private static async Task CompleteAuthorizationAsync( - HttpContext context, - ApplicationUser user, - OpenIddictRequest request, - object application, - IOpenIddictAuthorizationManager authorizationManager, - UserManager userManager, - IOpenIddictScopeManager scopeManager, - CancellationToken cancellationToken, - IReadOnlyCollection grantedScopes) - { - var resources = await scopeManager.ListResourcesAsync(ImmutableArray.CreateRange(grantedScopes), cancellationToken).ToListAsync(cancellationToken); - var applicationManager = context.RequestServices.GetRequiredService(); - var authorizations = await authorizationManager.FindAsync( - subject: user.Id, - client: await applicationManager.GetIdAsync(application, cancellationToken), - status: Statuses.Valid, - type: AuthorizationTypes.Permanent, - scopes: ImmutableArray.CreateRange(grantedScopes), - cancellationToken: cancellationToken).ToListAsync(cancellationToken); - - var authorization = authorizations.LastOrDefault(); - var principal = await OidcHelpers.CreatePrincipalAsync(user, userManager, grantedScopes, resources, authorizationId: null); - - authorization ??= await authorizationManager.CreateAsync( - principal: principal, - subject: user.Id, - client: (await applicationManager.GetIdAsync(application, cancellationToken))!, - type: AuthorizationTypes.Permanent, - scopes: ImmutableArray.CreateRange(grantedScopes), - cancellationToken: cancellationToken); - - principal.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization, cancellationToken)); - return Results.SignIn(principal, authenticationScheme: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + endpoints.MapMethods("/connect/authorize", [HttpMethods.Get, HttpMethods.Post], Handlers.AuthorizeHandler.AuthorizeAsync); + endpoints.MapPost("/connect/token", Handlers.TokenHandler.TokenAsync); + endpoints.MapMethods("/connect/logout", [HttpMethods.Get, HttpMethods.Post], Handlers.LogoutHandler.LogoutAsync); + endpoints.MapMethods("/connect/userinfo", [HttpMethods.Get, HttpMethods.Post], Handlers.UserInfoHandler.UserInfoAsync); } } diff --git a/SecurityService/Pages/Account/Grants/Index.cshtml.cs b/SecurityService/Pages/Account/Grants/Index.cshtml.cs index 2c3619fd..132842b5 100644 --- a/SecurityService/Pages/Account/Grants/Index.cshtml.cs +++ b/SecurityService/Pages/Account/Grants/Index.cshtml.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using SecurityService.Database; using SecurityService.Models; -using SecurityService.Oidc; +using SecurityService.BusinessLogic.Oidc; using SecurityService.Services; namespace SecurityService.Pages.Account.Grants; diff --git a/SecurityService/Pages/Account/Logout/Index.cshtml.cs b/SecurityService/Pages/Account/Logout/Index.cshtml.cs index 5de8cc3a..ab09ba9e 100644 --- a/SecurityService/Pages/Account/Logout/Index.cshtml.cs +++ b/SecurityService/Pages/Account/Logout/Index.cshtml.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using SecurityService.Oidc; +using SecurityService.BusinessLogic.Oidc; namespace SecurityService.Pages.Account.Logout; diff --git a/SecurityService/Pages/Connect/Verify.cshtml.cs b/SecurityService/Pages/Connect/Verify.cshtml.cs index 0360b94e..0b4ccaa0 100644 --- a/SecurityService/Pages/Connect/Verify.cshtml.cs +++ b/SecurityService/Pages/Connect/Verify.cshtml.cs @@ -7,7 +7,7 @@ using OpenIddict.Server.AspNetCore; using SecurityService.Database; using SecurityService.Database.DbContexts; -using SecurityService.Oidc; +using SecurityService.BusinessLogic.Oidc; using static OpenIddict.Abstractions.OpenIddictConstants; namespace SecurityService.Pages.Connect; @@ -96,7 +96,7 @@ public async Task OnPostAsync(CancellationToken cancellationToken var scopes = result.AuthenticationResult.Principal!.GetScopes(); var resources = await this._scopeManager.ListResourcesAsync(ImmutableArray.CreateRange(scopes), cancellationToken).ToListAsync(cancellationToken); - var principal = await OidcHelpers.CreatePrincipalAsync(user, this._userManager, scopes, resources, authorizationId: null); + var principal = await OidcHelpers.CreatePrincipal(user, this._userManager, scopes, resources, authorizationId: null); return this.SignIn(principal, new AuthenticationProperties { RedirectUri = "/" }, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } @@ -104,7 +104,7 @@ public async Task OnPostAsync(CancellationToken cancellationToken private async Task PopulateAsync(AuthenticateResult authenticationResult, CancellationToken cancellationToken) { this.RequestedScopes = authenticationResult.Principal!.GetScopes().ToArray(); - var scopeDisplay = await OidcHelpers.BuildScopeDisplayAsync(this.RequestedScopes, this._dbContext, cancellationToken); + var scopeDisplay = await OidcHelpers.BuildScopeDisplay(this.RequestedScopes, this._dbContext, cancellationToken); this.IdentityScopes = scopeDisplay.IdentityScopes; this.ApiScopes = scopeDisplay.ApiScopes; this.Input.UserCode = authenticationResult.Properties?.GetTokenValue(OpenIddictServerAspNetCoreConstants.Tokens.UserCode) ?? this.Request.Query["user_code"].FirstOrDefault() ?? string.Empty; diff --git a/SecurityService/Pages/Consent/Index.cshtml.cs b/SecurityService/Pages/Consent/Index.cshtml.cs index 47ff8ed3..48bee498 100644 --- a/SecurityService/Pages/Consent/Index.cshtml.cs +++ b/SecurityService/Pages/Consent/Index.cshtml.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using OpenIddict.Abstractions; using SecurityService.Database.DbContexts; -using SecurityService.Oidc; +using SecurityService.BusinessLogic.Oidc; namespace SecurityService.Pages.Consent; @@ -36,7 +36,7 @@ public async Task OnGetAsync(string returnUrl, CancellationToken var application = await this._applicationManager.FindByClientIdAsync(request.ClientId!, cancellationToken); this.ClientName = application is null ? request.ClientId! : await this._applicationManager.GetDisplayNameAsync(application, cancellationToken) ?? request.ClientId!; - var scopes = await OidcHelpers.BuildScopeDisplayAsync(request, this._dbContext, cancellationToken); + var scopes = await OidcHelpers.BuildScopeDisplay(request, this._dbContext, cancellationToken); this.IdentityScopes = scopes.IdentityScopes; this.ApiScopes = scopes.ApiScopes; return this.Page(); diff --git a/SecurityService/Program.cs b/SecurityService/Program.cs index 72b305d1..dde2e84f 100644 --- a/SecurityService/Program.cs +++ b/SecurityService/Program.cs @@ -207,7 +207,10 @@ builder.Services.AddAuthorization(); -builder.Services.AddMediatR(configuration => configuration.RegisterServicesFromAssembly(typeof(SecurityServiceCommands).Assembly)); +builder.Services.AddMediatR(configuration => +{ + configuration.RegisterServicesFromAssembly(typeof(SecurityServiceCommands).Assembly); +}); builder.Services.ConfigureHttpJsonOptions(jsonOptions => JsonSerializerConfiguration.ConfigureMinimalApi(jsonOptions.SerializerOptions)); builder.Services.AddRazorPages(); builder.Services.AddEndpointsApiExplorer();