diff --git a/samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingDelegatingHandler.cs b/samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingDelegatingHandler.cs index 3bfc57cd..d70bdf68 100644 --- a/samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingDelegatingHandler.cs +++ b/samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingDelegatingHandler.cs @@ -6,76 +6,75 @@ using static OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants; using static OpenIddict.Client.OpenIddictClientModels; -namespace Dantooine.WebAssembly.Server.Helpers +namespace Dantooine.WebAssembly.Server.Helpers; + +internal sealed class TokenRefreshingDelegatingHandler( + OpenIddictClientService service, HttpMessageHandler innerHandler) : DelegatingHandler(innerHandler) { - internal sealed class TokenRefreshingDelegatingHandler( - OpenIddictClientService service, HttpMessageHandler innerHandler) : DelegatingHandler(innerHandler) - { - private readonly OpenIddictClientService _service = service ?? throw new ArgumentNullException(nameof(service)); + private readonly OpenIddictClientService _service = service ?? throw new ArgumentNullException(nameof(service)); - protected override async Task SendAsync( - HttpRequestMessage request, CancellationToken cancellationToken) + protected override async Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + // If an access token expiration date was returned by the authorization server and stored + // in the authentication cookie, use it to determine whether the token is about to expire. + // If it's not, try to use it: if the resource server returns a 401 error response, try + // to refresh the tokens before replaying the request with the new access token attached. + var date = GetBackchannelAccessTokenExpirationDate(request.Options); + if (date is null || TimeProvider.System.GetUtcNow() <= date?.AddMinutes(-5)) { - // If an access token expiration date was returned by the authorization server and stored - // in the authentication cookie, use it to determine whether the token is about to expire. - // If it's not, try to use it: if the resource server returns a 401 error response, try - // to refresh the tokens before replaying the request with the new access token attached. - var date = GetBackchannelAccessTokenExpirationDate(request.Options); - if (date is null || TimeProvider.System.GetUtcNow() <= date?.AddMinutes(-5)) - { - request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Bearer, GetBackchannelAccessToken(request.Options)); + request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Bearer, GetBackchannelAccessToken(request.Options)); - var response = await base.SendAsync(request, cancellationToken); - if (response.StatusCode is not HttpStatusCode.Unauthorized) - { - return response; - } + var response = await base.SendAsync(request, cancellationToken); + if (response.StatusCode is not HttpStatusCode.Unauthorized) + { + return response; + } - // Note: this handler can be called concurrently for the same user if multiple HTTP - // requests are processed in parallel: while this results in multiple refresh token - // requests being sent concurrently, this is something OpenIddict allows during a short - // period of time (called refresh token reuse leeway and set to 30 seconds by default). - var result = await _service.AuthenticateWithRefreshTokenAsync(new RefreshTokenAuthenticationRequest - { - CancellationToken = cancellationToken, - DisableUserInfo = true, - RefreshToken = GetRefreshToken(request.Options) - }); + // Note: this handler can be called concurrently for the same user if multiple HTTP + // requests are processed in parallel: while this results in multiple refresh token + // requests being sent concurrently, this is something OpenIddict allows during a short + // period of time (called refresh token reuse leeway and set to 30 seconds by default). + var result = await _service.AuthenticateWithRefreshTokenAsync(new RefreshTokenAuthenticationRequest + { + CancellationToken = cancellationToken, + DisableUserInfo = true, + RefreshToken = GetRefreshToken(request.Options) + }); - request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Bearer, result.AccessToken); + request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Bearer, result.AccessToken); - return new TokenRefreshingHttpResponseMessage(result, await base.SendAsync(request, cancellationToken)); - } + return new TokenRefreshingHttpResponseMessage(result, await base.SendAsync(request, cancellationToken)); + } - // Otherwise, don't bother using the existing access token and refresh tokens immediately. - else + // Otherwise, don't bother using the existing access token and refresh tokens immediately. + else + { + var result = await _service.AuthenticateWithRefreshTokenAsync(new RefreshTokenAuthenticationRequest { - var result = await _service.AuthenticateWithRefreshTokenAsync(new RefreshTokenAuthenticationRequest - { - CancellationToken = cancellationToken, - DisableUserInfo = true, - RefreshToken = GetRefreshToken(request.Options) - }); + CancellationToken = cancellationToken, + DisableUserInfo = true, + RefreshToken = GetRefreshToken(request.Options) + }); - request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Bearer, result.AccessToken); + request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Bearer, result.AccessToken); - return new TokenRefreshingHttpResponseMessage(result, await base.SendAsync(request, cancellationToken)); - } + return new TokenRefreshingHttpResponseMessage(result, await base.SendAsync(request, cancellationToken)); + } - static string GetBackchannelAccessToken(HttpRequestOptions options) => - options.TryGetValue(new(Tokens.BackchannelAccessToken), out string? token) && - !string.IsNullOrEmpty(token) ? token : - throw new InvalidOperationException("The access token couldn't be found in the request options."); + static string GetBackchannelAccessToken(HttpRequestOptions options) => + options.TryGetValue(new(Tokens.BackchannelAccessToken), out string? token) && + !string.IsNullOrEmpty(token) ? token : + throw new InvalidOperationException("The access token couldn't be found in the request options."); - static DateTimeOffset? GetBackchannelAccessTokenExpirationDate(HttpRequestOptions options) => - options.TryGetValue(new(Tokens.BackchannelAccessTokenExpirationDate), out string? token) && - !string.IsNullOrEmpty(token) && - DateTimeOffset.TryParse(token, CultureInfo.InvariantCulture, out DateTimeOffset date) ? date : null; + static DateTimeOffset? GetBackchannelAccessTokenExpirationDate(HttpRequestOptions options) => + options.TryGetValue(new(Tokens.BackchannelAccessTokenExpirationDate), out string? token) && + !string.IsNullOrEmpty(token) && + DateTimeOffset.TryParse(token, CultureInfo.InvariantCulture, out DateTimeOffset date) ? date : null; - static string GetRefreshToken(HttpRequestOptions options) => - options.TryGetValue(new(Tokens.RefreshToken), out string? token) && - !string.IsNullOrEmpty(token) ? token : - throw new InvalidOperationException("The refresh token couldn't be found in the request options."); - } + static string GetRefreshToken(HttpRequestOptions options) => + options.TryGetValue(new(Tokens.RefreshToken), out string? token) && + !string.IsNullOrEmpty(token) ? token : + throw new InvalidOperationException("The refresh token couldn't be found in the request options."); } } diff --git a/samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingForwarderHttpClientFactory.cs b/samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingForwarderHttpClientFactory.cs index 4fdbe52d..fc10aa5d 100644 --- a/samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingForwarderHttpClientFactory.cs +++ b/samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingForwarderHttpClientFactory.cs @@ -1,18 +1,17 @@ using OpenIddict.Client; using Yarp.ReverseProxy.Forwarder; -namespace Dantooine.WebAssembly.Server.Helpers +namespace Dantooine.WebAssembly.Server.Helpers; + +internal sealed class TokenRefreshingForwarderHttpClientFactory(OpenIddictClientService service) : ForwarderHttpClientFactory { - internal sealed class TokenRefreshingForwarderHttpClientFactory(OpenIddictClientService service) : ForwarderHttpClientFactory - { - private readonly OpenIddictClientService _service = service ?? throw new ArgumentNullException(nameof(service)); + private readonly OpenIddictClientService _service = service ?? throw new ArgumentNullException(nameof(service)); - protected override HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler) - { - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(handler); + protected override HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(handler); - return new TokenRefreshingDelegatingHandler(_service, handler); - } + return new TokenRefreshingDelegatingHandler(_service, handler); } } diff --git a/samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingHttpResponseMessage.cs b/samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingHttpResponseMessage.cs index 99b8ecb6..6b4c05a1 100644 --- a/samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingHttpResponseMessage.cs +++ b/samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingHttpResponseMessage.cs @@ -1,26 +1,25 @@ using static OpenIddict.Client.OpenIddictClientModels; -namespace Dantooine.WebAssembly.Server.Helpers +namespace Dantooine.WebAssembly.Server.Helpers; + +internal sealed class TokenRefreshingHttpResponseMessage : HttpResponseMessage { - internal sealed class TokenRefreshingHttpResponseMessage : HttpResponseMessage + public TokenRefreshingHttpResponseMessage(RefreshTokenAuthenticationResult result, HttpResponseMessage response) { - public TokenRefreshingHttpResponseMessage(RefreshTokenAuthenticationResult result, HttpResponseMessage response) - { - ArgumentNullException.ThrowIfNull(response); - ArgumentNullException.ThrowIfNull(result); + ArgumentNullException.ThrowIfNull(response); + ArgumentNullException.ThrowIfNull(result); - RefreshTokenAuthenticationResult = result; + RefreshTokenAuthenticationResult = result; - Content = response.Content; - StatusCode = response.StatusCode; - Version = response.Version; + Content = response.Content; + StatusCode = response.StatusCode; + Version = response.Version; - foreach (var header in response.Headers) - { - Headers.Add(header.Key, header.Value); - } + foreach (var header in response.Headers) + { + Headers.Add(header.Key, header.Value); } - - public RefreshTokenAuthenticationResult RefreshTokenAuthenticationResult { get; } } + + public RefreshTokenAuthenticationResult RefreshTokenAuthenticationResult { get; } } diff --git a/samples/Geonosis/Geonosis.Auth/Controllers/AuthorizationController.cs b/samples/Geonosis/Geonosis.Auth/Controllers/AuthorizationController.cs index 45d1f375..c70bffcb 100644 --- a/samples/Geonosis/Geonosis.Auth/Controllers/AuthorizationController.cs +++ b/samples/Geonosis/Geonosis.Auth/Controllers/AuthorizationController.cs @@ -13,459 +13,458 @@ using OpenIddict.Server.AspNetCore; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Geonosis.Auth.Controllers +namespace Geonosis.Auth.Controllers; + +public class AuthorizationController : Controller { - public class AuthorizationController : Controller + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictAuthorizationManager _authorizationManager; + private readonly IOpenIddictScopeManager _scopeManager; + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + + public AuthorizationController( + IOpenIddictApplicationManager applicationManager, + IOpenIddictAuthorizationManager authorizationManager, + IOpenIddictScopeManager scopeManager, + SignInManager signInManager, + UserManager userManager) { - private readonly IOpenIddictApplicationManager _applicationManager; - private readonly IOpenIddictAuthorizationManager _authorizationManager; - private readonly IOpenIddictScopeManager _scopeManager; - private readonly SignInManager _signInManager; - private readonly UserManager _userManager; - - public AuthorizationController( - IOpenIddictApplicationManager applicationManager, - IOpenIddictAuthorizationManager authorizationManager, - IOpenIddictScopeManager scopeManager, - SignInManager signInManager, - UserManager userManager) - { - _applicationManager = applicationManager; - _authorizationManager = authorizationManager; - _scopeManager = scopeManager; - _signInManager = signInManager; - _userManager = userManager; - } + _applicationManager = applicationManager; + _authorizationManager = authorizationManager; + _scopeManager = scopeManager; + _signInManager = signInManager; + _userManager = userManager; + } - [HttpGet("~/connect/authorize")] - [HttpPost("~/connect/authorize")] - [IgnoreAntiforgeryToken] - public async Task Authorize() + [HttpGet("~/connect/authorize")] + [HttpPost("~/connect/authorize")] + [IgnoreAntiforgeryToken] + public async Task Authorize() + { + var request = HttpContext.GetOpenIddictServerRequest() ?? + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + + // Try to retrieve the user principal stored in the authentication cookie and redirect + // the user agent to the login page (or to an external provider) in the following cases: + // + // - If the user principal can't be extracted or the cookie is too old. + // - If prompt=login was specified by the client application. + // - If max_age=0 was specified by the client application (max_age=0 is equivalent to prompt=login). + // - If a max_age parameter was provided and the authentication cookie is not considered "fresh" enough. + // + // For scenarios where the default authentication handler configured in the ASP.NET Core + // authentication options shouldn't be used, a specific scheme can be specified here. + var result = await HttpContext.AuthenticateAsync(); + if (result is not { Succeeded: true } || + ((request.HasPromptValue(PromptValues.Login) || request.MaxAge is 0 || + (request.MaxAge is not null && result.Properties?.IssuedUtc is not null && + TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) && + TempData["IgnoreAuthenticationChallenge"] is null or false)) { - var request = HttpContext.GetOpenIddictServerRequest() ?? - throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + // If the client application requested promptless authentication, + // return an error indicating that the user is not logged in. + if (request.HasPromptValue(PromptValues.None)) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in." + })); + } - // Try to retrieve the user principal stored in the authentication cookie and redirect - // the user agent to the login page (or to an external provider) in the following cases: + // To avoid endless login endpoint -> authorization endpoint redirects, a special temp data entry is + // used to skip the challenge if the user agent has already been redirected to the login endpoint. // - // - If the user principal can't be extracted or the cookie is too old. - // - If prompt=login was specified by the client application. - // - If max_age=0 was specified by the client application (max_age=0 is equivalent to prompt=login). - // - If a max_age parameter was provided and the authentication cookie is not considered "fresh" enough. - // - // For scenarios where the default authentication handler configured in the ASP.NET Core + // Note: this flag doesn't guarantee that the user has accepted to re-authenticate. If such a guarantee + // is needed, the existing authentication cookie MUST be deleted AND revoked (e.g using ASP.NET Core + // Identity's security stamp feature with an extremely short revalidation time span) before triggering + // a challenge to redirect the user agent to the login endpoint. + TempData["IgnoreAuthenticationChallenge"] = true; + + // For scenarios where the default challenge handler configured in the ASP.NET Core // authentication options shouldn't be used, a specific scheme can be specified here. - var result = await HttpContext.AuthenticateAsync(); - if (result is not { Succeeded: true } || - ((request.HasPromptValue(PromptValues.Login) || request.MaxAge is 0 || - (request.MaxAge is not null && result.Properties?.IssuedUtc is not null && - TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) && - TempData["IgnoreAuthenticationChallenge"] is null or false)) + return Challenge(new AuthenticationProperties { - // If the client application requested promptless authentication, - // return an error indicating that the user is not logged in. - if (request.HasPromptValue(PromptValues.None)) - { - return Forbid( - authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - properties: new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in." - })); - } - - // To avoid endless login endpoint -> authorization endpoint redirects, a special temp data entry is - // used to skip the challenge if the user agent has already been redirected to the login endpoint. - // - // Note: this flag doesn't guarantee that the user has accepted to re-authenticate. If such a guarantee - // is needed, the existing authentication cookie MUST be deleted AND revoked (e.g using ASP.NET Core - // Identity's security stamp feature with an extremely short revalidation time span) before triggering - // a challenge to redirect the user agent to the login endpoint. - TempData["IgnoreAuthenticationChallenge"] = true; - - // For scenarios where the default challenge handler configured in the ASP.NET Core - // authentication options shouldn't be used, a specific scheme can be specified here. - return Challenge(new AuthenticationProperties + RedirectUri = Request.PathBase + Request.Path + QueryString.Create( + Request.HasFormContentType ? Request.Form : Request.Query) + }); + } + + // Retrieve the profile of the logged in user. + var user = await _userManager.GetUserAsync(result.Principal) ?? + throw new InvalidOperationException("The user details cannot be retrieved."); + + // Retrieve the application details from the database. + var application = await _applicationManager.FindByClientIdAsync(request.ClientId!) ?? + 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 authorizations = await _authorizationManager.FindAsync( + subject: await _userManager.GetUserIdAsync(user), + client : await _applicationManager.GetIdAsync(application), + status : Statuses.Valid, + type : AuthorizationTypes.Permanent, + scopes : request.GetScopes()).ToListAsync(); + + switch (await _applicationManager.GetConsentTypeAsync(application)) + { + // 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 ConsentTypes.External when authorizations.Count is 0: + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The logged in user is not allowed to access this client application." + })); + + // If the consent is implicit or if an authorization was found, + // return an authorization response without displaying the consent form. + case ConsentTypes.Implicit: + case ConsentTypes.External when authorizations.Count is not 0: + case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPromptValue(PromptValues.Consent): + // Create the claims-based identity that will be used by OpenIddict to generate tokens. + var identity = new ClaimsIdentity( + authenticationType: TokenValidationParameters.DefaultAuthenticationType, + nameType: Claims.Name, + roleType: Claims.Role); + + // Add the claims that will be persisted in the tokens. + identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user)) + .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) + .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) + .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); + + // 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.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 : await _userManager.GetUserIdAsync(user), + client : (await _applicationManager.GetIdAsync(application))!, + type : AuthorizationTypes.Permanent, + scopes : identity.GetScopes()); + + identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); + identity.SetDestinations(GetDestinations); + + return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + + // 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 ConsentTypes.Explicit when request.HasPromptValue(PromptValues.None): + case ConsentTypes.Systematic when request.HasPromptValue(PromptValues.None): + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "Interactive user consent is required." + })); + + // In every other case, render the consent form. + default: + return View(new AuthorizeViewModel { - RedirectUri = Request.PathBase + Request.Path + QueryString.Create( - Request.HasFormContentType ? Request.Form : Request.Query) + ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application), + Scope = request.Scope }); - } + } + } + + [Authorize, FormValueRequired("submit.Accept")] + [HttpPost("~/connect/authorize"), ValidateAntiForgeryToken] + public async Task Accept() + { + var request = HttpContext.GetOpenIddictServerRequest() ?? + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + + // Retrieve the profile of the logged in user. + var user = await _userManager.GetUserAsync(User) ?? + throw new InvalidOperationException("The user details cannot be retrieved."); + + // Retrieve the application details from the database. + var application = await _applicationManager.FindByClientIdAsync(request.ClientId!) ?? + 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 authorizations = await _authorizationManager.FindAsync( + subject: await _userManager.GetUserIdAsync(user), + client : await _applicationManager.GetIdAsync(application), + status : Statuses.Valid, + type : AuthorizationTypes.Permanent, + scopes : request.GetScopes()).ToListAsync(); + + // Note: the same check is already made in the other action but is repeated + // 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, ConsentTypes.External)) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The logged in user is not allowed to access this client application." + })); + } - // Retrieve the profile of the logged in user. - var user = await _userManager.GetUserAsync(result.Principal) ?? - throw new InvalidOperationException("The user details cannot be retrieved."); + // Create the claims-based identity that will be used by OpenIddict to generate tokens. + var identity = new ClaimsIdentity( + authenticationType: TokenValidationParameters.DefaultAuthenticationType, + nameType: Claims.Name, + roleType: Claims.Role); + + // Add the claims that will be persisted in the tokens. + identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user)) + .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) + .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) + .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); + + // 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.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 : await _userManager.GetUserIdAsync(user), + client : (await _applicationManager.GetIdAsync(application))!, + type : 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); + } - // Retrieve the application details from the database. - var application = await _applicationManager.FindByClientIdAsync(request.ClientId!) ?? - throw new InvalidOperationException("Details concerning the calling client application cannot be found."); + [Authorize, FormValueRequired("submit.Deny")] + [HttpPost("~/connect/authorize"), ValidateAntiForgeryToken] + // Notify OpenIddict that the authorization grant has been denied by the resource owner + // to redirect the user agent to the client application using the appropriate response_mode. + public IActionResult Deny() => Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - // Retrieve the permanent authorizations associated with the user and the calling client application. - var authorizations = await _authorizationManager.FindAsync( - subject: await _userManager.GetUserIdAsync(user), - client: await _applicationManager.GetIdAsync(application), - status: Statuses.Valid, - type: AuthorizationTypes.Permanent, - scopes: request.GetScopes()).ToListAsync(); + [HttpGet("~/connect/logout")] + //public IActionResult Logout() => View(); + public Task Logout() => LogoutPost(); - switch (await _applicationManager.GetConsentTypeAsync(application)) + [ActionName(nameof(Logout)), HttpPost("~/connect/logout"), ValidateAntiForgeryToken] + public async Task LogoutPost() + { + // Ask ASP.NET Core Identity to delete the local and external cookies created + // when the user agent is redirected from the external identity provider + // after a successful authentication flow (e.g Google or Facebook). + await _signInManager.SignOutAsync(); + + // Returning a SignOutResult will ask OpenIddict to redirect the user agent + // to the post_logout_redirect_uri specified by the client application or to + // the RedirectUri specified in the authentication properties if none was set. + return SignOut( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties { - // 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 ConsentTypes.External when authorizations.Count is 0: - return Forbid( - authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - properties: new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = - "The logged in user is not allowed to access this client application." - })); - - // If the consent is implicit or if an authorization was found, - // return an authorization response without displaying the consent form. - case ConsentTypes.Implicit: - case ConsentTypes.External when authorizations.Count is not 0: - case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPromptValue(PromptValues.Consent): - // Create the claims-based identity that will be used by OpenIddict to generate tokens. - var identity = new ClaimsIdentity( - authenticationType: TokenValidationParameters.DefaultAuthenticationType, - nameType: Claims.Name, - roleType: Claims.Role); - - // Add the claims that will be persisted in the tokens. - identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user)) - .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) - .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) - .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); - - // 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.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: await _userManager.GetUserIdAsync(user), - client: (await _applicationManager.GetIdAsync(application))!, - type: AuthorizationTypes.Permanent, - scopes: identity.GetScopes()); - - identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); - identity.SetDestinations(GetDestinations); - - return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - - // 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 ConsentTypes.Explicit when request.HasPromptValue(PromptValues.None): - case ConsentTypes.Systematic when request.HasPromptValue(PromptValues.None): - return Forbid( - authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - properties: new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = - "Interactive user consent is required." - })); - - // In every other case, render the consent form. - default: - return View(new AuthorizeViewModel + RedirectUri = "/" + }); + } + + [HttpPost("~/connect/token"), IgnoreAntiforgeryToken, Produces("application/json")] + public async Task Exchange() + { + var request = HttpContext.GetOpenIddictServerRequest() ?? + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + + if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType()) + { + // Retrieve the claims principal stored in the authorization code/refresh token. + var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + + // Retrieve the user profile corresponding to the authorization code/refresh token. + var user = await _userManager.FindByIdAsync(result.Principal!.GetClaim(Claims.Subject)!); + if (user is null) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary { - ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application), - Scope = request.Scope - }); + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." + })); } - } - [Authorize, FormValueRequired("submit.Accept")] - [HttpPost("~/connect/authorize"), ValidateAntiForgeryToken] - public async Task Accept() - { - var request = HttpContext.GetOpenIddictServerRequest() ?? - throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); - - // Retrieve the profile of the logged in user. - var user = await _userManager.GetUserAsync(User) ?? - throw new InvalidOperationException("The user details cannot be retrieved."); - - // Retrieve the application details from the database. - var application = await _applicationManager.FindByClientIdAsync(request.ClientId!) ?? - 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 authorizations = await _authorizationManager.FindAsync( - subject: await _userManager.GetUserIdAsync(user), - client: await _applicationManager.GetIdAsync(application), - status: Statuses.Valid, - type: AuthorizationTypes.Permanent, - scopes: request.GetScopes()).ToListAsync(); - - // Note: the same check is already made in the other action but is repeated - // 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, ConsentTypes.External)) + // Ensure the user is still allowed to sign in. + if (!await _signInManager.CanSignInAsync(user)) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = - "The logged in user is not allowed to access this client application." + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." })); } - // Create the claims-based identity that will be used by OpenIddict to generate tokens. - var identity = new ClaimsIdentity( + var identity = new ClaimsIdentity(result.Principal!.Claims, authenticationType: TokenValidationParameters.DefaultAuthenticationType, nameType: Claims.Name, roleType: Claims.Role); - // Add the claims that will be persisted in the tokens. + // Override the user claims present in the principal in case they + // changed since the authorization code/refresh token was issued. identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user)) .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); - // 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.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: await _userManager.GetUserIdAsync(user), - client: (await _applicationManager.GetIdAsync(application))!, - type: 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); } - [Authorize, FormValueRequired("submit.Deny")] - [HttpPost("~/connect/authorize"), ValidateAntiForgeryToken] - // Notify OpenIddict that the authorization grant has been denied by the resource owner - // to redirect the user agent to the client application using the appropriate response_mode. - public IActionResult Deny() => Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - - [HttpGet("~/connect/logout")] - //public IActionResult Logout() => View(); - public Task Logout() => LogoutPost(); - - [ActionName(nameof(Logout)), HttpPost("~/connect/logout"), ValidateAntiForgeryToken] - public async Task LogoutPost() + else if (request.IsTokenExchangeGrantType()) { - // Ask ASP.NET Core Identity to delete the local and external cookies created - // when the user agent is redirected from the external identity provider - // after a successful authentication flow (e.g Google or Facebook). - await _signInManager.SignOutAsync(); - - // Returning a SignOutResult will ask OpenIddict to redirect the user agent - // to the post_logout_redirect_uri specified by the client application or to - // the RedirectUri specified in the authentication properties if none was set. - return SignOut( - authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - properties: new AuthenticationProperties - { - RedirectUri = "/" - }); - } + // Retrieve the claims principal stored in the subject token. + // + // Note: the principal may not represent a user (e.g if the token was issued during a client credentials token + // request and represents a client application): developers are strongly encouraged to ensure that the user + // and client identifiers are randomly generated so that a malicious client cannot impersonate a legit user. + // + // See https://datatracker.ietf.org/doc/html/rfc9068#SecurityConsiderations for more information. + var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - [HttpPost("~/connect/token"), IgnoreAntiforgeryToken, Produces("application/json")] - public async Task Exchange() - { - var request = HttpContext.GetOpenIddictServerRequest() ?? - throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + // If available, retrieve the claims principal stored in the actor token. + var actor = result.Properties?.GetParameter(OpenIddictServerAspNetCoreConstants.Properties.ActorTokenPrincipal); - if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType()) + // Retrieve the user profile corresponding to the subject token. + var user = await _userManager.FindByIdAsync(result.Principal!.GetClaim(Claims.Subject)!); + if (user is null) { - // Retrieve the claims principal stored in the authorization code/refresh token. - var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." + })); + } - // Retrieve the user profile corresponding to the authorization code/refresh token. - var user = await _userManager.FindByIdAsync(result.Principal!.GetClaim(Claims.Subject)!); - if (user is null) - { - return Forbid( - authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - properties: new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." - })); - } - - // Ensure the user is still allowed to sign in. - if (!await _signInManager.CanSignInAsync(user)) - { - return Forbid( - authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - properties: new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." - })); - } - - var identity = new ClaimsIdentity(result.Principal!.Claims, - authenticationType: TokenValidationParameters.DefaultAuthenticationType, - nameType: Claims.Name, - roleType: Claims.Role); + // Ensure the user is still allowed to sign in. + if (!await _signInManager.CanSignInAsync(user)) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." + })); + } - // Override the user claims present in the principal in case they - // changed since the authorization code/refresh token was issued. - identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user)) - .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) - .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) - .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); + // Note: whether the identity represents a delegated or impersonated access (or any other + // model) is entirely up to the implementer: to support all scenarios, OpenIddict doesn't + // enforce any specific constraint on the identity used for the sign-in operation and only + // requires that the standard "act" and "may_act" claims be valid JSON objects if present. - identity.SetDestinations(GetDestinations); + var identity = new ClaimsIdentity( + authenticationType: TokenValidationParameters.DefaultAuthenticationType, + nameType: Claims.Name, + roleType: Claims.Role); - // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. - return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } + // Add the claims that will be persisted in the issued token. + identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user)) + .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) + .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) + .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) + .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); - else if (request.IsTokenExchangeGrantType()) + // Note: IdentityModel doesn't support serializing ClaimsIdentity.Actor to the + // standard "act" claim yet, which requires adding the "act" claim manually. + // + // For more information, see + // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3219. + if (!string.IsNullOrEmpty(actor?.GetClaim(Claims.Subject)) && + !string.Equals(identity.GetClaim(Claims.Subject), actor.GetClaim(Claims.Subject), StringComparison.Ordinal)) { - // Retrieve the claims principal stored in the subject token. - // - // Note: the principal may not represent a user (e.g if the token was issued during a client credentials token - // request and represents a client application): developers are strongly encouraged to ensure that the user - // and client identifiers are randomly generated so that a malicious client cannot impersonate a legit user. - // - // See https://datatracker.ietf.org/doc/html/rfc9068#SecurityConsiderations for more information. - var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - - // If available, retrieve the claims principal stored in the actor token. - var actor = result.Properties?.GetParameter(OpenIddictServerAspNetCoreConstants.Properties.ActorTokenPrincipal); - - // Retrieve the user profile corresponding to the subject token. - var user = await _userManager.FindByIdAsync(result.Principal!.GetClaim(Claims.Subject)!); - if (user is null) - { - return Forbid( - authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - properties: new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." - })); - } - - // Ensure the user is still allowed to sign in. - if (!await _signInManager.CanSignInAsync(user)) + identity.SetClaim(Claims.Actor, new JsonObject { - return Forbid( - authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - properties: new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." - })); - } - - // Note: whether the identity represents a delegated or impersonated access (or any other - // model) is entirely up to the implementer: to support all scenarios, OpenIddict doesn't - // enforce any specific constraint on the identity used for the sign-in operation and only - // requires that the standard "act" and "may_act" claims be valid JSON objects if present. - - var identity = new ClaimsIdentity( - authenticationType: TokenValidationParameters.DefaultAuthenticationType, - nameType: Claims.Name, - roleType: Claims.Role); - - // Add the claims that will be persisted in the issued token. - identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user)) - .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user)) - .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user)) - .SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user)) - .SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]); + [Claims.Subject] = actor.GetClaim(Claims.Subject) + }); + } - // Note: IdentityModel doesn't support serializing ClaimsIdentity.Actor to the - // standard "act" claim yet, which requires adding the "act" claim manually. - // - // For more information, see - // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3219. - if (!string.IsNullOrEmpty(actor?.GetClaim(Claims.Subject)) && - !string.Equals(identity.GetClaim(Claims.Subject), actor.GetClaim(Claims.Subject), StringComparison.Ordinal)) - { - identity.SetClaim(Claims.Actor, new JsonObject - { - [Claims.Subject] = actor.GetClaim(Claims.Subject) - }); - } + // 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.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync()); + identity.SetDestinations(GetDestinations); - // 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.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync()); - identity.SetDestinations(GetDestinations); + // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. + return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } - // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. - return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } + throw new InvalidOperationException("The specified grant type is not supported."); + } - throw new InvalidOperationException("The specified grant type is not supported."); - } + 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. - private static IEnumerable GetDestinations(Claim claim) + switch (claim.Type) { - // 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. + case Claims.Name or Claims.PreferredUsername: + yield return Destinations.AccessToken; - switch (claim.Type) - { - case Claims.Name or Claims.PreferredUsername: - yield return Destinations.AccessToken; + if (claim.Subject!.HasScope(Scopes.Profile)) + yield return Destinations.IdentityToken; - if (claim.Subject!.HasScope(Scopes.Profile)) - yield return Destinations.IdentityToken; + yield break; - yield break; + case Claims.Email: + yield return Destinations.AccessToken; - case Claims.Email: - yield return Destinations.AccessToken; + if (claim.Subject!.HasScope(Scopes.Email)) + yield return Destinations.IdentityToken; - if (claim.Subject!.HasScope(Scopes.Email)) - yield return Destinations.IdentityToken; + yield break; - yield break; + case Claims.Role: + yield return Destinations.AccessToken; - case Claims.Role: - yield return Destinations.AccessToken; + if (claim.Subject!.HasScope(Scopes.Roles)) + yield return Destinations.IdentityToken; - if (claim.Subject!.HasScope(Scopes.Roles)) - yield return Destinations.IdentityToken; + yield break; - 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; - // 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 Destinations.AccessToken; - yield break; - } + default: + yield return Destinations.AccessToken; + yield break; } } } diff --git a/samples/Geonosis/Geonosis.Auth/Controllers/HomeController.cs b/samples/Geonosis/Geonosis.Auth/Controllers/HomeController.cs index 2e3b318e..5d4360b8 100644 --- a/samples/Geonosis/Geonosis.Auth/Controllers/HomeController.cs +++ b/samples/Geonosis/Geonosis.Auth/Controllers/HomeController.cs @@ -2,24 +2,23 @@ using Geonosis.Auth.Models; using Microsoft.AspNetCore.Mvc; -namespace Geonosis.Auth.Controllers +namespace Geonosis.Auth.Controllers; + +public class HomeController : Controller { - public class HomeController : Controller + public IActionResult Index() { - public IActionResult Index() - { - return View(); - } + return View(); + } - public IActionResult Privacy() - { - return View(); - } + public IActionResult Privacy() + { + return View(); + } - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public IActionResult Error() - { - return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); - } + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } } diff --git a/samples/Geonosis/Geonosis.Auth/Data/ApplicationDbContext.cs b/samples/Geonosis/Geonosis.Auth/Data/ApplicationDbContext.cs index b5a63264..6d64bc45 100644 --- a/samples/Geonosis/Geonosis.Auth/Data/ApplicationDbContext.cs +++ b/samples/Geonosis/Geonosis.Auth/Data/ApplicationDbContext.cs @@ -1,18 +1,17 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; -namespace Geonosis.Auth.Data +namespace Geonosis.Auth.Data; + +internal sealed class ApplicationDbContext : IdentityDbContext { - internal sealed class ApplicationDbContext : IdentityDbContext + public ApplicationDbContext(DbContextOptions options) + : base(options) { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } + } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); } } diff --git a/samples/Geonosis/Geonosis.Auth/Data/ApplicationUser.cs b/samples/Geonosis/Geonosis.Auth/Data/ApplicationUser.cs index 92984de7..f622964d 100644 --- a/samples/Geonosis/Geonosis.Auth/Data/ApplicationUser.cs +++ b/samples/Geonosis/Geonosis.Auth/Data/ApplicationUser.cs @@ -1,8 +1,7 @@ using Microsoft.AspNetCore.Identity; -namespace Geonosis.Auth.Data +namespace Geonosis.Auth.Data; + +public sealed class ApplicationUser : IdentityUser { - public sealed class ApplicationUser : IdentityUser - { - } } diff --git a/samples/Geonosis/Geonosis.Auth/Helpers/FormValueRequiredAttribute.cs b/samples/Geonosis/Geonosis.Auth/Helpers/FormValueRequiredAttribute.cs index 28862bd4..b1ff291c 100644 --- a/samples/Geonosis/Geonosis.Auth/Helpers/FormValueRequiredAttribute.cs +++ b/samples/Geonosis/Geonosis.Auth/Helpers/FormValueRequiredAttribute.cs @@ -1,40 +1,39 @@ using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; -namespace Geonosis.Auth.Helpers +namespace Geonosis.Auth.Helpers; + +internal sealed class FormValueRequiredAttribute : ActionMethodSelectorAttribute { - internal sealed class FormValueRequiredAttribute : ActionMethodSelectorAttribute + private readonly string _name; + + public FormValueRequiredAttribute(string name) + { + _name = name; + } + + public override bool IsValidForRequest(RouteContext context, ActionDescriptor action) { - private readonly string _name; + if (string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase) || + string.Equals(context.HttpContext.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase) || + string.Equals(context.HttpContext.Request.Method, "DELETE", StringComparison.OrdinalIgnoreCase) || + string.Equals(context.HttpContext.Request.Method, "TRACE", StringComparison.OrdinalIgnoreCase)) + { + return false; + } - public FormValueRequiredAttribute(string name) + if (string.IsNullOrEmpty(context.HttpContext.Request.ContentType)) { - _name = name; + return false; } - public override bool IsValidForRequest(RouteContext context, ActionDescriptor action) + if (!context.HttpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase) || - string.Equals(context.HttpContext.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase) || - string.Equals(context.HttpContext.Request.Method, "DELETE", StringComparison.OrdinalIgnoreCase) || - string.Equals(context.HttpContext.Request.Method, "TRACE", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (string.IsNullOrEmpty(context.HttpContext.Request.ContentType)) - { - return false; - } - - if (!context.HttpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - return !string.IsNullOrEmpty(context.HttpContext.Request.Form[_name]); + return false; } - public string Name => _name; + return !string.IsNullOrEmpty(context.HttpContext.Request.Form[_name]); } + + public string Name => _name; } diff --git a/samples/Geonosis/Geonosis.Auth/Models/ErrorViewModel.cs b/samples/Geonosis/Geonosis.Auth/Models/ErrorViewModel.cs index 04549faf..0951fe2f 100644 --- a/samples/Geonosis/Geonosis.Auth/Models/ErrorViewModel.cs +++ b/samples/Geonosis/Geonosis.Auth/Models/ErrorViewModel.cs @@ -1,9 +1,8 @@ -namespace Geonosis.Auth.Models +namespace Geonosis.Auth.Models; + +public class ErrorViewModel { - public class ErrorViewModel - { - public string? RequestId { get; set; } + public string? RequestId { get; set; } - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - } + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); } diff --git a/samples/Geonosis/Geonosis.Auth/ViewModels/Authorization/AuthorizeViewModel.cs b/samples/Geonosis/Geonosis.Auth/ViewModels/Authorization/AuthorizeViewModel.cs index 13073b60..68de1075 100644 --- a/samples/Geonosis/Geonosis.Auth/ViewModels/Authorization/AuthorizeViewModel.cs +++ b/samples/Geonosis/Geonosis.Auth/ViewModels/Authorization/AuthorizeViewModel.cs @@ -1,13 +1,12 @@ using System.ComponentModel.DataAnnotations; -namespace Geonosis.Auth.ViewModels.Authorization +namespace Geonosis.Auth.ViewModels.Authorization; + +internal class AuthorizeViewModel { - internal class AuthorizeViewModel - { - [Display(Name = "Application")] - public string? ApplicationName { get; set; } + [Display(Name = "Application")] + public string? ApplicationName { get; set; } - [Display(Name = "Scope")] - public string? Scope { get; set; } - } + [Display(Name = "Scope")] + public string? Scope { get; set; } } diff --git a/samples/Geonosis/Geonosis.Auth/ViewModels/Shared/ErrorViewModel.cs b/samples/Geonosis/Geonosis.Auth/ViewModels/Shared/ErrorViewModel.cs index b9eebae3..2c326577 100644 --- a/samples/Geonosis/Geonosis.Auth/ViewModels/Shared/ErrorViewModel.cs +++ b/samples/Geonosis/Geonosis.Auth/ViewModels/Shared/ErrorViewModel.cs @@ -1,9 +1,8 @@ -namespace Geonosis.Auth.ViewModels.Shared +namespace Geonosis.Auth.ViewModels.Shared; + +internal class ErrorViewModel { - internal class ErrorViewModel - { - public string? RequestId { get; set; } + public string? RequestId { get; set; } - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - } + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); } diff --git a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Weather/ClientWeatherForecaster.cs b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Weather/ClientWeatherForecaster.cs index 0480516f..3af286c0 100644 --- a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Weather/ClientWeatherForecaster.cs +++ b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Weather/ClientWeatherForecaster.cs @@ -1,11 +1,10 @@ using System.Net.Http.Json; -namespace Geonosis.Ui.Client.Weather +namespace Geonosis.Ui.Client.Weather; + +internal sealed class ClientWeatherForecaster(HttpClient httpClient) : IWeatherForecaster { - internal sealed class ClientWeatherForecaster(HttpClient httpClient) : IWeatherForecaster - { - public async Task> GetWeatherForecastAsync() => - await httpClient.GetFromJsonAsync("/weather-forecast") ?? - throw new IOException("No weather forecast!"); - } + public async Task> GetWeatherForecastAsync() => + await httpClient.GetFromJsonAsync("/weather-forecast") ?? + throw new IOException("No weather forecast!"); } diff --git a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Weather/IWeatherForecaster.cs b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Weather/IWeatherForecaster.cs index 87505713..1f9240b1 100644 --- a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Weather/IWeatherForecaster.cs +++ b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Weather/IWeatherForecaster.cs @@ -1,7 +1,6 @@ -namespace Geonosis.Ui.Client.Weather +namespace Geonosis.Ui.Client.Weather; + +public interface IWeatherForecaster { - public interface IWeatherForecaster - { - Task> GetWeatherForecastAsync(); - } + Task> GetWeatherForecastAsync(); } diff --git a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Weather/WeatherForecast.cs b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Weather/WeatherForecast.cs index 3a3d4725..18b6e5c7 100644 --- a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Weather/WeatherForecast.cs +++ b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui.Client/Weather/WeatherForecast.cs @@ -1,10 +1,9 @@ -namespace Geonosis.Ui.Client.Weather +namespace Geonosis.Ui.Client.Weather; + +public sealed class WeatherForecast { - public sealed class WeatherForecast - { - public DateOnly Date { get; set; } - public int TemperatureC { get; set; } - public string? Summary { get; set; } - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - } + public DateOnly Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } diff --git a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Program.cs b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Program.cs index e5f293d7..727f6b95 100644 --- a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Program.cs +++ b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Program.cs @@ -203,18 +203,29 @@ var properties = new AuthenticationProperties(result.Properties.Items) { RedirectUri = result.Properties.RedirectUri ?? "/", + + // Set the creation and expiration dates of the ticket to null to decorrelate the lifetime + // of the resulting authentication cookie from the lifetime of the identity token returned by + // the authorization server (if applicable). In this case, the expiration date time will be + // automatically computed by the cookie handler using the lifetime configured in the options. + // + // Applications that prefer binding the lifetime of the ticket stored in the authentication cookie + // to the identity token returned by the identity provider can remove or comment these two lines: IssuedUtc = null, ExpiresUtc = null, + + // Note: this flag controls whether the authentication cookie that will be returned to the + // browser will be treated as a session cookie (i.e destroyed when the browser is closed) + // or as a persistent cookie. In both cases, the lifetime of the authentication ticket is + // always stored as protected data, preventing malicious users from trying to use an + // authentication cookie beyond the lifetime of the authentication ticket itself. IsPersistent = true }; // If needed, the tokens returned by the authorization server can be stored in the authentication cookie. // To make cookies less heavy, tokens that are not used are filtered out before creating the cookie. properties.StoreTokens(result.Properties.GetTokens().Where(token => token.Name is - OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken or - OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessTokenExpirationDate or - OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken or - OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken)); + OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken)); return TypedResults.SignIn(new ClaimsPrincipal(identity), properties); }).DisableAntiforgery(); diff --git a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Weather/ServerWeatherForecaster.cs b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Weather/ServerWeatherForecaster.cs index 5a8cd805..28028a95 100644 --- a/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Weather/ServerWeatherForecaster.cs +++ b/samples/Geonosis/Geonosis.Ui/Geonosis.Ui/Weather/ServerWeatherForecaster.cs @@ -4,31 +4,30 @@ using OpenIddict.Client.AspNetCore; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Geonosis.Ui.Weather +namespace Geonosis.Ui.Weather; + +internal sealed class ServerWeatherForecaster( + IHttpContextAccessor accessor, HttpClient client, OpenIddictClientService service) : IWeatherForecaster { - internal sealed class ServerWeatherForecaster( - IHttpContextAccessor accessor, HttpClient client, OpenIddictClientService service) : IWeatherForecaster + public async Task> GetWeatherForecastAsync() { - public async Task> GetWeatherForecastAsync() - { - var token = await accessor.HttpContext!.GetTokenAsync(OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken) - ?? throw new InvalidOperationException("The access token cannot be retrieved."); + var token = await accessor.HttpContext!.GetTokenAsync(OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken) + ?? throw new InvalidOperationException("The access token cannot be retrieved."); - var result = await service.AuthenticateWithTokenExchangeAsync(new() - { - SubjectToken = token, - SubjectTokenType = TokenTypeIdentifiers.AccessToken, - RequestedTokenType = TokenTypeIdentifiers.AccessToken, - Scopes = ["Weather.Read"] - }); + var result = await service.AuthenticateWithTokenExchangeAsync(new() + { + SubjectToken = token, + SubjectTokenType = TokenTypeIdentifiers.AccessToken, + RequestedTokenType = TokenTypeIdentifiers.AccessToken, + Scopes = ["Weather.Read"] + }); - using var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast"); - request.Headers.Authorization = new("Bearer", result.IssuedToken); + using var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast"); + request.Headers.Authorization = new("Bearer", result.IssuedToken); - using var response = await client.SendAsync(request); - response.EnsureSuccessStatusCode(); + using var response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); - return await response.Content.ReadFromJsonAsync() ?? []; - } + return await response.Content.ReadFromJsonAsync() ?? []; } }