用于承载令牌认证的Owin中间件,支持JWT密钥轮换

我正在寻找配置owin中间件承载令牌身份validation以支持Open Id Connect密钥轮换的一些指导。

Opend Id Connect规范说明了关于键旋转的以下内容:

可以使用以下方法完成签名密钥的旋转。 签名者在其jwks_uri位置的JWK集中发布其密钥,并在每个消息的JOSE标题中包括签名密钥的孩子,以向validation者指示将使用哪个密钥来validation签名。 可以通过在jwks_uri位置定期向JWK Set添加新密钥来覆盖密钥。 签名者可以自行决定开始使用新密钥,并使用kid值向validation者发出更改信号。 validation者知道返回到jwks_uri位置以在看到不熟悉的孩子值时重新检索密钥。

我在这个主题上可以找到的最相似的问题是: OWIN中的SecurityTokenSignatureKeyNotFoundException连接到Google的OpenID Connect中间件

解决方案不能正常工作,因为在发出新私钥和客户端刷新其公钥缓存之间会出现错误。

因此,我想配置客户端,以便在找到有效的,正确签名的,未过期的JWT令牌时下载丢失的公共JWK密钥,该JWT令牌具有不在本地缓存的孩子。

我目前正在使用IdentityServer3.AccessTokenValidation,但客户端在收到一个无法识别的孩子的令牌时不会下载新密钥。

我快速浏览了一下Microsoft.Owin.Security.Jwt – > UseJwtBearerAuthentication以及Microsoft.Owin.Security.OpenIdConnect – > UseOpenIdConnectAuthentication但我没有太过分。

我正在寻找一些方向来扩展/配置任何上述软件包以支持密钥轮换。

我用system.IdentityModel.Tokens.Jwt库来计算它。 我在版本控制方面遇到了很多麻烦,所以我已经包含了我最终使用的nuget包。 我有很多问题与Microsoft.IdentityModel.Tokens.Jwt,所以我放弃了这种方法。 无论如何这里是包:

        

这是代码。 它的工作方式是设置自定义密钥解析器。 每次传入令牌时都会调用此密钥解析器。当我们得到孩子缓存未命中时,我们向令牌服务发出新请求以下载最新的密钥集。 最初我想到首先检查密钥的各个部分(即非过期/有效发行者),但后来决定不这样做,因为如果我们无法确认令牌是否正确签名,那么添加这些检查是没有意义的。 攻击者可以将它们设置为他们想要的任何内容。

 using Microsoft.IdentityModel.Protocols; using System; using System.Collections.Generic; using System.IdentityModel.Tokens; using System.Linq; using System.Net.Http; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; public class ValidationMiddleware { private readonly Func, Task> next; private readonly Func tokenAccessor; private readonly ConfigurationManager configurationManager; private readonly Object locker = new Object(); private Dictionary securityKeys = new Dictionary(); public ValidationMiddleware(Func, Task> next, Func tokenAccessor) { this.next = next; this.tokenAccessor = tokenAccessor; configurationManager = new ConfigurationManager( "url to open id connect token service", new HttpClient(new WebRequestHandler())) { // Refresh the keys once an hour AutomaticRefreshInterval = new TimeSpan(1, 0, 0) }; } public async Task Invoke(IDictionary environment) { var token = tokenAccessor(); var validationParameters = new TokenValidationParameters { ValidAudience = "my valid audience", ValidIssuer = "url to open id connect token service", ValidateLifetime = true, RequireSignedTokens = true, RequireExpirationTime = true, ValidateAudience = true, ValidateIssuer = true, IssuerSigningKeyResolver = MySigningKeyResolver, // Key resolver gets called for every token }; JwtSecurityTokenHandler.InboundClaimTypeMap.Clear(); var tokenHandler = new JwtSecurityTokenHandler(); var claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken); // Assign Claims Principal to the context. await next.Invoke(environment); } private SecurityKey MySigningKeyResolver(string token, SecurityToken securityToken, SecurityKeyIdentifier keyIdentifier, TokenValidationParameters validationParameters) { var kid = keyIdentifier.OfType().FirstOrDefault().Id; if (!securityKeys.TryGetValue(kid, out SecurityKey securityKey)) { lock (locker) { // Double lock check to ensure that only the first thread to hit the lock gets the latest keys. if (!securityKeys.TryGetValue(kid, out securityKey)) { // TODO - Add throttling around this so that an attacker can't force tonnes of page requests. // Microsoft's Async Helper var result = AsyncHelper.RunSync(async () => await configurationManager.GetConfigurationAsync()); var latestSecurityKeys = new Dictionary(); foreach (var key in result.JsonWebKeySet.Keys) { var rsa = RSA.Create(); rsa.ImportParameters(new RSAParameters { Exponent = Base64UrlEncoder.DecodeBytes(key.E), Modulus = Base64UrlEncoder.DecodeBytes(key.N), }); latestSecurityKeys.Add(key.Kid, new RsaSecurityKey(rsa)); if (kid == key.Kid) { securityKey = new RsaSecurityKey(rsa); } } // Explicitly state that this assignment needs to be atomic. Interlocked.Exchange(ref securityKeys, latestSecurityKeys); } } } return securityKey; } } 

获取密钥的一些限制将有助于阻止恶意用户强制许多往返于令牌服务。