I am looking for some guidance with configuring owin middleware bearer token authentication to support Open Id Connect key rotation.
The Opend Id Connect spec says the following about key rotation:
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.
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.
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.
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.
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.
JSON Web Token (JWT, RFC 7519) is a way to encode claims in a JSON document that is then signed. JWTs can be used as OAuth 2.0 Bearer Tokens to encode all relevant parts of an access token into the access token itself instead of having to store them in a database.
In essence, a JSON Web Token (JWT) is a bearer token. It's a particular implementation which has been specified and standardised. JWT in particular uses cryptography to encode a timestamp and some other parameters. This way, you can check if it's valid by just decrypting it, without hitting a DB.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With