用于支持JWT密钥轮换的Bearer令牌认证的Owin中间件 [英] Owin middleware for Bearer Token Authentication that supports JWT key rotation

查看:125
本文介绍了用于支持JWT密钥轮换的Bearer令牌认证的Owin中间件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在寻找有关配置owin中间件承载令牌身份验证以支持Open Id Connect密钥轮换的指南.

I am looking for some guidance with configuring owin middleware bearer token authentication to support Open Id Connect key rotation.

Opend Id Connect规范说以下内容:按键旋转:

The Opend Id Connect spec says the following about key rotation:

签名密钥的旋转可以通过以下方法来完成.签名者将其密钥发布到位于其jwks_uri位置的JWK集中,并将签名密钥的孩子包含在每个消息的JOSE标头中,以向验证者指示要使用哪个密钥来验证签名.可以通过在jwks_uri位置上的JWK Set中定期添加新密钥来滚动密钥.签名者可以根据自己的判断开始使用新密钥,并使用kid值将更改发送给验证者.验证者知道看到不熟悉的孩子值时,会知道回到jwks_uri位置以重新获取密钥.

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.

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

The most similar question I could find on this subject is this: SecurityTokenSignatureKeyNotFoundException in OWIN OpenID Connect middleware connecting to 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.

因此,我希望将客户端配置为在发现有效的,正确签名的,未过期的JWT令牌(其子对象不在本地缓存)时下载丢失的公共JWK密钥.

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.

我当前正在使用 IdentityServer3.AccessTokenValidation ,但客户端在以下情况下不会下载新密钥它会与一个不认识的孩子一起收回令牌.

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.

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

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.

我正在寻找扩展/配置上述任何软件包以支持按键旋转的方向.

I'm looking for some direction to extend / configure any of the above packages to support the key rotation.

推荐答案

我使用system.IdentityModel.Tokens.Jwt库弄清楚了. 我在版本控制方面遇到很多麻烦,因此我加入了最终使用的nuget软件包. 我在使用Microsoft.IdentityModel.Tokens.Jwt时遇到很多问题,因此放弃了这种方法.无论如何,这里是软件包:

I figured it out using the system.IdentityModel.Tokens.Jwt library. I had a lot of trouble with versioning so I've included the nuget packages that I ended up using. I had lots of issues with Microsoft.IdentityModel.Tokens.Jwt so I abandoned that approach. 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 (i.e. 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.

这篇关于用于支持JWT密钥轮换的Bearer令牌认证的Owin中间件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆