/* * 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; using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Mvc.Server.Helpers; using Mvc.Server.Models; using Mvc.Server.ViewModels.Authorization; using Mvc.Server.ViewModels.Shared; using OpenIddict.Abstractions; using OpenIddict.Core; using OpenIddict.EntityFrameworkCore.Models; using OpenIddict.Server.AspNetCore; using static OpenIddict.Abstractions.OpenIddictConstants; namespace Mvc.Server { public class AuthorizationController : Controller { private readonly OpenIddictApplicationManager _applicationManager; private readonly SignInManager _signInManager; private readonly UserManager _userManager; public AuthorizationController( OpenIddictApplicationManager applicationManager, SignInManager signInManager, UserManager userManager) { _applicationManager = applicationManager; _signInManager = signInManager; _userManager = userManager; } #region Authorization code, implicit and implicit flows // Note: to support interactive flows like the code flow, // you must provide your own authorization endpoint action: [Authorize, HttpGet("~/connect/authorize")] public async Task Authorize() { var request = HttpContext.GetOpenIddictServerRequest(); if (request == null) { throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); } // Retrieve the application details from the database. var application = await _applicationManager.FindByClientIdAsync(request.ClientId); if (application == null) { return View("Error", new ErrorViewModel { Error = Errors.InvalidClient, ErrorDescription = "Details concerning the calling client application cannot be found in the database" }); } // Flow the request_id to allow OpenIddict to restore // the original authorization request from the cache. return View(new AuthorizeViewModel { ApplicationName = await _applicationManager.GetDisplayNameAsync(application), Parameters = request.GetFlattenedParameters(), Scope = request.Scope }); } [Authorize, FormValueRequired("submit.Accept")] [HttpPost("~/connect/authorize"), ValidateAntiForgeryToken] public async Task Accept() { var request = HttpContext.GetOpenIddictServerRequest(); if (request == null) { throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); } // Retrieve the profile of the logged in user. var user = await _userManager.GetUserAsync(User); if (user == null) { return View("Error", new ErrorViewModel { Error = Errors.ServerError, ErrorDescription = "An internal error has occurred" }); } var principal = await _signInManager.CreateUserPrincipalAsync(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. principal.SetScopes(request.GetScopes()); principal.SetResources("resource_server"); foreach (var claim in principal.Claims) { claim.SetDestinations(GetDestinations(claim, principal)); } // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. return SignIn(principal, 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); // Note: the logout action is only useful when implementing interactive // flows like the authorization code flow or the implicit flow. [HttpGet("~/connect/logout")] public IActionResult Logout() { var request = HttpContext.GetOpenIddictServerRequest(); if (request == null) { throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); } // Flow the request_id to allow OpenIddict to restore // the original logout request from the distributed cache. return View(new LogoutViewModel { Parameters = request.GetFlattenedParameters() }); } [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. return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } #endregion #region Password, authorization code and refresh token flows // Note: to support non-interactive flows like password, // you must provide your own token endpoint action: [HttpPost("~/connect/token"), Produces("application/json")] public async Task Exchange() { var request = HttpContext.GetOpenIddictServerRequest(); if (request == null) { throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); } if (request.IsPasswordGrantType()) { var user = await _userManager.FindByNameAsync(request.Username); if (user == null) { var properties = new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid." }); return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } // 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) { var properties = new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid." }); return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } var principal = await _signInManager.CreateUserPrincipalAsync(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. principal.SetScopes(request.GetScopes()); principal.SetResources("resource_server"); foreach (var claim in principal.Claims) { claim.SetDestinations(GetDestinations(claim, principal)); } // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } else if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType()) { // Retrieve the claims principal stored in the authorization code/refresh token. var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal; // Retrieve the user profile corresponding to the authorization code/refresh token. // Note: if you want to automatically invalidate the authorization code/refresh token // when the user password/roles change, use the following line instead: // var user = _signInManager.ValidateSecurityStampAsync(info.Principal); var user = await _userManager.GetUserAsync(principal); if (user == null) { var properties = new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." }); return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } // Ensure the user is still allowed to sign in. if (!await _signInManager.CanSignInAsync(user)) { var properties = new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." }); return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } foreach (var claim in principal.Claims) { claim.SetDestinations(GetDestinations(claim, principal)); } // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } throw new InvalidOperationException("The specified grant type is not supported."); } #endregion private IEnumerable GetDestinations(Claim claim, ClaimsPrincipal principal) { // 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: yield return Destinations.AccessToken; if (principal.HasScope(Scopes.Profile)) yield return Destinations.IdentityToken; yield break; case Claims.Email: yield return Destinations.AccessToken; if (principal.HasScope(Scopes.Email)) yield return Destinations.IdentityToken; yield break; case Claims.Role: yield return Destinations.AccessToken; if (principal.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; } } } }