'How do I validate a JWT using JwtSecurityTokenHandler and a JWKS endpoint?

I am prototyping the use of IdentityServer4 to secure several services, with the caveat that those services will likely not be migrated (in the forseeable future) to use the OWIN middleware idiom of ASP.NET Core. Consequently, I can not leverage the many middleware helpers that automate the validation of a JWT by simply providing the well-known JWKS endpoint of IdentityServer, among other things.

It would be nice if I could reconstruct this behavior, and I'd like to leverage Microsoft's JwtSecurityTokenHandler implementation if possible. However, I can not figure out how to utilize the JsonWebKeySet and JsonWebKey types provided via IdentityServer's discovery endpoint to extract keys and perform the validation.

JwtSecurityTokenHandler uses TokenValidationParameters to validate a JWT, and those parameters require an instance of one or more SecurityKey objects to perform the validation.

ClaimsPrincipal ValidateJwt(string token, IdentityModel.Client.DiscoveryResponse discovery)
{
    JwtSecurityToken jwt = new JwtSecurityToken(token);

    TokenValidationParameters validationParameters = new TokenValidationParameters
    {
        ValidateAudience = true,
        ValidateIssuer = true,
        RequireSignedTokens = true,
        ValidIssuer = "expected-issuer",
        ValidAudience = "expected-audience",
        IssuerSigningKeys = discovery.KeySet.Keys /* not quite */
    };

    JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
    SecurityToken validatedToken;
    return handler.ValidateToken(jwt, validationParameters, out validatedToken);
}

How do I perform the necessary translation from JsonWebKeySet to IEnumerable<SecurityKey> so that the validation can occur? Is there another method (apart from OWIN middleware) that will also work using the DiscoveryResponse data above?

(Sadly, the documentation for System.IdentityModel.Tokens.Jwt is not up to date.)



Solution 1:[1]

Check this sample:

https://github.com/IdentityServer/IdentityServer4/blob/master/samples/Clients/old/MvcManual/Controllers/HomeController.cs#L148

It manually retrieves the key from the JWK and populates the validation parameters.

Solution 2:[2]

var jwks = "{ keys: [..." // your jwks json string
var signingKeys = new JsonWebKeySet(jwks).GetSigningKeys();

Then simply assign it to the IssuerSigningKeys property of your TokenValidationParameters.

If you are reading the jwks from a web service, then you would need a http client to read it first.

Solution 3:[3]

Using code from the packages:

  • IdentityModel
  • System.IdentityModel.Tokens.Jwt

Imports:

open IdentityModel
open IdentityModel.Client
open System.Security.Cryptography
open System.IdentityModel.Tokens.Jwt
open Microsoft.IdentityModel.Tokens

Conversion from JsonWebKey to SecurityKey:

module JsonWebKey =

  let toSecurityKey (webKey : Jwk.JsonWebKey) =
    let e = Base64Url.Decode(webKey.E)
    let n = Base64Url.Decode(webKey.N)

    let mutable rsap = RSAParameters()

    rsap.Exponent <- e
    rsap.Modulus <- n

    let key = RsaSecurityKey(rsap)

    key.KeyId <- webKey.Kid

    key :> SecurityKey

Then to validate a token:

task {
  let cache = new DiscoveryCache(oidConfigUrl)

  let! ddr = cache.GetAsync()

  let tokenHandler = JwtSecurityTokenHandler()

  let validationParameters = TokenValidationParameters()

  let issuerSigningKeys =
    ddr.KeySet.Keys
    |> Seq.map JsonWebKey.toSecurityKey
    |> Seq.toList

  validationParameters.IssuerSigningKeys <- issuerSigningKeys
  validationParameters.ValidIssuer <- ddr.Issuer
  validationParameters.ValidAudience <- "account"
  validationParameters.RequireSignedTokens <- true
  validationParameters.ValidateIssuer <- true
  validationParameters.ValidateLifetime <- true
  validationParameters.ValidateAudience <- true

  let! validationResult = 
    tokenHandler.ValidateTokenAsync(accessToken, validationParameters)

  printfn $"IsValid: {validationResult.IsValid}"
  printfn $"Exception: {validationResult.Exception}"
  printfn $"Claims: {validationResult.Claims |> Seq.toList}"
  printfn $"ClaimsIdentity: {validationResult.ClaimsIdentity.ToString()}"
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 David Peden
Solution 2 Daniel B
Solution 3