using Fengling.AuthService.Data; using Fengling.AuthService.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using System.Security.Claims; using System.Security.Cryptography; using Microsoft.AspNetCore; using static OpenIddict.Abstractions.OpenIddictConstants; namespace Fengling.AuthService.Controllers; [ApiController] [Route("connect")] public class TokenController( IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager, IOpenIddictScopeManager scopeManager, UserManager userManager, SignInManager signInManager, ILogger logger) : ControllerBase { private readonly ILogger _logger = logger; [HttpPost("token")] public async Task Exchange() { var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("OpenIddict request is null"); var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme); if (request.IsAuthorizationCodeGrantType()) { return await ExchangeAuthorizationCodeAsync(request, result); } if (request.IsRefreshTokenGrantType()) { return await ExchangeRefreshTokenAsync(request); } if (request.IsPasswordGrantType()) { return await ExchangePasswordAsync(request); } return BadRequest(new OpenIddictResponse { Error = Errors.UnsupportedGrantType, ErrorDescription = "The specified grant type is not supported." }); } private async Task ExchangeAuthorizationCodeAsync(OpenIddictRequest request, AuthenticateResult result) { var application = await applicationManager.FindByClientIdAsync(request.ClientId); if (application == null) { return BadRequest(new OpenIddictResponse { Error = Errors.InvalidClient, ErrorDescription = "The specified client is invalid." }); } var authorization = await authorizationManager.FindAsync( subject: result.Principal?.GetClaim(Claims.Subject), client: await applicationManager.GetIdAsync(application), status: Statuses.Valid, type: AuthorizationTypes.Permanent, scopes: request.GetScopes()).FirstOrDefaultAsync(); if (authorization == null) { return BadRequest(new OpenIddictResponse { Error = Errors.InvalidGrant, ErrorDescription = "The authorization code is invalid." }); } var user = await userManager.FindByIdAsync(result.Principal?.GetClaim(Claims.Subject)); if (user == null || user.IsDeleted) { return BadRequest(new OpenIddictResponse { Error = Errors.InvalidGrant, ErrorDescription = "The user is no longer valid." }); } var claims = new List { new(Claims.Subject, await userManager.GetUserIdAsync(user)), new(Claims.Name, await userManager.GetUserNameAsync(user)), new(Claims.Email, await userManager.GetEmailAsync(user) ?? ""), new("tenant_id", user.TenantId.ToString()) }; var roles = await userManager.GetRolesAsync(user); foreach (var role in roles) { claims.Add(new Claim(Claims.Role, role)); } var identity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); var principal = new ClaimsPrincipal(identity); principal.SetScopes(request.GetScopes()); principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync()); principal.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization)); return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } private async Task ExchangeRefreshTokenAsync(OpenIddictRequest request) { var principalResult = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); if (principalResult is not { Succeeded: true }) { return BadRequest(new OpenIddictResponse { Error = Errors.InvalidGrant, ErrorDescription = "The refresh token is invalid." }); } var user = await userManager.GetUserAsync(principalResult.Principal); if (user == null || user.IsDeleted) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." }!)); } // 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] = OpenIddictConstants.Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." }!)); } var principal = principalResult.Principal; foreach (var claim in principal!.Claims) { claim.SetDestinations(GetDestinations(claim, principal)); } principal.SetScopes(request.GetScopes()); principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync()); return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } 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 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; } } private async Task ExchangePasswordAsync(OpenIddictRequest request) { var user = await userManager.FindByNameAsync(request.Username); if (user == null || user.IsDeleted) { return BadRequest(new OpenIddictResponse { Error = Errors.InvalidGrant, ErrorDescription = "用户名或密码错误" }); } var signInResult = await signInManager.CheckPasswordSignInAsync(user, request.Password, false); if (!signInResult.Succeeded) { return BadRequest(new OpenIddictResponse { Error = Errors.InvalidGrant, ErrorDescription = "用户名或密码错误" }); } var claims = new List { new(Claims.Subject, await userManager.GetUserIdAsync(user)), new(Claims.Name, await userManager.GetUserNameAsync(user)), new(Claims.Email, await userManager.GetEmailAsync(user) ?? ""), new("tenant_id", user.TenantId.ToString()) }; var roles = await userManager.GetRolesAsync(user); foreach (var role in roles) { claims.Add(new Claim(Claims.Role, role)); } var identity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); var principal = new ClaimsPrincipal(identity); principal.SetScopes(request.GetScopes()); principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync()); return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } }