- 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
272 lines
10 KiB
C#
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);
|
|
}
|
|
} |