You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
}

额外注意事项

  1. Audience一致性:确保两种流程请求令牌时指定的scope或aud与API配置的Audience一致,否则会验证失败。
  2. 客户端权限配置:给Admin Client配置对应API权限(如useradministration.read),并在请求令牌时添加scope参数:
    // 在RequestAccessToken的FormUrlEncodedContent中添加
    new KeyValuePair<string, string>("scope", "useradministration.read")
    
  3. 异步优化:原RequestAccessToken使用同步调用阻塞线程,建议重构为异步安全的缓存逻辑(比如用SemaphoreSlim替代lock)。

内容的提问来源于stack exchange,提问作者Jack Reynolds

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 09:37:04