简体   繁体   English

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

[英]Owin middleware for Bearer Token Authentication that supports JWT key rotation

I am looking for some guidance with configuring owin middleware bearer token authentication to support Open Id Connect key rotation. 我正在寻找配置owin中间件承载令牌身份验证以支持Open Id Connect密钥轮换的一些指导。

The Opend Id Connect spec says the following about key rotation: Opend Id Connect规范说明了关于键旋转的以下内容:

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. 签名者在其jwks_uri位置的JWK集中发布其密钥,并在每个消息的JOSE标题中包括签名密钥的孩子,以向验证者指示将使用哪个密钥来验证签名。 Keys can be rolled over by periodically adding new keys to the JWK Set at the jwks_uri location. 可以通过在jwks_uri位置定期向JWK Set添加新密钥来覆盖密钥。 The signer can begin using a new key at its discretion and signals the change to the verifier using the kid value. 签名者可以自行决定开始使用新密钥,并使用kid值向验证者发出更改信号。 The verifier knows to go back to the jwks_uri location to re-retrieve the keys when it sees an unfamiliar kid value. 验证者知道返回到jwks_uri位置以在看到不熟悉的孩子值时重新检索密钥。

The most similar question I could find on this subject is this: SecurityTokenSignatureKeyNotFoundException in OWIN OpenID Connect middleware connecting to Google 我在这个问题上找到的最相似的问题是: OWIN中的SecurityTokenSignatureKeyNotFoundException OpenID连接到Google的连接中间件

The solution doesn't quite work as you will get errors between the time a new private key is issued and the time a client refreshes their cache of public keys. 解决方案不能正常工作,因为在发出新私钥和客户端刷新其公钥缓存之间会出现错误。

So I want to configure the client to download the missing public JWK key whenever it finds a valid, correctly signed, non-expired JWT token that has a kid that is not cache locally. 因此,我想配置客户端,以便在找到有效且正确签名的非过期JWT令牌时下载丢失的公共JWK密钥,该JWT令牌具有不在本地缓存的子节点。

I am current using IdentityServer3.AccessTokenValidation but the client does not download a new key when it recevies a token with a kid it doesn't recognise. 我目前正在使用IdentityServer3.AccessTokenValidation,但是当客户端收到一个无法识别的孩子的令牌时,客户端不会下载新密钥。

I have had a quick look at Microsoft.Owin.Security.Jwt -> UseJwtBearerAuthentication And also Microsoft.Owin.Security.OpenIdConnect -> UseOpenIdConnectAuthentication But I didn't get too far. 我快速浏览了一下Microsoft.Owin.Security.Jwt - > UseJwtBearerAuthentication以及Microsoft.Owin.Security.OpenIdConnect - > UseOpenIdConnectAuthentication但我没有太过分。

I'm looking for some direction to extend / configure any of the above packages to support the key rotation. 我正在寻找一些方向来扩展/配置任何上述软件包以支持密钥轮换。

I figured it out using the system.IdentityModel.Tokens.Jwt library. 我用system.IdentityModel.Tokens.Jwt库来计算它。 I had a lot of trouble with versioning so I've included the nuget packages that I ended up using. 我在版本控制方面遇到了很多麻烦,所以我已经包含了我最终使用的nuget包。 I had lots of issues with Microsoft.IdentityModel.Tokens.Jwt so I abandoned that approach. 我有很多问题与Microsoft.IdentityModel.Tokens.Jwt,所以我放弃了这种方法。 Anyway here are the packages: 无论如何这里是包:

<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" />

And here is the code. 这是代码。 The way it works is by setting a custom key resolver. 它的工作方式是设置自定义密钥解析器。 This key resolver gets called everytime a token is passed in. When we get a kid cache miss we make a new request to the Token Service to download the latest set of keys. 每次传入令牌时都会调用此密钥解析器。当我们得到子缓存未命中时,我们向令牌服务发出新请求以下载最新的密钥集。 Initially I thought of checking various parts of the key first (ie non expired / valid issuer) but then decided against this because if we cannot confirm that the token is signed correctly then adding those checks is pointless. 最初我想到首先检查密钥的各个部分(即非过期/有效发行者),但后来决定不这样做,因为如果我们无法确认令牌是否正确签名,那么添加这些检查毫无意义。 An attacker could set them to whatever they want. 攻击者可以将它们设置为他们想要的任何内容。

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;
    }
}

Some throttling around the getting of the keys would make sense to stop a malicious user forcing many roundtrips to the token service. 获取密钥的某些限制对于阻止恶意用户强制许多往返于令牌服务是有意义的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM