OpenIddict API服务器中ClaimsPrincipal缺失自定义声明的问题
问题:OpenIddict资源服务器无法自动获取Access Token中的自定义声明
环境配置
OpenIddict服务器(Server.csproj)
builder.Services.AddOpenIddict() .AddCore(options => ...) .AddServer(options => ...) .AddValidation(options => ...) .AddUIStore(options => ...) .AddUIApis(options => ...)
受保护的API服务器(Api.csproj)
builder.Services .AddOpenIddict() .AddValidation(options => { options.SetIssuer("..."); options .UseIntrospection() .SetClientId("xxx") .SetClientSecret("xxx"); options.UseSystemNetHttp(); options.UseAspNetCore(); });
现象
API接口可正常完成认证,但ClaimsPrincipal中缺少Access Token里的自定义声明:
接口代码示例:
[HttpGet()] [Authorize(Policy = "DefaultPolicy")] public async Task<IActionResult> MyGetMethod() { var accessToken = await this.HttpContext.GetTokenAsync("access_token"); var isAuthenticated = this.HttpContext.User?.Identity?.IsAuthenticated; ... }
通过Jwt.io解析access_token,可见包含目标自定义声明:
{ "sub": "user@xxx.xxx", "name": "Adrien Pellegrini", "email": "xxx@xxx.xxx", "employeeId": xxx, "username": "user", "given_name": "Adrien", "family_name": "Pellegrini", "office_location": "xxx", "role": "a_super_role", "oi_prst": "MyClientId", "client_id": "MyClientId", "oi_tkn_id": "0edc928b-xxx-xxx-xxxx-xxxxe9ca", "scope": "openid profile email roles offline_access", "jti": "f9d386bf-xxx-xxx-xxx-xxxxxx29fa", "exp": 1686050931, "iss": "https://localhost:5001/", "iat": 1686047331 }
但ClaimsPrincipal.Claims仅包含基础声明:
"sub: user@xxx.xxx" "jti: 9dbd9460-xxx-xxx-xxx-xxxxx34423" "token_usage: access_token" "client_id: MyClientId" "iat: 1686053762" "exp: 1686057362" "oi_tkn_typ: access_token" "oi_crt_dt: Tue, 06 Jun 2023 12:16:02 GMT" "oi_exp_dt: Tue, 06 Jun 2023 13:16:02 GMT" "oi_prst: MyClientId"
需求
希望将employeeId、username、given_name、family_name、office_location、role这些自定义声明自动包含到ClaimsPrincipal中。目前已尝试自定义验证处理程序,但需手动解析令牌,期望更自动化的实现方式:
public class MyEventHandler : IOpenIddictValidationHandler<ValidateTokenContext> { public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>() .UseScopedHandler<MyEventHandler>() .SetType(OpenIddictValidationHandlerType.Custom) .Build(); public ValueTask HandleAsync(ValidateTokenContext context) { var principal = context.Principal; // use and parse context.Token return default; } }
解决方案
方案1:服务器端配置内省端点返回自定义声明
由于当前使用**令牌内省(Introspection)**模式,OpenIddict内省端点默认仅返回标准字段,需在服务器端添加事件处理,将自定义声明注入内省响应:
.AddServer(options => { // 其他服务器配置... options.AddEventHandler<HandleIntrospectionRequestContext>(builder => { builder.UseInlineHandler(context => { var principal = context.TokenPrincipal; if (principal == null) return default; // 按需添加自定义声明到内省响应 if (principal.HasClaim(c => c.Type == "employeeId")) { context.Response["employeeId"] = principal.FindFirst("employeeId")!.Value; } if (principal.HasClaim(c => c.Type == "username")) { context.Response["username"] = principal.FindFirst("username")!.Value; } if (principal.HasClaim(c => c.Type == ClaimTypes.GivenName)) { context.Response["given_name"] = principal.FindFirst(ClaimTypes.GivenName)!.Value; } if (principal.HasClaim(c => c.Type == ClaimTypes.Surname)) { context.Response["family_name"] = principal.FindFirst(ClaimTypes.Surname)!.Value; } if (principal.HasClaim(c => c.Type == "office_location")) { context.Response["office_location"] = principal.FindFirst("office_location")!.Value; } if (principal.HasClaim(c => c.Type == ClaimTypes.Role)) { context.Response["role"] = principal.FindFirst(ClaimTypes.Role)!.Value; } return default; }); }); })
方案2:API客户端端配置声明映射
在API的验证配置中添加声明转换器,将内省响应中的自定义字段映射到ClaimsPrincipal:
.AddValidation(options => { options.SetIssuer("https://localhost:5001/"); options.AddAudiences("your-api-audience"); // 替换为你的API受众 options .UseIntrospection() .SetClientId("xxx") .SetClientSecret("xxx"); // 声明转换逻辑 options.AddClaimTransformer(context => { var response = context.IntrospectionResponse; if (response == null) return default; if (response.TryGetValue("employeeId", out var value)) { context.Principal.AddClaim(new Claim("employeeId", value.ToString()!)); } if (response.TryGetValue("username", out value)) { context.Principal.AddClaim(new Claim("username", value.ToString()!)); } if (response.TryGetValue("given_name", out value)) { context.Principal.AddClaim(new Claim(ClaimTypes.GivenName, value.ToString()!)); } if (response.TryGetValue("family_name", out value)) { context.Principal.AddClaim(new Claim(ClaimTypes.Surname, value.ToString()!)); } if (response.TryGetValue("office_location", out value)) { context.Principal.AddClaim(new Claim("office_location", value.ToString()!)); } if (response.TryGetValue("role", out value)) { context.Principal.AddClaim(new Claim(ClaimTypes.Role, value.ToString()!)); } return default; }); options.UseSystemNetHttp(); options.UseAspNetCore(); });
方案3:切换为本地JWT验证(推荐)
如果你的Access Token是JWT格式,可直接使用本地验证模式,API会自动解析JWT并提取所有声明,无需调用内省端点:
.AddValidation(options => { options.SetIssuer("https://localhost:5001/"); options.AddAudiences("your-api-audience"); // 使用本地验证(直接解析JWT) options.UseLocalValidation(); options.UseSystemNetHttp(); // 自动获取服务器密钥集用于签名验证 options.UseAspNetCore(); });
内容的提问来源于stack exchange,提问作者pellea
相关产品推荐
相关产品推荐

