JSON Web Tokens (JWTs) have become very popular. Many developers use them in their applications for authentication and authorization purposes, but not all of them really know why and how to validate their JWTs.
This article will explain why you need to validate a JWT, what it means in general, and give you an overview of the many ways you can do it in the .NET platform.
What Are JWTs?
If you landed on this article, you probably know what a JSON Web Token is. If not, here is a great introduction to JWTs and a tool to decode them.
Simply put, JSON Web Token is an open standard that defines how to share information between parties using the JSON format. The JWT structure consists of three concatenated Base64 url-encoded strings, separated by dots: the header, the payload, and the signature The information carried by a JWT is digitally signed so you can verify its origin and trust it. The digital signature also guarantees the integrity of the information, but it doesn't guarantee privacy unless explicitly encrypted.
Besides this, the JWT specification doesn't define the specific purpose of a token. While most developers think of authentication when dealing with JWTs, this is not an intrinsic property of tokens in the JWT format. JWT is just a format. The meaning of the token depends on the specific context or application.
For example, in the OpenID Connect (OIDC) context, the JWT format is used for the ID token, which is the token that proves a user is authenticated. In the OAuth 2.0 context, the JWT format can optionally be used for the access token, which is the token that allows a client application to access a specific resource on behalf of the user.
To learn more about ID and access tokens and their differences, you can read this blog post or watch this video instead.
Interested in getting up-to-speed with JWTs as soon as possible?
Download the free ebookWhat Does Token Validation Mean?
The main purpose of a JWT is to carry information that the recipient can trust. So, the first thing your application needs to do when it receives a JWT is to ensure that the information it carries is trustworthy. This is where validation comes in.
In short, validating a JSON Web Token means ensuring that the token has the structure defined in the standard specification and that you can trust the information it carries.
The JWT validation is based on the following five criteria:
Token structure. The first check is about the token's structure. Does the token match the structure of a JSON Web Token? If the token doesn't follow the standard guidelines, it's not valid.
- Token integrity. The next check is for the token's integrity. Has the token been tampered with? You can verify this by checking the signature. If you verify that the token's signature is valid, then it has not been altered in any way.
- Token expiration. JWTs have an expiration time defined in the
exp
claim. Is the token expired when it has been received? If a token has expired, you should no longer trust its contents. - Expected authority. The next check is to verify that the token was issued and signed by the expected sender (authority). There are two steps to this check:
- Verify that the authority is the one expected by your application by comparing it to the
iss
claim (issuer). - Verify that the key used to sign the JWT actually belongs to the expected authority.
- Verify that the authority is the one expected by your application by comparing it to the
- Expected audience. Finally, verify that the token is intended for your application. In other words, you should check that the
aud
claim matches with the value that identifies your application. For example, in an ID token, the value for the audience is the client ID; in an access token in JWT format, the value for the audience is the API identifier.
To learn more about digital signatures, read this article. Check out this article for the differences between the signing algorithms used in a JWT.
Token validation doesn't involve the custom information carried by the token. This is an application-specific logic, which is out of the scope of token validation. For example, validating an ID token does not include verifying that the user's email address in the email
claim exists.
Validate a JWT Using an Auth0 SDK
Now that you know what validating a JWT means, you are ready to learn how to validate your tokens in .NET.
Before jumping into the validation code, you should evaluate whether you really need to explicitly validate the JWTs you receive from an issuer like Auth0. Depending on the specific application, you may want to use an Auth0 SDK, which will take care of all the validation steps for you. For example, if you are developing an ASP.NET Core MVC application or a Blazor Server application, you can use the Auth0 ASP.NET Core Authentication SDK. This SDK will take care of the entire process of obtaining the JWT tokens for your application, including token validation. Check out this article for an overview of this specific SDK.
So, if your application can benefit from an SDK, the best move is to write less code and get the JWT validation for free. Take a look at this article to learn which Auth0 SDK is right for your .NET application type.
Using the ASP.NET Core Middleware
If, for some reason, you can't use an Auth0 SDK, you can configure your ASP.NET Core application by using two middleware:
- The OpenIdConnect middleware, which allows your ASP.NET Core web applications to authenticate users and get ID and access tokens via OpenId Connect.
- The JwtBearer middleware, which enables your ASP.NET Core web API to handle access tokens.
Both middleware provide you with a convenient way to obtain and validate JWT tokens. The following code snippet shows an example of how to configure OpenIdConnect middleware:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("Auth0", options => {
options.Authority = $"https://{builder.Configuration["Auth0:Domain"]}";
options.ClientId = builder.Configuration["Auth0:ClientId"];
options.CallbackPath = new PathString("/callback");
});
// ...other code...
The next example shows how to use the JwtBearer middleware:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = $"https://{builder.Configuration["Auth0:Domain"]}";
options.Audience = builder.Configuration["Auth0:Audience"];
});
// ...other code...
In both cases, by configuring the Authority
option, you implicitly ask the middleware to validate your tokens. The middleware will use this value to retrieve the issuer's signing key, which is needed to validate the token's signature. All the other required validation steps are performed automatically. There is nothing else you need to do.
Simplified configuration for the JwtBearer middleware
Starting with .NET 7, you can use the JwtBearer middleware in a simplified way. You define your validation values in the appsettings.json
file, as in the following example:
// appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Authentication": {
"Schemes": {
"Bearer": {
"Authority": "https://{DOMAIN}",
"ValidAudiences": [ "{AUDIENCE}" ],
"ValidIssuer": "{DOMAIN}"
}
}
}
}
Then you can simply add the middleware to your ASP.NET Core application, as follows:
// Program.cs
// ...existing code...
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddJwtBearer();
// ...existing code...
The behavior is exactly the same as before but with much less code.
Custom validation with the ASP.NET Core middleware
If you have specific needs, you can customize the JWT validation using the TokenValidationParameters
option. For example, if you already have the issuer's public key stored on your file system, you can configure your middleware as shown in the following example:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Audience = builder.Configuration["Auth0:Audience"];
options.TokenValidationParameters =
new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = $"{builder.Configuration["Auth0:Domain"]}",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new X509SecurityKey(new X509Certificate2(certificateFilePath)),
ValidateLifetime = true
};
});
// ...other code...
You can see that the Audience
option is still there while the Authority
option is missing. Instead of the Authority
option, you find the TokenValidationParameters
option, which is assigned an instance of the TokenValidationParameters
class. The properties of this instance define what to check in order to complete the JWT validation. Specifically, you ask to validate the issuer (ValidateIssuer
) and provide the value to match with (ValidIssuer
). You ask to validate the issuer's signature (ValidateIssuerSigningKey
) and provide the issuer's public signing key (IssuerSigningKey
) embedded in a certificate stored in the file system. And finally, you ask to check the expiration time (ValidateLifetime
).
You can see how removing the Authority
option from the middleware configuration forces you to manually specify what to check in order to validate the token.
Validate a JWT “By Hand”
In case you can't use the ASP.NET Core middleware, for example, when you are not developing a web application, don't get desperate. .NET gives you what you need, although this requires some effort on your side.
Validate your token
First, you need to install the Microsoft.IdentityModel.Tokens
package from NuGet:
dotnet add package Microsoft.IdentityModel.Tokens
This package provides everything you need for token manipulation, including validation, of course.
Have a look at the following function:
public bool ValidateToken(
string token,
string issuer,
string audience,
ICollection<SecurityKey> signingKeys,
out JwtSecurityToken jwt
)
{
var validationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidIssuer = issuer,
ValidateAudience = true,
ValidAudience = audience
ValidateIssuerSigningKey = true,
IssuerSigningKeys = signingKeys,
ValidateLifetime = true
};
try
{
var tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
jwt = (JwtSecurityToken)validatedToken;
return true;
} catch (SecurityTokenValidationException ex) {
// Log the reason why the token is not valid
return false
}
}
This function takes a token and validates it by using the other parameters: the issuer, the audience, and the signing keys. If the token is valid, it returns true
and provides a JwtSecurityToken
instance representing the token as the jwt
output parameter.
Once again, you see the TokenValidationParameters
class, which allows you to define the validation criteria. This time there is no implicit validation. You need to specify all the required validation criteria and the valid values. Then you create a token handler (JwtSecurityTokenHandler
) and use it to validate the token against the validation parameters. If the validation succeeds, you get an object representing the token, which you assign to the output parameter after casting to JwtSecurityToken
. Finally, the function returns true
.
If the validation fails, it should log the reason for the failed validation and return false
.
Get the signing keys
The ValidateToken()
function expects you to pass the basic information for the token validation: the token issuer identifier, the audience, and the issuer's signing keys. You may have all this information on hand or need to retrieve some of it. For example, you can obtain the signing key of the issuer online. According to the OIDC standard, you can find the set of keys (JSON Web Key Sets) to verify the issuer's signatures at a public and well-defined URL: https://{DOMAIN}/.well-known/openid-configuration
.
Read this document to learn more about the JSON Web Key Sets.
To validate a JWT using the ValidateToken()
function, you must first retrieve the signing key from this URL. As you can see, things start to get complex. This is why I suggest using an Auth0 SDK in the first place, or at least the ASP.NET Core middleware, if possible.
However, if using one of these tools is not feasible in your specific case, here is what you need to do to get the issuer's signing keys.
First, you need to install another package from NuGet: the Microsoft.IdentityModel.Protocols.OpenIdConnect
package:
dotnet add package Microsoft.IdentityModel.Protocols.OpenIdConnect
This package provides you with the ConfigurationManager
class, which helps you retrieve the JSON Web Key Sets. The following code shows you how to obtain the signing key of your JWT issuer:
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
$"https://{builder.Configuration["Auth0:Domain"]/.well-known/openid-configuration}",
new OpenIdConnectConfigurationRetriever(),
new HttpDocumentRetriever());
var discoveryDocument = await configurationManager.GetConfigurationAsync();
var signingKeys = discoveryDocument.SigningKeys;
Once you have the signing keys, you are ready to use the ValidateToken()
function.
Recap
Throughout this article, you have learned what JWT validation is and why you need to do it. You also learned that as a .NET developer, you have many options for validating the JWTs your application receives: from a zero-code approach to an almost manual validation.
With the Auth0 SDKs, you get the JWT validation for free. No specific code is required. The SDKs validate tokens for you.
Using the ASP.NET Core middleware, OpenIdConnect and JwtBearer, you get implicit token validation in the default cases. However, you can customize your validation criteria if you are dealing with special cases.
In case you need full control over the validation process, you can rely on the Microsoft.IdentityModel.Tokens
package for the JWT manipulation and validation and on the Microsoft.IdentityModel.Protocols.OpenIdConnect
package for retrieving the issuer's signing keys.
Of course, the suggestion is to delegate as much as possible to the SDKs and middleware because the less code you write, the fewer bugs you may create 🙂.