using Fengling.Platform.Domain.AggregatesModel.UserAggregate; using Fengling.Platform.Domain.AggregatesModel.RoleAggregate; using Fengling.Platform.Infrastructure; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using System.Security.Claims; using Fengling.AuthService.Configuration; using Fengling.AuthService.ViewModels; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Primitives; using Swashbuckle.AspNetCore.Annotations; using static OpenIddict.Abstractions.OpenIddictConstants; namespace Fengling.AuthService.Controllers; public class AuthorizationController( IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager, IOpenIddictScopeManager scopeManager, SignInManager signInManager, UserManager userManager, ILogger logger) : Controller { [Authorize, FormValueRequired("submit.Accept")] [Tags("submit.Accept")] [HttpPost("~/connect/authorize"), ValidateAntiForgeryToken] [SwaggerIgnore] 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."); if (user == null) { 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: OpenIddictConstants.Statuses.Valid, type: OpenIddictConstants.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.Any() && await applicationManager.HasConsentTypeAsync(application, OpenIddictConstants.ConsentTypes.External)) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The logged in user is not allowed to access this client application." }!)); } var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme); var principal = result.Principal!; // 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(await scopeManager.ListResourcesAsync(principal.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(); if (authorization == null) { authorization = await authorizationManager.CreateAsync( principal: principal, subject: await userManager.GetUserIdAsync(user)!, client: await applicationManager.GetIdAsync(application) ?? string.Empty, type: OpenIddictConstants.AuthorizationTypes.Permanent, scopes: principal.GetScopes()); } principal.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization)); 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")] [Tags("submit.Deny")] [SwaggerIgnore] [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/authorize")] [HttpPost("~/connect/authorize")] [Tags("Authorize")] [IgnoreAntiforgeryToken] public async Task Authorize() { var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); // If prompt=login was specified by the client application, // immediately return the user agent to the login page. if (request.HasPromptValue(OpenIddictConstants.PromptValues.Login)) { // To avoid endless login -> authorization redirects, the prompt=login flag // is removed from the authorization request payload before redirecting the user. var prompt = string.Join(" ", request.GetPromptValues().Remove(OpenIddictConstants.PromptValues.Login)); var parameters = Request.HasFormContentType ? Request.Form.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList() : Request.Query.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList(); parameters.Add(KeyValuePair.Create(OpenIddictConstants.Parameters.Prompt, new StringValues(prompt))); return Challenge( authenticationSchemes: IdentityConstants.ApplicationScheme, properties: new AuthenticationProperties { RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters) }); } // Retrieve the user principal stored in the authentication cookie. // If a max_age parameter was provided, ensure that the cookie is not too old. // If the user principal can't be extracted or the cookie is too old, redirect the user to the login page. var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme); if (result is not { Succeeded: true } || (request.MaxAge != null && result.Properties?.IssuedUtc != null && DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) { // If the client application requested promptless authentication, // return an error indicating that the user is not logged in. if (request.HasPromptValue(OpenIddictConstants.PromptValues.None)) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.LoginRequired, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in." }!)); } return Challenge( authenticationSchemes: IdentityConstants.ApplicationScheme, properties: new AuthenticationProperties { RedirectUri = Request.PathBase + Request.Path + QueryString.Create( Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList()) }); } // 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: OpenIddictConstants.Statuses.Valid, type: OpenIddictConstants.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 OpenIddictConstants.ConsentTypes.External when !authorizations.Any(): return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The logged in user is not allowed to access this client application." }!)); // If the consent is implicit or if an authorization was found, // return an authorization response without displaying the consent form. case OpenIddictConstants.ConsentTypes.Implicit: case OpenIddictConstants.ConsentTypes.External when authorizations.Any(): case OpenIddictConstants.ConsentTypes.Explicit when authorizations.Any() && !request.HasPromptValue(OpenIddictConstants.PromptValues.Consent): 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(await scopeManager.ListResourcesAsync(principal.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(); if (authorization == null) { authorization = await authorizationManager.CreateAsync( principal: principal, subject: await userManager.GetUserIdAsync(user), client: (await applicationManager.GetIdAsync(application))!, type: OpenIddictConstants.AuthorizationTypes.Permanent, scopes: principal.GetScopes()); } principal.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization)); foreach (var claim in principal.Claims) { claim.SetDestinations(GetDestinations(claim, principal)); } return SignIn(principal, 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 OpenIddictConstants.ConsentTypes.Explicit when request.HasPromptValue(OpenIddictConstants.PromptValues.None): case OpenIddictConstants.ConsentTypes.Systematic when request.HasPromptValue(OpenIddictConstants.PromptValues.None): return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Interactive user consent is required." }!)); // In every other case, render the consent form. default: return View(new AuthorizeViewModel(await applicationManager.GetDisplayNameAsync(application), request.Scope)); } } private IEnumerable GetDestinations(Claim claim, ClaimsPrincipal principal) { // Note: by default, claims are NOT automatically included in 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 OpenIddictConstants.Claims.Name: yield return OpenIddictConstants.Destinations.AccessToken; if (principal.HasScope(OpenIddictConstants.Permissions.Scopes.Profile)) yield return OpenIddictConstants.Destinations.IdentityToken; yield break; case OpenIddictConstants.Claims.Email: yield return OpenIddictConstants.Destinations.AccessToken; if (principal.HasScope(OpenIddictConstants.Permissions.Scopes.Email)) yield return OpenIddictConstants.Destinations.IdentityToken; yield break; case OpenIddictConstants.Claims.Role: yield return OpenIddictConstants.Destinations.AccessToken; if (principal.HasScope(OpenIddictConstants.Permissions.Scopes.Roles)) yield return OpenIddictConstants.Destinations.IdentityToken; yield break; // Never include the security stamp in the access and identity tokens, as it's a secret value. case "AspNet.Identity.SecurityStamp": yield break; default: yield return OpenIddictConstants.Destinations.AccessToken; yield break; } } [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)] [HttpGet("~/connect/userinfo")] public async Task UserInfo() { var user = await userManager.GetUserAsync(User) ?? throw new InvalidOperationException("The user details cannot be retrieved."); // 获取用户的角色 var roles = await userManager.GetRolesAsync(user); // 获取用户的租户信息 var tenantInfo = user.TenantInfo; var claims = new List { new(OpenIddictConstants.Claims.Subject, await userManager.GetUserIdAsync(user)), new(OpenIddictConstants.Claims.Name, user.UserName!), new(OpenIddictConstants.Claims.PreferredUsername, user.UserName!) }; if (claims == null) throw new ArgumentNullException(nameof(claims)); if (!string.IsNullOrEmpty(user.Email)) { claims.Add(new(OpenIddictConstants.Claims.Email, user.Email!)); } // 添加角色 claims foreach (var role in roles) { claims.Add(new(OpenIddictConstants.Claims.Role, role)); } // 添加自定义 tenant 相关 claims claims.Add(new Claim("tenant_id", tenantInfo.TenantId.ToString())); claims.Add(new Claim("tenant_code", tenantInfo.TenantCode)); claims.Add(new Claim("tenant_name", tenantInfo.TenantName)); return Ok(new Dictionary { ["sub"] = await userManager.GetUserIdAsync(user), ["name"] = user.UserName ?? "Anonymous", ["preferred_username"] = user.UserName ?? "Anonymous", ["email"] = user.Email ?? "", ["role"] = roles.ToArray(), ["tenant_id"] = tenantInfo.TenantId.ToString(), ["tenant_code"] = tenantInfo?.TenantCode ?? "", ["tenant_name"] = tenantInfo?.TenantName ?? "" }); } }