Zero-Dependency JWT Decoding in C#
The .NET base library does not include native Base64URL support before .NET 5, so you must pad the string and swap URL-safe characters. This function decodes a JWT payload with zero NuGet packages on .NET Standard 2.0+.
using System;
using System.Text;
using System.Text.Json;
using System.Collections.Generic;
public static class JwtInspector {
// Decodes JWT payload without verifying — inspection only.
public static Dictionary<string, JsonElement> DecodePayload(string token) {
var parts = token.Split('.');
if (parts.Length != 3) throw new ArgumentException("Not a JWT");
var payload = parts[1]
.Replace('-', '+')
.Replace('_', '/');
// Pad to multiple of 4
switch (payload.Length % 4) {
case 2: payload += "=="; break;
case 3: payload += "="; break;
}
var bytes = Convert.FromBase64String(payload);
var json = Encoding.UTF8.GetString(bytes);
return JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
}
}
// Usage
var claims = JwtInspector.DecodePayload("eyJhbGciOi...");
Console.WriteLine(claims["sub"].GetString());
Console.WriteLine(claims["exp"].GetInt64());For logging, debugging, and developer tools. Never trust these claims for authorization — no signature is checked.
Using System.IdentityModel.Tokens.Jwt — The Microsoft Library
The official Microsoft JWT implementation. Ships with ASP.NET Core, integrates with Identity Server, Azure AD, and IdentityModel.
Installation
dotnet add package System.IdentityModel.Tokens.Jwt
dotnet add package Microsoft.IdentityModel.TokensRead (Decode) a Token
using System.IdentityModel.Tokens.Jwt;
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(tokenString);
// Header
Console.WriteLine(jwt.Header.Alg); // e.g. RS256
Console.WriteLine(jwt.Header.Kid); // key ID
// Payload
foreach (var claim in jwt.Claims) {
Console.WriteLine($"{claim.Type}: {claim.Value}");
}
Console.WriteLine(jwt.Subject);
Console.WriteLine(jwt.ValidFrom); // nbf
Console.WriteLine(jwt.ValidTo); // exp
Console.WriteLine(jwt.Issuer); // iss
Console.WriteLine(jwt.Audiences.First()); // audVerify with HS256 Secret
using Microsoft.IdentityModel.Tokens;
using System.Text;
var secret = Environment.GetEnvironmentVariable("JWT_SECRET");
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
var tvp = new TokenValidationParameters {
ValidIssuer = "https://auth.example.com",
ValidAudience = "https://api.example.com",
IssuerSigningKey = key,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromSeconds(30), // Reasonable drift tolerance
};
try {
var principal = new JwtSecurityTokenHandler()
.ValidateToken(tokenString, tvp, out SecurityToken validated);
var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return principal;
} catch (SecurityTokenExpiredException) {
// exp is in the past
return null;
} catch (SecurityTokenInvalidSignatureException) {
// Signature does not match
return null;
} catch (SecurityTokenException) {
// Any other validation failure
return null;
}Verify with RS256 (Public Key)
using System.Security.Cryptography;
// Load public key from PEM
var rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText("public.pem"));
var rsaKey = new RsaSecurityKey(rsa);
var tvp = new TokenValidationParameters {
ValidIssuer = "https://auth.example.com",
ValidAudience = "https://api.example.com",
IssuerSigningKey = rsaKey,
ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 },
// Reject alg confusion — never accept HS256 with an RSA key
};
var principal = new JwtSecurityTokenHandler()
.ValidateToken(tokenString, tvp, out _);ASP.NET Core Configuration (Program.cs)
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
// HS256 with static secret
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"])),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
};
});
// RS256 with JWKS discovery (recommended for Auth0, Azure AD, Cognito)
// builder.Services.AddAuthentication("Bearer")
// .AddJwtBearer("Bearer", options => {
// options.Authority = "https://your-tenant.auth0.com/";
// options.Audience = "https://api.example.com";
// });
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/me", (ClaimsPrincipal user) =>
Results.Ok(new { userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value })
).RequireAuthorization();
app.Run();Controller Usage with [Authorize]
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class UsersController : ControllerBase {
[HttpGet("me")]
public IActionResult Me() {
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var email = User.FindFirst(ClaimTypes.Email)?.Value;
var roles = User.FindAll(ClaimTypes.Role).Select(r => r.Value);
return Ok(new { userId, email, roles });
}
[HttpGet("admin")]
[Authorize(Roles = "admin")]
public IActionResult AdminOnly() => Ok("secret data");
}Common .NET JWT Errors
- SecurityTokenExpiredException — exp is in the past. Client must refresh the token.
- SecurityTokenNotYetValidException — nbf is in the future. Usually a clock-skew issue on the client.
- SecurityTokenInvalidSignatureException — signature does not match your key. Wrong secret, wrong public key, or the token was tampered with.
- SecurityTokenMalformedException — token is not three dot-separated segments.
- SecurityTokenInvalidAudienceException — aud claim does not match ValidAudience.
- SecurityTokenInvalidAlgorithmException — alg header is not in ValidAlgorithms. Set this list explicitly to prevent alg confusion attacks.
Security Best Practices for .NET JWT
- Always set
ValidAlgorithmsin TokenValidationParameters. Never allow "none" and never allow HS256 with an asymmetric key. - For HS256, the secret must be at least 32 random bytes. Use
RandomNumberGenerator.GetBytes(32), store in Key Vault / User Secrets — never in source. - For production with an IdP (Auth0, Okta, Azure AD, Cognito), use
options.Authorityand let JwtBearer handle JWKS discovery, caching, and rotation. - Set
ClockSkew = TimeSpan.FromSeconds(30)— the default is 5 minutes which is far too generous. - Enable
RequireExpirationTime = trueandRequireSignedTokens = true— both default to true but be explicit. - Do not log full tokens. Log only jti (JWT ID) and sub (subject) for audit trails.
Key Facts
- Official lib:
- System.IdentityModel.Tokens.Jwt + Microsoft.IdentityModel.Tokens
- Alt lib:
- jose-jwt (dvsekhvalnov/jose-jwt) for JWE and custom needs
- HS256 secret:
- 32+ random bytes. RandomNumberGenerator.GetBytes(32).
- JWKS:
- Set
options.Authorityin AddJwtBearer — auto-discovery + rotation - Framework:
- .NET Standard 2.0+ / .NET Framework 4.6.1+ / .NET 8
Related JWT Tools
- JWT Decoder Online — decode any token instantly
- JWT Decoder Node.js — jsonwebtoken and jose patterns
- JWT Decoder Python — PyJWT for Flask/Django/FastAPI
- JWT Decoder Java — jjwt and Spring Boot patterns
- JWT Decoder Go — golang-jwt/jwt v5 and Gin/Fiber