升级OpenIddict 4.0.0后启用令牌加密时HotChocolate GraphQL授权失效
我有一个ASP.NET Core项目,同时托管了基于OpenIddict的身份服务器和基于HotChocolate GraphQL的资源服务器。系统启用了*客户端凭证(Client-credentials)*流,采用RSA算法加密令牌。之前使用OpenIddict v3.1.1时一切正常,升级到v4.0.0后授权功能失效。禁用令牌加密时功能恢复正常,但启用加密后,调试发现声明(Claims)完全无法传递。由于业务和安全要求不能关闭加密,且OpenIddict 3.x到4.x的迁移指南未提及加密密钥相关变更,同时v3.1.1已停止Bug修复支持,必须解决此问题。
ASP.NET Core管道中的OpenIddict配置
public static void AddOpenIddict(this IServiceCollection services, IConfiguration configuration) { var openIddictOptions = configuration.GetSection("OpenIddict").Get<OpenIddictOptions>(); var encryptionKeyData = openIddictOptions.EncryptionKey.RSA; var signingKeyData = openIddictOptions.SigningKey.RSA; var encryptionKey = RSA.Create(); var signingKey = RSA.Create(); encryptionKey.ImportFromEncryptedPem(encryptionKeyData.ToCharArray(), openIddictOptions.EncryptionKey.Passphrase.ToCharArray()); signingKey.ImportFromEncryptedPem(signingKeyData.ToCharArray(), openIddictOptions.SigningKey.Passphrase.ToCharArray()); encryptionKey.ImportFromEncryptedPem(encryptionKeyData.ToCharArray(), openIddictOptions.EncryptionKey.Passphrase.ToCharArray()); signingKey.ImportFromEncryptedPem(signingKeyData.ToCharArray(), openIddictOptions.SigningKey.Passphrase.ToCharArray()); var sk = new RsaSecurityKey(signingKey); var ek = new RsaSecurityKey(encryptionKey); services.AddOpenIddict() .AddCore(options => { options.UseEntityFrameworkCore() .UseDbContext<AuthDbContext>() .ReplaceDefaultEntities<Guid>(); }) .AddServer(options => { // 迁移指南提示:更新端点URI options.SetCryptographyEndpointUris("oauth2/.well-known/jwks"); options.SetConfigurationEndpointUris("oauth2/.well-known/openid-configuration"); options.SetTokenEndpointUris("oauth2/connect/token"); options.AllowClientCredentialsFlow(); options.SetUserinfoEndpointUris("oauth2/connect/userinfo"); options.SetIntrospectionEndpointUris("oauth2/connect/introspection"); options.AddSigningKey(sk); options.AddEncryptionKey(ek); //options.DisableAccessTokenEncryption(); // 取消注释此行后功能正常 options.UseAspNetCore(o => { // 注意:禁用是因为默认OpenIddict只接受HTTPS端点请求 o.DisableTransportSecurityRequirement(); o.EnableTokenEndpointPassthrough(); }); }) .AddValidation(options => { options.UseLocalServer(); options.UseAspNetCore(); }); }
授权控制器的令牌获取方法
[HttpPost("~/oauth2/connect/token")] [Produces("application/json")] public async Task<IActionResult> Exchange() { var request = HttpContext.GetOpenIddictServerRequest(); if (request.IsClientCredentialsGrantType()) { // 注意:客户端凭证已由OpenIddict自动验证: // 如果client_id或client_secret无效,此方法不会被调用 var application = await _applicationManager.FindByClientIdAsync(request.ClientId); if (application == null) { throw new InvalidOperationException("无法在数据库中找到应用程序详情。"); } // 创建包含声明的ClaimsIdentity,用于生成id_token、令牌或授权码 var identity = new ClaimsIdentity( authenticationType: TokenValidationParameters.DefaultAuthenticationType, nameType: OpenIddictConstants.Claims.Name, roleType: OpenIddictConstants.Claims.Role); var clientId = await _applicationManager.GetClientIdAsync(application); var organizationId = await _applicationManager.GetDisplayNameAsync(application); // 迁移指南提示:移除指定目标列表的AddClaims调用 identity.SetClaim(type: OpenIddictConstants.Claims.Subject, value: organizationId) .SetClaim(type: OpenIddictConstants.Claims.ClientId, value: clientId) .SetClaim(type: "organization_id", value: organizationId); identity.SetDestinations(static claim => claim.Type switch { _ => new[] { OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken } }); return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } throw new NotImplementedException("指定的授权类型未实现。"); }
授权失效的资源控制器
public class Query { private readonly IMapper _mapper; public Query(IMapper mapper) { _mapper = mapper; } [HotChocolate.AspNetCore.Authorization.Authorize] public async Task<Organization> GetMe(ClaimsPrincipal claimsPrincipal, [Service] IDbContextFactory<DbContext> dbContextFactory, CancellationToken ct) { var organizationId = Ulid.Parse(claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier)); ... // 省略后续代码 } } }
ASP.NET Core管道中的GraphQL配置
public static void AddGraphQL(this IServiceCollection services, IWebHostEnvironment webHostEnvironment) { services.AddGraphQLServer() .AddAuthorization() .AddQueryType<Query>() }
使用的包及版本
- OpenIddict (4.0.0)
- OpenIddict.AspNetCore (4.0.0)
- OpenIddict.EntityFrameworkCore (4.0.0)
- HotChocolate.AspNetCore (12.13.2)
- HotChocolate.AspNetCore.Authorization (12.13.2)
- HotChocolate.Diagnostics (12.13.2)
补充:日志捕获的异常信息
Microsoft.IdentityModel.Tokens.SecurityTokenDecryptionFailedException: IDX10603: 解密失败。尝试的密钥: 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey, KeyId: '', InternalId: 'nu9-WIVbfvvJafm3g4th-uHPFDf8eoyJf-M1ByrotW8'.
'.
捕获的异常:
'System.ArgumentNullException: IDX10000: 参数 'ciphertext' 不能为 'null' 或空对象。 (参数 'ciphertext')
在 Microsoft.IdentityModel.Tokens.AuthenticatedEncryptionProvider.Decrypt(Byte[] ciphertext, Byte[] authenticatedData, Byte[] iv, Byte[] authenticationTag)
在 Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities.DecryptToken(CryptoProviderFactory cryptoProviderFactory, SecurityKey key, String encAlg, Byte[] ciphertext, Byte[] headerAscii, Byte[] initializationVector, Byte[] authenticationTag)
在 Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities.DecryptJwtToken(SecurityToken securityToken, TokenValidationParameters validationParameters, JwtTokenDecryptionParameters decryptionParameters
ASP.NET Core管道中的令牌验证参数配置
services.AddAuthentication() .AddJwtBearer(options => { var openIddictOptions = Configuration.GetSection("OpenIddict").Get<OpenIddictOptions>(); var encryptionKeyData = openIddictOptions.EncryptionKey.RSA; var signingKeyData = openIddictOptions.SigningKey.RSA; var encryptionKey = RSA.Create(); var signingKey = RSA.Create(); encryptionKey.ImportFromEncryptedPem(encryptionKeyData.ToCharArray(), openIddictOptions.EncryptionKey.Passphrase.ToCharArray()); signingKey.ImportFromEncryptedPem(signingKeyData.ToCharArray(), openIddictOptions.SigningKey.Passphrase.ToCharArray()); encryptionKey.ImportFromEncryptedPem(encryptionKeyData.ToCharArray(), openIddictOptions.EncryptionKey.Passphrase.ToCharArray()); signingKey.ImportFromEncryptedPem(signingKeyData.ToCharArray(), openIddictOptions.SigningKey.Passphrase.ToCharArray()); var sk = new RsaSecurityKey(signingKey); var ek = new RsaSecurityKey(encryptionKey); options.IncludeErrorDetails = true; options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { IssuerSigningKey = sk, TokenDecryptionKey = ek, RequireExpirationTime = false, ValidateLifetime = true, ValidateAudience = false, ValidateIssuer = false }; });
内容的提问来源于stack exchange,提问作者Sk Shahnawaz-ul Haque

