diff --git a/backend/LexBoxApi/Auth/AuthKernel.cs b/backend/LexBoxApi/Auth/AuthKernel.cs index 9fadbe6fd..38fae368b 100644 --- a/backend/LexBoxApi/Auth/AuthKernel.cs +++ b/backend/LexBoxApi/Auth/AuthKernel.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; using System.Text; @@ -204,7 +205,6 @@ public static void AddLexBoxAuth(IServiceCollection services, private static void AddOpenId(IServiceCollection services, IWebHostEnvironment environment) { services.Add(ScopeRequestFixer.Descriptor.ServiceDescriptor); - //openid server services.AddOpenIddict() .AddCore(options => @@ -215,11 +215,12 @@ private static void AddOpenId(IServiceCollection services, IWebHostEnvironment e .AddServer(options => { options.RegisterScopes("openid", "profile", "email"); + //todo add application claims options.RegisterClaims("aud", "email", "exp", "iss", "iat", "sub", "name"); - options.SetAuthorizationEndpointUris("api/login/open-id-auth"); - options.SetTokenEndpointUris("api/login/token"); - options.SetIntrospectionEndpointUris("api/connect/introspect"); - options.SetUserinfoEndpointUris("api/connect/userinfo"); + options.SetAuthorizationEndpointUris("api/oauth/open-id-auth"); + options.SetTokenEndpointUris("api/oauth/token"); + options.SetIntrospectionEndpointUris("api/oauth/introspect"); + options.SetUserinfoEndpointUris("api/oauth/userinfo"); options.Configure(serverOptions => serverOptions.Handlers.Add(ScopeRequestFixer.Descriptor)); options.AllowAuthorizationCodeFlow() diff --git a/backend/LexBoxApi/Controllers/LoginController.cs b/backend/LexBoxApi/Controllers/LoginController.cs index 06cbd85c8..34466ab10 100644 --- a/backend/LexBoxApi/Controllers/LoginController.cs +++ b/backend/LexBoxApi/Controllers/LoginController.cs @@ -1,4 +1,3 @@ -using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using LexBoxApi.Auth; using LexBoxApi.Auth.Attributes; @@ -9,24 +8,11 @@ using LexCore.Auth; using LexData; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Http; -using LexCore.Entities; using System.Security.Claims; -using System.Text.Json; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Google; -using Microsoft.Extensions.Primitives; -using Microsoft.IdentityModel.Tokens; -using OpenIddict.Abstractions; -using OpenIddict.EntityFrameworkCore.Models; -using OpenIddict.Server.AspNetCore; -using Org.BouncyCastle.Ocsp; namespace LexBoxApi.Controllers; @@ -38,10 +24,7 @@ public class LoginController( LoggedInContext loggedInContext, EmailService emailService, UserService userService, - TurnstileService turnstileService, - ProjectService projectService, - IOpenIddictApplicationManager applicationManager, - IOpenIddictAuthorizationManager authorizationManager) + TurnstileService turnstileService) : ControllerBase { /// @@ -172,290 +155,6 @@ public async Task> VerifyEmail( return Redirect(returnTo); } - [HttpGet("open-id-auth")] - [HttpPost("open-id-auth")] - [ProducesResponseType(400)] - [ProducesDefaultResponseType] - public async Task Authorize(string? returnUrl = null) - { - var request = HttpContext.GetOpenIddictServerRequest(); - if (request is null) - { - return BadRequest(); - } - - if (IsAcceptRequest()) - { - var lexAuthUser1 = loggedInContext.User; - var request1 = HttpContext.GetOpenIddictServerRequest() ?? - throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); - return await FinishSignIn(lexAuthUser1, request1); - } - - // Retrieve the user principal stored in the authentication cookie. - // If the user principal can't be extracted or the cookie is too old, redirect the user to the login page. - var result = await HttpContext.AuthenticateAsync(); - var lexAuthUser = result.Succeeded ? LexAuthUser.FromClaimsPrincipal(result.Principal) : null; - if (!result.Succeeded || - lexAuthUser is null || - request.HasPrompt(OpenIddictConstants.Prompts.Login) || - IsExpired(request, result)) - { - // If the client application requested promptless authentication, - // return an error indicating that the user is not logged in. - if (request.HasPrompt(OpenIddictConstants.Prompts.None)) - { - return Forbid( - authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - properties: new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = - OpenIddictConstants.Errors.LoginRequired, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = - "The user is not logged in." - })); - } - - // To avoid endless login -> authorization redirects, the prompt=login flag - // is removed from the authorization request payload before redirecting the user. - var prompt = string.Join(" ", request.GetPrompts().Remove(OpenIddictConstants.Prompts.Login)); - - var parameters = Request.HasFormContentType - ? Request.Form.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList() - : Request.Query.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList(); - - parameters.Add(KeyValuePair.Create(OpenIddictConstants.Parameters.Prompt, new StringValues(prompt))); - - return Challenge( - authenticationSchemes: CookieAuthenticationDefaults.AuthenticationScheme, - properties: new AuthenticationProperties - { - RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters) - }); - } - - var userId = lexAuthUser.Id.ToString(); - var requestClientId = request.ClientId; - ArgumentException.ThrowIfNullOrEmpty(requestClientId); - var application = await applicationManager.FindByClientIdAsync(requestClientId) ?? - throw new InvalidOperationException( - "Details concerning the calling client application cannot be found."); - var applicationId = await applicationManager.GetIdAsync(application) ?? - throw new InvalidOperationException("The calling client application could not be found."); - - // Retrieve the permanent authorizations associated with the user and the calling client application. - var authorizations = await authorizationManager.FindAsync( - subject: userId, - client: applicationId, - status: OpenIddictConstants.Statuses.Valid, - type: OpenIddictConstants.AuthorizationTypes.Permanent, - scopes: request.GetScopes()).ToListAsync(); - - switch (await applicationManager.GetConsentTypeAsync(application)) - { - // If the consent is implicit or if an authorization was found, - // return an authorization response without displaying the consent form. - case OpenIddictConstants.ConsentTypes.Implicit: - case OpenIddictConstants.ConsentTypes.External when authorizations.Count is not 0: - case OpenIddictConstants.ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPrompt(OpenIddictConstants.Prompts.Consent): - - return await FinishSignIn(lexAuthUser, request, applicationId, authorizations); - - // If the consent is external (e.g when authorizations are granted by a sysadmin), - // immediately return an error if no authorization can be found in the database. - case OpenIddictConstants.ConsentTypes.External when authorizations.Count is 0: - return Forbid( - authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - properties: new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = - "The logged in user is not allowed to access this client application." - })); - - // At this point, no authorization was found in the database and an error must be returned - // if the client application specified prompt=none in the authorization request. - case OpenIddictConstants.ConsentTypes.Explicit when request.HasPrompt(OpenIddictConstants.Prompts.None): - case OpenIddictConstants.ConsentTypes.Systematic when request.HasPrompt(OpenIddictConstants.Prompts.None): - return Forbid( - authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - properties: new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = - "Interactive user consent is required." - })); - - // In every other case, send user to consent page - default: - var parameters = Request.HasFormContentType - ? Request.Form.ToList() - : Request.Query.ToList(); - var data = JsonSerializer.Serialize(parameters.ToDictionary(pair => pair.Key, pair => pair.Value.ToString())); - var queryString = new QueryString() - .Add("appName", await applicationManager.GetDisplayNameAsync(application) ?? "Unknown app") - .Add("scope", request.Scope ?? "") - .Add("postback", data); - return Redirect($"/authorize{queryString.Value}"); - } - } - - private static bool IsExpired(OpenIddictRequest request, AuthenticateResult result) - { - // If a max_age parameter was provided, ensure that the cookie is not too old. - return (request.MaxAge != null && result.Properties?.IssuedUtc != null && - DateTimeOffset.UtcNow - result.Properties.IssuedUtc > - TimeSpan.FromSeconds(request.MaxAge.Value)); - } - - private bool IsAcceptRequest() - { - return Request.Method == "POST" && Request.Form.ContainsKey("submit.accept") && User.Identity?.IsAuthenticated == true; - } - - [HttpPost("token")] - [AllowAnonymous] - public async Task Exchange() - { - var request = HttpContext.GetOpenIddictServerRequest() ?? - throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); - // Retrieve the claims principal stored in the authorization code/refresh token. - var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - var lexAuthUser = result.Succeeded ? LexAuthUser.FromClaimsPrincipal(result.Principal) : null; - if (!result.Succeeded || lexAuthUser is null) - { - return Forbid( - authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - properties: new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = - "The token is no longer valid." - })); - } - - return await FinishSignIn(lexAuthUser, request); - } - - private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddictRequest request) - { - var requestClientId = request.ClientId; - ArgumentException.ThrowIfNullOrEmpty(requestClientId); - var application = await applicationManager.FindByClientIdAsync(requestClientId) ?? - throw new InvalidOperationException( - "Details concerning the calling client application cannot be found."); - // Retrieve the permanent authorizations associated with the user and the calling client application. - var applicationId = await applicationManager.GetIdAsync(application) ?? throw new InvalidOperationException("The calling client application could not be found."); - var authorizations = await authorizationManager.FindAsync( - subject: lexAuthUser.Id.ToString(), - client: applicationId, - status: OpenIddictConstants.Statuses.Valid, - type: OpenIddictConstants.AuthorizationTypes.Permanent, - scopes: request.GetScopes()).ToListAsync(); - - //allow cors response for redirect hosts - var redirectUrisAsync = await applicationManager.GetRedirectUrisAsync(application); - Response.Headers.AccessControlAllowOrigin = redirectUrisAsync - .Select(uri => new Uri(uri).GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)).ToArray(); - - // Note: this check is here to ensure a malicious user can't abuse this POST-only endpoint and - // force it to return a valid response without the external authorization. - if (authorizations.Count is 0 && - await applicationManager.HasConsentTypeAsync(application, OpenIddictConstants.ConsentTypes.External)) - { - return Forbid( - authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - properties: new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = - "The logged in user is not allowed to access this client application." - })); - } - - return await FinishSignIn(lexAuthUser, request, applicationId, authorizations); - } - private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddictRequest request, string applicationId, List authorizations) - { - var userId = lexAuthUser.Id.ToString(); - // Create the claims-based identity that will be used by OpenIddict to generate tokens. - var identity = new ClaimsIdentity( - authenticationType: TokenValidationParameters.DefaultAuthenticationType, - nameType: OpenIddictConstants.Claims.Name, - roleType: OpenIddictConstants.Claims.Role); - - // Add the claims that will be persisted in the tokens. - identity.SetClaim(OpenIddictConstants.Claims.Subject, userId) - .SetClaim(OpenIddictConstants.Claims.Email, lexAuthUser.Email) - .SetClaim(OpenIddictConstants.Claims.Name, lexAuthUser.Name) - .SetClaim(OpenIddictConstants.Claims.Role, lexAuthUser.Role.ToString()); - - // Note: in this sample, the granted scopes match the requested scope - // but you may want to allow the user to uncheck specific scopes. - // For that, simply restrict the list of scopes before calling SetScopes. - identity.SetScopes(request.GetScopes()); - identity.SetAudiences(LexboxAudience.LexboxApi.ToString()); - // identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync()); - - // Automatically create a permanent authorization to avoid requiring explicit consent - // for future authorization or token requests containing the same scopes. - var authorization = authorizations.LastOrDefault(); - authorization ??= await authorizationManager.CreateAsync( - identity: identity, - subject : userId, - client : applicationId, - type : OpenIddictConstants.AuthorizationTypes.Permanent, - scopes : identity.GetScopes()); - - identity.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization)); - identity.SetDestinations(GetDestinations); - - // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. - return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } - - private static IEnumerable GetDestinations(Claim claim) - { - // Note: by default, claims are NOT automatically included in the access and identity tokens. - // To allow OpenIddict to serialize them, you must attach them a destination, that specifies - // whether they should be included in access tokens, in identity tokens or in both. - - var claimsIdentity = claim.Subject; - ArgumentNullException.ThrowIfNull(claimsIdentity); - switch (claim.Type) - { - case OpenIddictConstants.Claims.Name: - yield return OpenIddictConstants.Destinations.AccessToken; - - if (claimsIdentity.HasScope(OpenIddictConstants.Scopes.Profile)) - yield return OpenIddictConstants.Destinations.IdentityToken; - - yield break; - - case OpenIddictConstants.Claims.Email: - yield return OpenIddictConstants.Destinations.AccessToken; - - if (claimsIdentity.HasScope(OpenIddictConstants.Scopes.Email)) - yield return OpenIddictConstants.Destinations.IdentityToken; - - yield break; - - case OpenIddictConstants.Claims.Role: - yield return OpenIddictConstants.Destinations.AccessToken; - - if (claimsIdentity.HasScope(OpenIddictConstants.Scopes.Roles)) - yield return OpenIddictConstants.Destinations.IdentityToken; - - yield break; - - // Never include the security stamp in the access and identity tokens, as it's a secret value. - case "AspNet.Identity.SecurityStamp": yield break; - - default: - yield return OpenIddictConstants.Destinations.AccessToken; - yield break; - } - } [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] diff --git a/backend/LexBoxApi/Controllers/OauthController.cs b/backend/LexBoxApi/Controllers/OauthController.cs new file mode 100644 index 000000000..b7024a295 --- /dev/null +++ b/backend/LexBoxApi/Controllers/OauthController.cs @@ -0,0 +1,307 @@ +using System.Security.Claims; +using System.Text.Json; +using LexBoxApi.Auth; +using LexCore.Auth; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; + +namespace LexBoxApi.Controllers; + +[ApiController] +[Route("/api/oauth")] +public class OauthController( + LoggedInContext loggedInContext, + IOpenIddictApplicationManager applicationManager, + IOpenIddictAuthorizationManager authorizationManager +) : ControllerBase +{ + + [HttpGet("open-id-auth")] + [HttpPost("open-id-auth")] + [ProducesResponseType(400)] + [ProducesDefaultResponseType] + public async Task Authorize() + { + var request = HttpContext.GetOpenIddictServerRequest(); + if (request is null) + { + return BadRequest(); + } + + if (IsAcceptRequest()) + { + return await FinishSignIn(loggedInContext.User, request); + } + + // Retrieve the user principal stored in the authentication cookie. + // If the user principal can't be extracted or the cookie is too old, redirect the user to the login page. + var result = await HttpContext.AuthenticateAsync(); + var lexAuthUser = result.Succeeded ? LexAuthUser.FromClaimsPrincipal(result.Principal) : null; + if (!result.Succeeded || + lexAuthUser is null || + request.HasPrompt(OpenIddictConstants.Prompts.Login) || + IsExpired(request, result)) + { + // If the client application requested promptless authentication, + // return an error indicating that the user is not logged in. + if (request.HasPrompt(OpenIddictConstants.Prompts.None)) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = + OpenIddictConstants.Errors.LoginRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The user is not logged in." + })); + } + + // To avoid endless login -> authorization redirects, the prompt=login flag + // is removed from the authorization request payload before redirecting the user. + var prompt = string.Join(" ", request.GetPrompts().Remove(OpenIddictConstants.Prompts.Login)); + + var parameters = Request.HasFormContentType + ? Request.Form.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList() + : Request.Query.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList(); + + parameters.Add(KeyValuePair.Create(OpenIddictConstants.Parameters.Prompt, new StringValues(prompt))); + + return Challenge( + authenticationSchemes: CookieAuthenticationDefaults.AuthenticationScheme, + properties: new AuthenticationProperties + { + RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters) + }); + } + + var userId = lexAuthUser.Id.ToString(); + var requestClientId = request.ClientId; + ArgumentException.ThrowIfNullOrEmpty(requestClientId); + var application = await applicationManager.FindByClientIdAsync(requestClientId) ?? + throw new InvalidOperationException( + "Details concerning the calling client application cannot be found."); + var applicationId = await applicationManager.GetIdAsync(application) ?? + throw new InvalidOperationException("The calling client application could not be found."); + + // Retrieve the permanent authorizations associated with the user and the calling client application. + var authorizations = await authorizationManager.FindAsync( + subject: userId, + client: applicationId, + status: OpenIddictConstants.Statuses.Valid, + type: OpenIddictConstants.AuthorizationTypes.Permanent, + scopes: request.GetScopes()).ToListAsync(); + + switch (await applicationManager.GetConsentTypeAsync(application)) + { + // If the consent is implicit or if an authorization was found, + // return an authorization response without displaying the consent form. + case OpenIddictConstants.ConsentTypes.Implicit: + case OpenIddictConstants.ConsentTypes.External when authorizations.Count is not 0: + case OpenIddictConstants.ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPrompt(OpenIddictConstants.Prompts.Consent): + + return await FinishSignIn(lexAuthUser, request, applicationId, authorizations); + + // If the consent is external (e.g when authorizations are granted by a sysadmin), + // immediately return an error if no authorization can be found in the database. + case OpenIddictConstants.ConsentTypes.External when authorizations.Count is 0: + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The logged in user is not allowed to access this client application." + })); + + // At this point, no authorization was found in the database and an error must be returned + // if the client application specified prompt=none in the authorization request. + case OpenIddictConstants.ConsentTypes.Explicit when request.HasPrompt(OpenIddictConstants.Prompts.None): + case OpenIddictConstants.ConsentTypes.Systematic when request.HasPrompt(OpenIddictConstants.Prompts.None): + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "Interactive user consent is required." + })); + + // In every other case, send user to consent page + default: + var parameters = Request.HasFormContentType + ? Request.Form.ToList() + : Request.Query.ToList(); + var data = JsonSerializer.Serialize(parameters.ToDictionary(pair => pair.Key, pair => pair.Value.ToString())); + var queryString = new QueryString() + .Add("appName", await applicationManager.GetDisplayNameAsync(application) ?? "Unknown app") + .Add("scope", request.Scope ?? "") + .Add("postback", data); + return Redirect($"/authorize{queryString.Value}"); + } + } + + private static bool IsExpired(OpenIddictRequest request, AuthenticateResult result) + { + // If a max_age parameter was provided, ensure that the cookie is not too old. + return (request.MaxAge != null && result.Properties?.IssuedUtc != null && + DateTimeOffset.UtcNow - result.Properties.IssuedUtc > + TimeSpan.FromSeconds(request.MaxAge.Value)); + } + + private bool IsAcceptRequest() + { + return Request.Method == "POST" && Request.Form.ContainsKey("submit.accept") && User.Identity?.IsAuthenticated == true; + } + + [HttpPost("token")] + [AllowAnonymous] + public async Task Exchange() + { + var request = HttpContext.GetOpenIddictServerRequest() ?? + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + // Retrieve the claims principal stored in the authorization code/refresh token. + var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + var lexAuthUser = result.Succeeded ? LexAuthUser.FromClaimsPrincipal(result.Principal) : null; + if (!result.Succeeded || lexAuthUser is null) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The token is no longer valid." + })); + } + + return await FinishSignIn(lexAuthUser, request); + } + + private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddictRequest request) + { + var requestClientId = request.ClientId; + ArgumentException.ThrowIfNullOrEmpty(requestClientId); + var application = await applicationManager.FindByClientIdAsync(requestClientId) ?? + throw new InvalidOperationException( + "Details concerning the calling client application cannot be found."); + // Retrieve the permanent authorizations associated with the user and the calling client application. + var applicationId = await applicationManager.GetIdAsync(application) ?? throw new InvalidOperationException("The calling client application could not be found."); + var authorizations = await authorizationManager.FindAsync( + subject: lexAuthUser.Id.ToString(), + client: applicationId, + status: OpenIddictConstants.Statuses.Valid, + type: OpenIddictConstants.AuthorizationTypes.Permanent, + scopes: request.GetScopes()).ToListAsync(); + + //allow cors response for redirect hosts + var redirectUrisAsync = await applicationManager.GetRedirectUrisAsync(application); + Response.Headers.AccessControlAllowOrigin = redirectUrisAsync + .Select(uri => new Uri(uri).GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)).ToArray(); + + // Note: this check is here to ensure a malicious user can't abuse this POST-only endpoint and + // force it to return a valid response without the external authorization. + if (authorizations.Count is 0 && + await applicationManager.HasConsentTypeAsync(application, OpenIddictConstants.ConsentTypes.External)) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The logged in user is not allowed to access this client application." + })); + } + + return await FinishSignIn(lexAuthUser, request, applicationId, authorizations); + } + private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddictRequest request, string applicationId, List authorizations) + { + var userId = lexAuthUser.Id.ToString(); + // Create the claims-based identity that will be used by OpenIddict to generate tokens. + var identity = new ClaimsIdentity( + authenticationType: TokenValidationParameters.DefaultAuthenticationType, + nameType: OpenIddictConstants.Claims.Name, + roleType: OpenIddictConstants.Claims.Role); + + // Add the claims that will be persisted in the tokens. + identity.SetClaim(OpenIddictConstants.Claims.Subject, userId) + .SetClaim(OpenIddictConstants.Claims.Email, lexAuthUser.Email) + .SetClaim(OpenIddictConstants.Claims.Name, lexAuthUser.Name) + .SetClaim(OpenIddictConstants.Claims.Role, lexAuthUser.Role.ToString()); + + // Note: in this sample, the granted scopes match the requested scope + // but you may want to allow the user to uncheck specific scopes. + // For that, simply restrict the list of scopes before calling SetScopes. + identity.SetScopes(request.GetScopes()); + identity.SetAudiences(LexboxAudience.LexboxApi.ToString()); + // identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync()); + + // Automatically create a permanent authorization to avoid requiring explicit consent + // for future authorization or token requests containing the same scopes. + var authorization = authorizations.LastOrDefault(); + authorization ??= await authorizationManager.CreateAsync( + identity: identity, + subject : userId, + client : applicationId, + type : OpenIddictConstants.AuthorizationTypes.Permanent, + scopes : identity.GetScopes()); + + identity.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization)); + identity.SetDestinations(GetDestinations); + + // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. + return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + private static IEnumerable GetDestinations(Claim claim) + { + // Note: by default, claims are NOT automatically included in the access and identity tokens. + // To allow OpenIddict to serialize them, you must attach them a destination, that specifies + // whether they should be included in access tokens, in identity tokens or in both. + + var claimsIdentity = claim.Subject; + ArgumentNullException.ThrowIfNull(claimsIdentity); + switch (claim.Type) + { + case OpenIddictConstants.Claims.Name: + yield return OpenIddictConstants.Destinations.AccessToken; + + if (claimsIdentity.HasScope(OpenIddictConstants.Scopes.Profile)) + yield return OpenIddictConstants.Destinations.IdentityToken; + + yield break; + + case OpenIddictConstants.Claims.Email: + yield return OpenIddictConstants.Destinations.AccessToken; + + if (claimsIdentity.HasScope(OpenIddictConstants.Scopes.Email)) + yield return OpenIddictConstants.Destinations.IdentityToken; + + yield break; + + case OpenIddictConstants.Claims.Role: + yield return OpenIddictConstants.Destinations.AccessToken; + + if (claimsIdentity.HasScope(OpenIddictConstants.Scopes.Roles)) + yield return OpenIddictConstants.Destinations.IdentityToken; + + yield break; + + // Never include the security stamp in the access and identity tokens, as it's a secret value. + case "AspNet.Identity.SecurityStamp": yield break; + + default: + yield return OpenIddictConstants.Destinations.AccessToken; + yield break; + } + } +} diff --git a/frontend/src/routes/(authenticated)/authorize/+page.svelte b/frontend/src/routes/(authenticated)/authorize/+page.svelte index 645e659de..0e09dcff2 100644 --- a/frontend/src/routes/(authenticated)/authorize/+page.svelte +++ b/frontend/src/routes/(authenticated)/authorize/+page.svelte @@ -12,7 +12,7 @@
-
+
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 051c174e1..3879e3cbf 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -34,7 +34,7 @@ export default defineConfig({ codegen(gqlOptions), precompileIntl('src/lib/i18n/locales'), sveltekit(), - // basicSsl() + exposeServer ? basicSsl() : null, // crypto.subtle is only available on secure connections ], optimizeDeps: { },