On this page
JWT Validation Guide
When you use Okta to get OAuth 2.0 or OpenID Connect tokens for a user, the response contains a signed JWT (id_token
and/or access_token
).
If you are writing low-level code that retrieves or uses these tokens, it's important to validate the tokens before you trust them. This guide shows you how to validate tokens manually.
Note: This guide is specific to .NET and C#. If you need general information, read Validate Access Tokens and Validate ID Tokens instead.
Who should use this guide
You don't need to validate tokens manually if:
- You are using ASP.NET or ASP.NET Core
- You send the tokens to Okta to be validated (this is called token introspection)
If you need to validate a token manually, and don't want to make a network call to Okta, this guide helps you validate tokens locally.
What you need
- Your authorization server URL (see Composing your base URL)
- A token (JWT string)
- Libraries for retrieving the signing keys and validating the token
This guide uses the official Microsoft OpenID Connect and JWT libraries, but you can adapt it to other key and token parsing libraries.
Get the signing keys
Okta signs JWTs using asymmetric encryption (RS256) (opens new window), and publishes the public signing keys in a JSON Web Key Set (JWKS) as part of the OAuth 2.0 and OpenID Connect discovery documents. The signing keys are rotated on a regular basis. The first step to verify a signed JWT is to retrieve the current signing keys.
The OpenIdConnectConfigurationRetriever
class in the Microsoft.IdentityModel.Protocols.OpenIdConnect (opens new window) package downloads and parses the discovery document to get the key set. You can use it in conjunction with the ConfigurationManager
class that handles caching the response and refreshing it regularly:
// Replace with your authorization server URL:
var issuer = "https://${yourOktaDomain}/oauth2/default";
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
issuer + "/.well-known/oauth-authorization-server",
new OpenIdConnectConfigurationRetriever(),
new HttpDocumentRetriever());
After you instantiate the configurationManager
, keep it around as a singleton. You only need to set it up once.
Validate a token
The JwtSecurityTokenHandler
class in the System.IdentityModel.Tokens.Jwt (opens new window) package handles the low-level details of validating a JWT.
You can write a method that takes the token, the issuer, and the configurationManager
that you create. The method is async
because the configurationManager
may need to make an HTTP call to get the signing keys (if they aren't already cached):
private static async Task<JwtSecurityToken> ValidateToken(
string token,
string issuer,
IConfigurationManager<OpenIdConnectConfiguration> configurationManager,
CancellationToken ct = default(CancellationToken))
{
if (string.IsNullOrEmpty(token)) throw new ArgumentNullException(nameof(token));
if (string.IsNullOrEmpty(issuer)) throw new ArgumentNullException(nameof(issuer));
var discoveryDocument = await configurationManager.GetConfigurationAsync(ct);
var signingKeys = discoveryDocument.SigningKeys;
var validationParameters = new TokenValidationParameters
{
RequireExpirationTime = true,
RequireSignedTokens = true,
ValidateIssuer = true,
ValidIssuer = issuer,
ValidateIssuerSigningKey = true,
IssuerSigningKeys = signingKeys,
ValidateLifetime = true,
// Allow for some drift in server time
// (a lower value is better; we recommend two minutes or less)
ClockSkew = TimeSpan.FromMinutes(2),
// See additional validation for aud below
};
try
{
var principal = new JwtSecurityTokenHandler()
.ValidateToken(token, validationParameters, out var rawValidatedToken);
return (JwtSecurityToken)rawValidatedToken;
}
catch (SecurityTokenValidationException)
{
// Logging, etc.
return null;
}
}
To use the method, pass it a token, and the issuer and configurationManager
that you declared earlier:
var accessToken = "eyJh...";
var validatedToken = await ValidateToken(accessToken, issuer, configurationManager);
if (validatedToken == null)
{
Console.WriteLine("Invalid token");
}
else
{
// Additional validation...
Console.WriteLine("Token is valid!");
}
This method returns an instance of JwtSecurityToken
if the token is valid, or null
if it is invalid. Returning JwtSecurityToken
makes it possible to retrieve claims from the token later.
Depending on your application, you could change this method to return a boolean, log specific exceptions like SecurityTokenExpiredException
with a message, or handle validation failures in some other way.
Additional validation for access tokens
If you are validating access tokens, you should verify that the aud
(audience) claim equals the audience that is configured for your authorization server in the Admin Console.
For example, if your authorization server audience is set to MyAwesomeApi
, add this to the validation parameters:
ValidateAudience = true,
ValidAudience = "MyAwesomeApi",
You also must verify that the alg
claim matches the expected algorithm that was used to sign the token. You have to perform this check after the ValidateToken
method returns a validated token:
// Validate alg
var validatedToken = await ValidateToken(accessToken, issuer, configurationManager);
var expectedAlg = SecurityAlgorithms.RsaSha256; //Okta uses RS256
if (validatedToken.Header?.Alg == null || validatedToken.Header?.Alg != expectedAlg)
{
throw new SecurityTokenValidationException("The alg must be RS256.");
}
Additional validation for ID tokens
When validating an ID token, you should verify that the aud
(Audience) claim equals the Client ID of the current application.
Add this to the validation parameters:
ValidateAudience = true,
ValidAudience = "xyz123", // This Application's Client ID
You also must verify that the alg
claim matches the expected algorithm that was used to sign the token. You have to perform this check after the ValidateToken
method returns a validated token:
// Validate alg
var validatedToken = await ValidateToken(idToken, issuer, configurationManager);
var expectedAlg = SecurityAlgorithms.RsaSha256; //Okta uses RS256
if (validatedToken.Header?.Alg == null || validatedToken.Header?.Alg != expectedAlg)
{
throw new SecurityTokenValidationException("The alg must be RS256.");
}
If you specified a nonce during the initial code exchange when your application retrieved the ID token, you should verify that the nonce matches:
var validatedToken = await ValidateToken(idToken, issuer, configurationManager);
// Validate nonce
var expectedNonce = "foobar"; // Retrieve this from a saved cookie or other mechanism
var nonceMatches = validatedToken.Payload.TryGetValue("nonce", out var rawNonce)
&& rawNonce.ToString() == expectedNonce;
if (!nonceMatches)
{
throw new SecurityTokenValidationException("The nonce was invalid.");
}
Decode token claims
The sample ValidateToken
method above both validates a token and decodes its claims. You can use the returned JwtSecurityToken
object to inspect the claims in the token.
For example, you can get the sub
(Subject) claim with the Subject
property:
Console.WriteLine($"Token subject: {validatedToken.Subject}");
You can access more claims with the Payload
property or loop over the entire Claims
collection:
Console.WriteLine("All claims:");
foreach (var claim in validatedToken.Claims)
{
Console.WriteLine($"{claim.Type}\t{claim.Value}");
}
Conclusion
This guide provides the basic steps required to locally verify an access or ID token signed by Okta. It uses packages from Microsoft for key parsing and token validation, but the general principles should apply to any JWT validation library.