/* * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) * See https://github.com/openiddict/openiddict-core for more information concerning * the license and the contributors participating to this project. */ using System.Security.Claims; using System.Text.Json.Nodes; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; using OpenIddict.Client; using OpenIddict.Client.AspNetCore; using OpenIddict.Sandbox.AspNetCore.Server.Helpers; using OpenIddict.Sandbox.AspNetCore.Server.Models; using OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Authorization; using OpenIddict.Server.AspNetCore; using static OpenIddict.Abstractions.OpenIddictConstants; namespace OpenIddict.Sandbox.AspNetCore.Server; public class AuthorizationController : Controller { private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictAuthorizationManager _authorizationManager; private readonly OpenIddictClientService _clientService; private readonly IOpenIddictScopeManager _scopeManager; private readonly SignInManager _signInManager; private readonly UserManager _userManager; public AuthorizationController( IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager, OpenIddictClientService clientService, IOpenIddictScopeManager scopeManager, SignInManager signInManager, UserManager userManager) { _applicationManager = applicationManager; _authorizationManager = authorizationManager; _clientService = clientService; _scopeManager = scopeManager; _signInManager = signInManager; _userManager = userManager; } #region Authorization code, implicit and hybrid flows // Note: to support interactive flows like the code flow, // you must provide your own authorization endpoint action: [HttpGet("~/connect/authorize")] [HttpPost("~/connect/authorize")] [IgnoreAntiforgeryToken] public async Task Authorize() { // Note: the request object contains all the parameters specified in the query string or request form // or initially sent to the pushed authorization endpoint for a PAR-enabled authorization flow. // As such, the data contained in this object MUST NOT be serialized or returned unprotected to the // user agent (e.g as HTML hidden input fields). If only the query string or request form parameters // need to be resolved, the Request.Query and Request.Form collections must be used instead. 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)) { // 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 applications that want to allow the client to select the external authentication provider // that will be used to authenticate the user, the identity_provider parameter can be used for that. if (!string.IsNullOrEmpty(request.IdentityProvider)) { var registrations = await _clientService.GetClientRegistrationsAsync(); if (!registrations.Any(registration => string.Equals(registration.ProviderName, request.IdentityProvider, StringComparison.Ordinal))) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidRequest, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified identity provider is not valid." })); } var properties = _signInManager.ConfigureExternalAuthenticationProperties( provider: request.IdentityProvider, redirectUrl: Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = Request.PathBase + Request.Path + QueryString.Create( Request.HasFormContentType ? Request.Form : Request.Query) })); // Note: when only one client is registered in the client options, // specifying the issuer URI or the provider name is not required. properties.SetString(OpenIddictClientAspNetCoreConstants.Properties.ProviderName, request.IdentityProvider); // Ask the OpenIddict client middleware to redirect the user agent to the identity provider. return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme); } // 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 { ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application), Scope = request.Scope }); } } [Authorize, FormValueRequired("submit.Accept")] [HttpPost("~/connect/authorize"), ValidateAntiForgeryToken] public async Task Accept() { // Note: the request object contains all the parameters specified in the query string or request form // (or initially sent to the pushed authorization endpoint for a PAR-enabled authorization flow). // As such, the data contained in this object MUST NOT be serialized or returned unprotected to the // user agent (e.g as HTML hidden input fields). If only the query string or request form parameters // need to be resolved, the Request.Query and Request.Form collections must be used instead. 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." })); } // 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); } [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); #endregion #region Device flow // Note: to support the device authorization flow, you must provide your own verification endpoint action: [Authorize, HttpGet("~/connect/verify"), IgnoreAntiforgeryToken] public async Task Verify() { // Retrieve the claims principal associated with the user code. var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); if (result is { Succeeded: true } && !string.IsNullOrEmpty(result.Principal.GetClaim(Claims.ClientId))) { // Retrieve the application details from the database using the client_id stored in the principal. var application = await _applicationManager.FindByClientIdAsync(result.Principal.GetClaim(Claims.ClientId)!) ?? throw new InvalidOperationException("Details concerning the calling client application cannot be found."); // Render a form asking the user to confirm the authorization demand. return View(new VerifyViewModel { ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application), Scope = string.Join(" ", result.Principal.GetScopes()), UserCode = result.Properties.GetTokenValue(OpenIddictServerAspNetCoreConstants.Tokens.UserCode) }); } // If a user code was specified (e.g as part of the verification_uri_complete) // but is not valid, render a form asking the user to enter the user code manually. else if (!string.IsNullOrEmpty(result.Properties?.GetTokenValue(OpenIddictServerAspNetCoreConstants.Tokens.UserCode))) { return View(new VerifyViewModel { Error = Errors.InvalidToken, ErrorDescription = "The specified user code is not valid. Please make sure you typed it correctly." }); } // Otherwise, render a form asking the user to enter the user code manually. return View(new VerifyViewModel()); } [Authorize, FormValueRequired("submit.Accept")] [HttpPost("~/connect/verify"), ValidateAntiForgeryToken] public async Task VerifyAccept() { // 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 claims principal associated with the user code. var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); if (result is { Succeeded: true } && !string.IsNullOrEmpty(result.Principal.GetClaim(Claims.ClientId))) { // 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(result.Principal.GetScopes()); identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync()); identity.SetDestinations(GetDestinations); var properties = new AuthenticationProperties { // This property points to the address OpenIddict will automatically // redirect the user to after validating the authorization demand. RedirectUri = "/" }; return SignIn(new ClaimsPrincipal(identity), properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } // Redisplay the form when the user code is not valid. return View(new VerifyViewModel { Error = Errors.InvalidToken, ErrorDescription = "The specified user code is not valid. Please make sure you typed it correctly." }); } [Authorize, FormValueRequired("submit.Deny")] [HttpPost("~/connect/verify"), ValidateAntiForgeryToken] // Notify OpenIddict that the authorization grant has been denied by the resource owner. public IActionResult VerifyDeny() => Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties() { // This property points to the address OpenIddict will automatically // redirect the user to after rejecting the authorization demand. RedirectUri = "/" }); #endregion #region End session support for interactive flows like code and implicit // Note: the end session action is only useful when implementing interactive // flows like the authorization code flow or the implicit flow. [HttpGet("~/connect/endsession")] public IActionResult EndSession() => View(); [ActionName(nameof(EndSession)), HttpPost("~/connect/endsession"), ValidateAntiForgeryToken] public async Task EndSessionPost() { // 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 = "/" }); } #endregion #region Password, authorization code, device, refresh token and token exchange flows // Note: to support non-interactive flows like password, // you must provide your own token endpoint action: [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.IsPasswordGrantType()) { var user = await _userManager.FindByNameAsync(request.Username!); if (user is null) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid." })); } // Validate the username/password parameters and ensure the account is not locked out. var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password!, lockoutOnFailure: true); if (!result.Succeeded) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid." })); } // 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()); identity.SetDestinations(GetDestinations); // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } else if (request.IsAuthorizationCodeGrantType() || request.IsDeviceCodeGrantType() || request.IsRefreshTokenGrantType()) { // Retrieve the claims principal stored in the authorization code/device 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 { [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); // 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)]); identity.SetDestinations(GetDestinations); // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } else if (request.IsTokenExchangeGrantType()) { // 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)) { 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)]); // 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); // 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."); } #endregion private 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. switch (claim.Type) { case Claims.Name or Claims.PreferredUsername: yield return Destinations.AccessToken; if (claim.Subject!.HasScope(Scopes.Profile)) yield return Destinations.IdentityToken; yield break; case Claims.Email: yield return Destinations.AccessToken; if (claim.Subject!.HasScope(Scopes.Email)) yield return Destinations.IdentityToken; yield break; case Claims.Role: yield return Destinations.AccessToken; if (claim.Subject!.HasScope(Scopes.Roles)) yield return 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 Destinations.AccessToken; yield break; } } }