Updated on
To decode a JWT in .NET, we pass the token string to JsonWebTokenHandler.ReadJsonWebToken() from the Microsoft.IdentityModel.JsonWebTokens package and read the header and claims from the JsonWebToken it returns.
Decoding checks nothing, so we trust none of what we read until the token has been validated.
JSON Web Tokens (JWTs) are a popular way to transmit information between parties in a compact and verifiable manner. They are a proven way of implementing Bearer Token Authentication and securing APIs.
Let’s begin.
What Is Inside a JWT Bearer Token?
A signed JWT bearer token is three Base64url-encoded parts joined by dots: a header, a payload and a signature.
The header is a small JSON object that says how the token was signed. It carries the signing algorithm (alg), usually the token type (typ) and often the id of the signing key (kid).
The payload is another JSON object, and it holds the claims. These are statements about who issued the token (iss), who it is for (aud), when it expires (exp), and anything else the issuer chose to add, such as scopes or a user id (sub).
The signature is computed over the encoded header and payload with the issuer’s key. It lets an API prove that nobody changed the first two parts.
Base64url is only an encoding, so anyone holding the token can read the header and the payload. Not every bearer token is a JWT, though. Some identity servers issue opaque reference tokens, and those have nothing inside to decode.
Let’s look at a real access token, issued by the Duende demo identity server to a machine client:
eyJhbGciOiJSUzI1NiIsImtpZCI6IjlBNkIyOTEwRDYyQ0UwMzc5RjNCREI1MjBBNTc1RTIyIiwidHlwIjoiYXQrand0In0.eyJpc3MiOiJodHRwczovL2RlbW8uZHVlbmRlc29mdHdhcmUuY29tIiwibmJmIjoxNzAwNjU0MjQ0LCJpYXQiOjE3MDA2NTQyNDQsImV4cCI6MTcwMDY1Nzg0NCwiYXVkIjoiYXBpIiwic2NvcGUiOlsiYXBpIl0sImNsaWVudF9pZCI6Im0ybSIsImp0aSI6IkUwRjYyQTA1MjM1RDY4MzdFNjcwNzExRTY1NDFDMjlEIn0.pFpzXr3jMKBYpB1JC6bq04xMyJ5gsWCq45TjO2SH44Ai6Fnic-5hn_IAYGaRPTu8dqwZjH0aBZd2UEmpvQs3WgWUANHbqG7fOAmaQQ7z3T8RRBWj5kpD6tlXmsACJAVZZl4Yra8cvrf0gacC9UHQtX9WRF51y7NeG2ZWIPq5OB4jzKvObDmcoujQgjaRRX-j7pMcpAWGxG_VMjVR9kqeOlhsfOeg3K8STIsZ46XvPhTD5CtK9j5HyYWCpadFGlpmskT8E_lBwrCGdJQae8EEO7-NllRLLaTTqz52KsT-Mhyolu7MMZy1lm58NXhv4g5_rzLbKelTWnbsBJzu-phdkg
Its header decodes to {"alg":"RS256","kid":"9A6B2910D62CE0379F3BDB520A575E22","typ":"at+jwt"}. So the issuer signed it with an RSA key, and an API would pick the matching public key by that kid.
The payload carries an issuer of https://demo.duendesoftware.com, an audience of api, a client_id of m2m and a scope of api. There is no sub claim at all, because the client credentials grant issues the token to a machine, not to a user. The JWT specification, RFC 7519, allows that: of the subject claim it says “Use of this claim is OPTIONAL.”
Which claims go in is the identity server’s decision. In Duende, for example, we can add custom claims to an access token.
jwt.io is a convenient tool that allows us to view the stored information in the JWT. Let’s use it to examine the decoded information of the token we just saw:
The tool decodes all the properties of the JWT separated as Header, Payload, and Signature sections. A token pasted into any website is a credential handed to that website, so we keep production tokens out of online decoders and decode them in our own code instead. Next, let’s explore how to decode JWTs with .NET code.
How Do We Read a JWT With JsonWebTokenHandler?
First, we need to install the required NuGet package to get this working:
dotnet add package Microsoft.IdentityModel.JsonWebTokens
Older code and many answers online use the System.IdentityModel.Tokens.Jwt package instead. It still works, but its own NuGet description now says: “As of IdentityModel 7x, this is a legacy tool that should be replaced with Microsoft.IdentityModel.JsonWebTokens.” ASP.NET Core made the same move in version 8, when its JWT bearer handler switched to JsonWebToken by default.
Every method from here on goes in a static JwtUtils class, with the using lines from the snippets at the top of its file. Our first task is to convert the token in the string format to an instance of a JsonWebToken class in the Microsoft.IdentityModel.JsonWebTokens namespace:
using Microsoft.IdentityModel.JsonWebTokens;
public static JsonWebToken ConvertJwtStringToJsonWebToken(string jwt)
{
var handler = new JsonWebTokenHandler();
var token = handler.ReadJsonWebToken(jwt);
return token;
}
First, we create an instance of a JsonWebTokenHandler. Next, we use the ReadJsonWebToken() method to read and parse the input JWT string, which converts it into a JsonWebToken. The method then returns the acquired token.
If the string is not a JWT at all, ReadJsonWebToken() throws an ArgumentException, and for an opaque reference token that is a SecurityTokenMalformedException. When we cannot be sure what we were given, handler.CanReadToken(jwt) lets us check its shape first.
Once we obtain the token, we decode it as the next step:
public static DecodedToken DecodeJwt(JsonWebToken token)
{
var keyId = token.Kid;
var audience = token.Audiences.ToList();
var claims = token.Claims.Select(claim => (claim.Type, claim.Value)).ToList();
return new DecodedToken(
keyId,
token.Issuer,
audience,
claims,
token.ValidTo,
token.Alg,
token.EncodedToken,
token.Subject,
token.ValidFrom,
token.EncodedHeader,
token.EncodedPayload
);
}
The DecodedToken it returns is our own record, in a file of its own:
public record DecodedToken(
string KeyId,
string Issuer,
List<string> Audience,
List<(string Type, string Value)> Claims,
DateTime Expiration,
string SigningAlgorithm,
string RawData,
string Subject,
DateTime ValidFrom,
string Header,
string Payload
);
We extract essential information from a provided JsonWebToken. Through a series of operations, we retrieve key details from the token. Initially, we capture the key id (Kid) from the token’s Header, followed by acquiring the Issuer information directly from the token itself. The Audience data, representing recipients, is retrieved and formatted into a list for easy access.
Then, we extract Claims, converting them into a list of tuples containing the claim Type and its corresponding Value. To determine the token’s expiration, we retrieve the expiration date (ValidTo), which comes back in UTC. Additionally, we capture the signing algorithm used to sign the token (Alg).
Also, we fetch the complete encoded token (EncodedToken) directly from the token. We then attempt to gather the Subject information, which may sometimes be empty, especially in scenarios like “client_credentials” (Machine to Machine) grants.
Furthermore, we retrieve the token’s validity start date (ValidFrom), capturing the moment the token is considered valid. We also acquire the header and payload in their encoded form (EncodedHeader and EncodedPayload, respectively).
In summary, we systematically gather crucial details such as key id, issuer, audience, claims, expiration, signing algorithm, raw data, subject, validity start, header, and payload from the provided JWT, consolidating them into DecodedToken type for further processing or analysis.
Code written for JwtSecurityTokenHandler moves over with little more than renames. This table maps every member we used, plus the two that behave differently with our sample token.
| What we read | JwtSecurityToken (legacy) | JsonWebToken |
|---|---|---|
| Handler | JwtSecurityTokenHandler | JsonWebTokenHandler |
| Read the token | ReadJwtToken(jwt) | ReadJsonWebToken(jwt) |
| Key id | Header.Kid | Kid |
| Signing algorithm | SignatureAlgorithm | Alg |
| Issuer | Issuer | Issuer |
| Audiences | Audiences | Audiences |
| Claims | Claims | Claims |
Subject when the token has no sub | Subject returns null | Subject returns an empty string |
| Expiry and start | ValidTo, ValidFrom (UTC) | ValidTo, ValidFrom (UTC) |
| Whole token | RawData | EncodedToken |
| Encoded header and payload | EncodedHeader, EncodedPayload | EncodedHeader, EncodedPayload |
ToString() | decoded header and payload as JSON | the encoded header and payload |
Inside ASP.NET Core, the token that JwtBearer events hand us has been a JsonWebToken since version 8, so an old as JwtSecurityToken cast there now returns null.
Does Decoding a JWT Validate It?
No. Decoding only reads what the token says about itself.
ReadJsonWebToken() does not check the signature, the issuer, the audience or the expiry. A token with a forged signature, or one that expired years ago, decodes just as happily as a fresh one.
We can see this with the sample token from earlier. It expired on 22 November 2023, and ReadJsonWebToken() still returns every claim without a complaint. If we replace the last five characters of its signature, it still decodes.
That makes decoding the right tool for debugging and for logging what a token says. It is the wrong tool for deciding whether a caller may do something.
For that, the API validates the token. Hand the same token to JsonWebTokenHandler.ValidateTokenAsync() with the expected issuer and audience, and it returns a failed result carrying a SecurityTokenExpiredException.
In an ASP.NET Core API, we rarely call that method ourselves. The JWT bearer handler validates the signature, issuer, audience and lifetime of every incoming token before our code runs, which is what we set up in JWT authentication in ASP.NET Core Web API.
Once the request is through, the validated claims are already on User, so there is nothing left to decode by hand. We show how to read the current user’s claims in a separate article.
Can We Decode a JWT Without a NuGet Package?
Yes. Since .NET 9, the Base64Url class in System.Buffers.Text decodes Base64url text directly, so splitting the token on its dots is all the parsing we need:
using System.Buffers.Text;
using System.Text;
public static (string Header, string Payload) DecodeJwtWithoutLibrary(string jwt)
{
var parts = jwt.Split('.');
var header = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[0]));
var payload = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[1]));
return (header, payload);
}
Here, we split the token into its three parts and decode only the first two, because the signature is binary data with no JSON inside. Base64Url.DecodeFromChars() returns the raw bytes, and Encoding.UTF8.GetString() turns them into JSON text.
For our sample token, the header comes back as:
{"alg":"RS256","kid":"9A6B2910D62CE0379F3BDB520A575E22","typ":"at+jwt"}
Why not Convert.FromBase64String()? Base64url replaces + and / with - and _, and it drops the = padding. Our sample’s header is 95 characters long and its payload 239, neither a multiple of four, and Convert.FromBase64String() throws a FormatException on both.
This route gives us the header and the payload as JSON strings, which suits a quick look in a log or a test. When we need the issuer, the expiry or the audience as typed values, JsonWebToken already does that work for us.
Conclusion
Decoding a JWT in .NET takes one call. JsonWebTokenHandler.ReadJsonWebToken() turns the token string into a JsonWebToken, and its properties give us the key id, issuer, audience, claims, expiry and the rest. Code still on JwtSecurityTokenHandler moves over with a handful of renames, and Base64Url covers the quick look with no package at all.
Decoding never tells us whether to trust the token. Validation does, and in an ASP.NET Core API the JWT bearer handler validates every incoming token for us.
If the token we want to inspect arrives with an incoming request, the next step is to get the access token from HttpContext first.
Tested with .NET 10.0.10 and Microsoft.IdentityModel.JsonWebTokens 8.23.0.

