fengling-auth-service/Controllers/TokenController.cs
movingsam d4aff05804 refactor: clean up Member module and update Console
- Remove redundant PointsRule repositories (use single PointsRuleRepository)
- Clean up Member migrations and consolidate to single Init migration
- Update Console frontend API and components for Tenant
- Add H5LinkService for member H5 integration
2026-02-18 23:34:40 +08:00

272 lines
10 KiB
C#

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<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ILogger<TokenController> logger)
: ControllerBase
{
[HttpPost("token")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("OpenIddict request is null");
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
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<IActionResult> 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<Claim>
{
new(Claims.Subject, await userManager.GetUserIdAsync(user)),
new(Claims.Name, await userManager.GetUserNameAsync(user) ?? throw new InvalidOperationException()),
new(Claims.Email, await userManager.GetEmailAsync(user) ?? ""),
new("tenant_id", user.TenantInfo.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);
// 设置 Claim Destinations
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
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<IActionResult> 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<string, string>
{
[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<string, string>
{
[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<string> 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;
// 明确处理租户 ID - 这是业务关键信息
case "tenant_id":
yield return OpenIddictConstants.Destinations.AccessToken;
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<IActionResult> 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<Claim>
{
new(Claims.Subject, await userManager.GetUserIdAsync(user)),
new(Claims.Name, await userManager.GetUserNameAsync(user) ?? string.Empty),
new(Claims.Email, await userManager.GetEmailAsync(user) ?? ""),
new("tenant_id", user.TenantInfo.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);
// 设置 Claim Destinations
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);
}
}