JWT
System.IdentityModel.Tokens.Jwt
token verification
decoding tokens
.NET security

Decoding and verifying JWT token using System.IdentityModel.Tokens.Jwt

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

JWT handling in .NET usually starts with the same string, but there are two very different jobs you can do with it. One job is decoding, which only parses the token so you can inspect claims and headers. The other is verification, which checks whether the token is trustworthy enough to drive authentication or authorization decisions.

Decoding Is Parsing, Not Trust

JwtSecurityTokenHandler.ReadJwtToken is useful when you want to inspect a token during debugging or log analysis. It reads the header and payload, but it does not prove that the signature is valid or that the token was issued for your API.

csharp
1using System;
2using System.IdentityModel.Tokens.Jwt;
3
4var tokenText = Environment.GetEnvironmentVariable("JWT_TEXT");
5var handler = new JwtSecurityTokenHandler();
6var jwt = handler.ReadJwtToken(tokenText);
7
8Console.WriteLine($"alg: {jwt.Header.Alg}");
9Console.WriteLine($"iss: {jwt.Issuer}");
10Console.WriteLine($"sub: {jwt.Subject}");

That output is convenient, but it is still untrusted input. A forged token can be decoded just as easily as a valid one. If your code reads a decoded role claim and immediately grants access, the signature check has effectively been skipped.

Validate With Explicit Rules

Verification happens through ValidateToken. The key point is that validation should reflect your actual security policy, not a vague set of defaults. For most APIs, that means checking the signing key, issuer, audience, and expiration time together.

csharp
1using System;
2using System.IdentityModel.Tokens.Jwt;
3using System.Security.Claims;
4using System.Text;
5using Microsoft.IdentityModel.Tokens;
6
7var key = new SymmetricSecurityKey(
8    Encoding.UTF8.GetBytes("0123456789abcdef0123456789abcdef")
9);
10
11var validationParameters = new TokenValidationParameters
12{
13    ValidateIssuerSigningKey = true,
14    IssuerSigningKey = key,
15    ValidateIssuer = true,
16    ValidIssuer = "https://auth.example.com",
17    ValidateAudience = true,
18    ValidAudience = "api://inventory",
19    ValidateLifetime = true,
20    ClockSkew = TimeSpan.FromMinutes(1)
21};
22
23var handler = new JwtSecurityTokenHandler();
24
25try
26{
27    ClaimsPrincipal principal = handler.ValidateToken(
28        tokenText,
29        validationParameters,
30        out SecurityToken validatedToken
31    );
32
33    Console.WriteLine(principal.FindFirst("sub")?.Value);
34}
35catch (SecurityTokenException ex)
36{
37    Console.WriteLine($"Token rejected: {ex.Message}");
38}

Small configuration mistakes matter here. If the API accepts a token for the wrong audience, the signature may still be correct while the token is still wrong for your service. That is why audience and issuer checks belong in the same validation block as the signature.

Check What You Expect After Validation

A successful validation gives you a ClaimsPrincipal, but it is still worth checking for claim shape and token metadata that your application depends on. Some teams also verify the expected signing algorithm after the cryptographic check so that changes in issuer configuration do not silently weaken assumptions.

csharp
1if (validatedToken is JwtSecurityToken jwtToken &&
2    !string.Equals(jwtToken.Header.Alg, SecurityAlgorithms.HmacSha256, StringComparison.Ordinal))
3{
4    throw new SecurityTokenException("Unexpected signing algorithm");
5}
6
7var subject = principal.FindFirst("sub")?.Value;
8var scope = principal.FindFirst("scope")?.Value;
9
10if (string.IsNullOrWhiteSpace(subject) || string.IsNullOrWhiteSpace(scope))
11{
12    throw new SecurityTokenException("Required claims are missing");
13}

This second layer is not a replacement for signature verification. It is there to keep the application strict about what a valid identity must contain before business logic runs.

Centralize Validation in ASP.NET Core

Manual calls to ValidateToken are fine for small utilities, but production APIs usually want one shared configuration path. In ASP.NET Core, the normal approach is to configure JWT bearer authentication once and let middleware populate the authenticated user for each request.

csharp
1builder.Services
2    .AddAuthentication("Bearer")
3    .AddJwtBearer("Bearer", options =>
4    {
5        options.TokenValidationParameters = validationParameters;
6    });

That keeps every endpoint on the same policy. It also makes testing easier, because failure cases such as expired tokens or invalid issuers behave consistently everywhere instead of depending on hand-written controller code.

Handle Keys Like a Deployment Concern

Token verification is only as strong as the key management behind it. A local demo can read a symmetric secret from configuration, but a real system usually loads secrets or public keys from a secure store and supports rotation. If key rotation is part of the issuer design, your service should tolerate overlap periods where both old and new keys are valid.

Testing should cover more than the happy path. Include expired tokens, wrong audiences, tampered signatures, and missing claims. Those cases often catch the accidental weakening of TokenValidationParameters during future refactors.

Common Pitfalls

Decoding a JWT and treating its claims as trusted data is the most common failure. Another frequent mistake is validating the signature but disabling issuer or audience checks because setup feels inconvenient. Teams also get into trouble when secrets are hardcoded, clock skew is set too high, or claim requirements are enforced loosely and silently defaulted.

Summary

  • Decoding only parses a JWT; it does not prove trust.
  • Use ValidateToken with explicit issuer, audience, lifetime, and signing key rules.
  • Treat claim presence and expected algorithm as part of application-level validation.
  • Centralize validation in ASP.NET Core so every endpoint follows the same policy.
  • Test failure paths, not just successful authentication.

Course illustration
Course illustration

All Rights Reserved.