Skip to content
ecdsa.com

Verify a JWT with JWKS · .NET

How to verify a JWT with a JWKS in C#

To verify a JWT against a JWKS in C#, load the issuer's key set into Microsoft.IdentityModel's JsonWebKeySet and hand its signing keys to JsonWebTokenHandler.ValidateTokenAsync with TokenValidationParameters for issuer, audience and algorithm. The handler picks the key by kid. The run serves a real JWKS over HTTPS and checks five tokens.

Tested with Microsoft.IdentityModel.JsonWebTokens 8.23.0 (.NET SDK 10.0.402).

app.cs — JWKS keys, TokenValidationParameters, ValidateTokenAsync (a .NET 10 file-based app)
#:package Microsoft.IdentityModel.JsonWebTokens@8.23.0
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
// Fetch the issuer's key set once (cache it; refetch on a schedule or on an unknown kid)
using var http = new HttpClient();
var jwks = new JsonWebKeySet(await http.GetStringAsync("https://issuer.example.com/.well-known/jwks.json"));
var parameters = new TokenValidationParameters
{
ValidIssuer = "https://issuer.example.com",
ValidAudience = "api://orders",
IssuerSigningKeys = jwks.GetSigningKeys(), // the handler picks the one whose kid matches
ValidAlgorithms = ["ES256"], // never let the token choose the algorithm
RequireExpirationTime = true,
};
var handler = new JsonWebTokenHandler();
foreach (var line in File.ReadLines("tokens.txt"))
{
if (line.Split(' ') is not [var label, var token]) continue;
TokenValidationResult result = await handler.ValidateTokenAsync(token, parameters); // returns, does not throw
Console.WriteLine(result.IsValid
? $"{label}: accepted, sub={result.Claims["sub"]}"
: $"{label}: rejected: {result.Exception.GetType().Name}: {result.Exception.Message.Split('.')[0]}"); // "IDXnnnnn: …"
}

How it works

  1. new JsonWebKeySet(json) parses the JWKS; GetSigningKeys() turns each usable JWK into a SecurityKey with its KeyId.
  2. TokenValidationParameters lists what a valid token is: ValidIssuer, ValidAudience, IssuerSigningKeys, ValidAlgorithms and RequireExpirationTime.
  3. ValidateTokenAsync returns a TokenValidationResult instead of throwing; IsValid, Claims and Exception describe the outcome.
  4. The IDX codes identify the check that failed: IDX10214 audience, IDX10205 issuer, IDX10223 lifetime, IDX10503 no key for the kid.

Gotchas

  • In ASP.NET Core, prefer AddJwtBearer with options.Authority: it reads the issuer's discovery document, fetches the JWKS and refreshes it on rotation. Loading the JWKS by hand, as here, suits workers and tests, and then refreshing is your job.
  • Leaving ValidAudience unset does not skip the check, it fails (IDX10208: Unable to validate audience). ValidateAudience = false turns it off — and makes tokens for other APIs valid for yours.
  • Exception messages hide values as [PII of type … is hidden] unless IdentityModelEventSource.ShowPII is on; enable it only in development.
  • ValidAlgorithms is not set by default. Set it, so the accepted algorithm is part of your configuration rather than whatever the keys happen to support.

Related recipes