OpenIddict IDP能否实现多认证方案兼容两种令牌?
我正在开发单点登录(SSO)系统,配套做了用户管理的Admin Client应用。用户信息存储在身份提供商(IDP)的数据库中,因此Admin Client通过调用IDP的API操作用户数据,避免多源存储或直接访问。
系统采用OpenIddict Server作为IDP,OpenIddict Client作为Admin Client应用。我尝试用两种认证流程实现需求:
- 用户登录时采用**授权码流(Authorization Code Flow)**获取包含用户声明的访问令牌;
- Admin Client调用IDP API时采用**客户端凭证流(Client Credentials Flow)**获取访问令牌。
Admin Client会在用户发起请求到调用IDP API的控制器时检查用户角色,因此选择客户端凭证流来授权Admin Client对IDP的请求。
获取访问令牌的代码如下:
private TokenResponse RequestAccessToken() { lock (accessTokenLock) { if (DateTime.Now.AddMinutes(1) > accessTokenExpiry) { HttpRequestMessage request = new() { Method = HttpMethod.Post, RequestUri = new Uri($"{client.BaseAddress}connect/token"), Content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("grant_type", "client_credentials"), new KeyValuePair<string, string>("client_id", clientId), new KeyValuePair<string, string>("client_secret", clientSecret) }) }; HttpResponseMessage? response = client.Send(request); response.EnsureSuccessStatusCode(); var stringResponse = response.Content.ReadAsStringAsync().Result; var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(stringResponse); accessTokenExpiry = DateTime.Now.AddSeconds(tokenResponse.ExpiresIn); return tokenResponse; } return new TokenResponse(); } } private class TokenResponse { [JsonProperty("token_type")] public string TokenType { get; set; } [JsonProperty("access_token")] public string AccessToken { get; set; } [JsonProperty("expires_in")] public int ExpiresIn { get; set; } }
使用该令牌调用IDP API的代码:
public async Task<List<UserDTO>> GetAllUsersAsync() { try { TokenResponse accessToken = RequestAccessToken(); HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = new Uri("useradministration/getallusersdetailsandclaims", UriKind.Relative), Headers = { Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken) } }; var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); var userData = System.Text.Json.JsonSerializer.Deserialize<List<UserDTO>>(content, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); return userData; } catch (Exception ex) { throw new InvalidOperationException(ex.Message); } }
IDP中的useradministration/getallusersdetailsandclaims端点受[Authorize]特性保护,但使用客户端凭证流获取的令牌发起请求时,收到了IDP返回的未授权响应。我猜测IDP未验证该请求中的令牌,且客户端凭证流获取的令牌与授权码流获取的令牌存在差异。
请问能否在IDP中实现两种不同的认证方案,使其能够同时处理授权码流和客户端凭证流获取的令牌? 这样既能验证Admin Client请求发送的令牌,也能处理其他方式发起的请求的授权/拒绝。
完全可以在OpenIddict Server中配置多种认证方案来兼容不同流程的令牌,核心是确保两种令牌的验证逻辑都被正确注册,且API端点能根据令牌类型选择对应的验证方式。
步骤1:注册双认证方案
在IDP的Program.cs(或Startup类)中,同时注册针对**用户令牌(授权码流)和客户端令牌(客户端凭证流)**的JWT Bearer认证方案:
builder.Services.AddAuthentication(options => { // 可根据实际需求调整默认认证方案 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) // 针对授权码流的用户令牌认证 .AddJwtBearer("UserBearer", options => { options.Authority = builder.Configuration["OpenIddict:Authority"]; options.Audience = "your-api-resource-name"; // 需与授权码流请求的audience一致 options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, NameClaimType = OpenIddictConstants.Claims.Name, RoleClaimType = OpenIddictConstants.Claims.Role }; }) // 针对客户端凭证流的客户端令牌认证 .AddJwtBearer("ClientBearer", options => { options.Authority = builder.Configuration["OpenIddict:Authority"]; options.Audience = "your-api-resource-name"; // 需与客户端凭证流请求的audience一致 options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, NameClaimType = OpenIddictConstants.Claims.ClientId, RoleClaimType = OpenIddictConstants.Claims.Role }; });
步骤2:配置OpenIddict Server支持双流程
确保OpenIddict Server已启用授权码流和客户端凭证流:
builder.Services.AddOpenIddict() .AddServer(options => { // 启用所需端点 options.SetAuthorizationEndpointUris("/connect/authorize") .SetTokenEndpointUris("/connect/token") .SetUserinfoEndpointUris("/connect/userinfo"); // 启用两种认证流程 options.AllowAuthorizationCodeFlow() .AllowClientCredentialsFlow(); // 配置令牌签名(生产环境建议使用正式证书) options.AddDevelopmentEncryptionCertificate() .AddDevelopmentSigningCertificate(); // 配置资源服务器兼容AspNetCore options.UseAspNetCore() .EnableAuthorizationEndpointPassthrough() .EnableTokenEndpointPassthrough() .EnableUserinfoEndpointPassthrough(); });
步骤3:让API端点支持双认证
修改受保护的API端点,指定允许两种认证方案,并根据令牌类型区分权限逻辑:
[Authorize(AuthenticationSchemes = "UserBearer,ClientBearer")] [HttpGet("useradministration/getallusersdetailsandclaims")] public async Task<IActionResult> GetAllUsersDetailsAndClaims() { var principal = User; // 区分令牌类型:客户端凭证流的主体包含client_id声明 if (principal.HasClaim(c => c.Type == OpenIddictConstants.Claims.ClientId)) { var clientId = principal.FindFirstValue(OpenIddictConstants.Claims.ClientId); // 验证客户端是否有权限访问该API if (!IsClientAuthorized(clientId)) { return Forbid(); } } else { // 授权码流:验证用户角色 if (!principal.IsInRole("Admin")) { return Forbid(); } } // 业务逻辑:返回用户数据 var users = await _userManager.Users.ToListAsync(); var userDtos = users.Select(u => new UserDTO { Id = u.Id, UserName = u.UserName }).ToList(); return Ok(userDtos); }
额外注意事项
- Audience一致性:确保两种流程请求令牌时指定的
scope或aud与API配置的Audience一致,否则会验证失败。 - 客户端权限配置:给Admin Client配置对应API权限(如
useradministration.read),并在请求令牌时添加scope参数:// 在RequestAccessToken的FormUrlEncodedContent中添加 new KeyValuePair<string, string>("scope", "useradministration.read") - 异步优化:原
RequestAccessToken使用同步调用阻塞线程,建议重构为异步安全的缓存逻辑(比如用SemaphoreSlim替代lock)。
内容的提问来源于stack exchange,提问作者Jack Reynolds

