支持 JWT 密钥轮换的 Bearer Token Authentication 的 Owin 中间件

Owin middleware for Bearer Token Authentication that supports JWT key rotation

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

Opend Id Connect spec 关于密钥轮换的说明如下:

Rotation of signing keys can be accomplished with the following approach. The signer publishes its keys in a JWK Set at its jwks_uri location and includes the kid of the signing key in the JOSE Header of each message to indicate to the verifier which key is to be used to validate the signature. Keys can be rolled over by periodically adding new keys to the JWK Set at the jwks_uri location. The signer can begin using a new key at its discretion and signals the change to the verifier using the kid value. The verifier knows to go back to the jwks_uri location to re-retrieve the keys when it sees an unfamiliar kid value.

关于这个主题,我能找到的最相似的问题是:

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

所以我想将客户端配置为下载丢失的 public JWK 密钥,只要它找到一个有效的、正确签名的 non-expired JWT 令牌,该令牌有一个未在本地缓存的孩子。

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

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

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

我使用 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;
    }
}

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