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

Posted

技术标签:

【中文标题】支持 JWT 密钥轮换的承载令牌认证的 Owin 中间件【英文标题】:Owin middleware for Bearer Token Authentication that supports JWT key rotation 【发布时间】:2018-04-12 21:48:33 【问题描述】:

我正在寻找有关配置 owin 中间件不记名令牌身份验证以支持 Open Id Connect 密钥轮换的指导。

Opend Id Connect spec 说以下关于密钥轮换的内容:

可以通过以下方法完成签名密钥的轮换。签名者在其 jwks_uri 位置的 JWK 集中发布其密钥,并将签名密钥的孩子包含在每条消息的 JOSE 标头中,以向验证者指示要使用哪个密钥来验证签名。可以通过定期将新密钥添加到 jwks_uri 位置的 JWK 集中来滚动密钥。签名者可以自行决定开始使用新密钥,并使用孩子值向验证者发出更改信号。验证者在看到不熟悉的孩子值时知道返回 jwks_uri 位置重新检索密钥。

我能找到的关于这个主题的最相似的问题是: SecurityTokenSignatureKeyNotFoundException in OWIN OpenID Connect middleware connecting to Google

该解决方案不太奏效,因为在发布新私钥和客户端刷新其公钥缓存之间会出现错误。

因此,我想将客户端配置为在找到有效、正确签名、未过期的 JWT 令牌时下载缺少的公共 JWK 密钥,该令牌有一个未在本地缓存的孩子。

我目前正在使用IdentityServer3.AccessTokenValidation,但是当客户端收到一个它不认识的孩子的令牌时,它没有下载新密钥。

我快速浏览了 Microsoft.Owin.Security.Jwt -> UseJwtBearerAuthentication 还有 Microsoft.Owin.Security.OpenIdConnect -> UseOpenIdConnectAuthentication 但我并没有走得太远。

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

【问题讨论】:

【参考方案1】:

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

<package id="Microsoft.IdentityModel.Protocol.Extensions" version="1.0.2.206221351" targetFramework="net462" />
<package id="Microsoft.Win32.Primitives" version="4.0.1" targetFramework="net462" />
<package id="System.IdentityModel.Tokens.Jwt" version="4.0.2.206221351" targetFramework="net462" />
<package id="System.Net.Http" version="4.1.0" targetFramework="net462" />
<package id="System.Security.Cryptography.Algorithms" version="4.2.0" targetFramework="net462" />
<package id="System.Security.Cryptography.Encoding" version="4.0.0" targetFramework="net462" />
<package id="System.Security.Cryptography.Primitives" version="4.0.0" targetFramework="net462" />
<package id="System.Security.Cryptography.X509Certificates" version="4.1.0" targetFramework="net462" />

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

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<IDictionary<string, object>, Task> next;
    private readonly Func<string> tokenAccessor;
    private readonly ConfigurationManager<OpenIdConnectConfiguration> configurationManager;

    private readonly Object locker = new Object();
    private Dictionary<string, SecurityKey> securityKeys = new Dictionary<string, SecurityKey>();

    public ValidationMiddleware(Func<IDictionary<string, object>, Task> next, Func<string> tokenAccessor)
    
        this.next = next;
        this.tokenAccessor = tokenAccessor;

        configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
            "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<string, object> 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<NamedKeySecurityKeyIdentifierClause>().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<string, SecurityKey>();
                    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;
    

对获取密钥进行一些限制对于阻止恶意用户强制多次往返令牌服务是有意义的。

【讨论】:

您应该使用自定义身份验证过滤器而不是这个。 非常有趣的答案!我希望我可以多次投票。 @Zimano:你能解释一下如何/为什么这是一个更好的方法吗? 我很抱歉,我的评论是非常仓促的,是工作中非常忙碌的发布的一部分。我想我的意思是您可以将这种行为封装在身份验证过滤器属性中,例如[JwtRotationAuth]。这样,您可以通过简单地在 API 方法等上方添加属性来保护具有该 Authentication 过滤器属性的方法,而不必专门引用和管理 ValidationMiddleware 类的实例。

以上是关于支持 JWT 密钥轮换的承载令牌认证的 Owin 中间件的主要内容,如果未能解决你的问题,请参考以下文章

Auth0 - 在 Owin 上使用带有承载访问令牌的 JWT 使用 RS256 进行身份验证

Bearer Token 认证和 JWT

zuul:不接受承载认证

修改 OWIN OAuth 中间件以使用 JWT 不记名令牌

Owin 身份令牌认证令牌端点以 404 响应

Spring Cloud Security JWT:使用配置服务器/密钥轮换分发公钥