How to handle JWT in .NET
Everything you need to know to implement and validate JWTs securely in .NET: from token creation and JWKS verification to ASP.NET Core middleware integration, with code examples and best practices throughout.
.NET has a unique position in the JWT ecosystem: the libraries are built and maintained by Microsoft as part of the IdentityModel project, tightly integrated with ASP.NET Core's authentication pipeline, and used by virtually every .NET application that consumes tokens from Azure AD, Entra ID, or any OIDC-compliant provider. You do not need to evaluate third-party libraries or piece together a solution. The tooling is first-party, well-documented, and deeply woven into the framework.
That said, the IdentityModel library has gone through a significant generational shift. The older JwtSecurityTokenHandler (in System.IdentityModel.Tokens.Jwt) has been replaced by the faster, async-native JsonWebTokenHandler (in Microsoft.IdentityModel.JsonWebTokens). If you are working on an existing codebase, you may still encounter the legacy API. If you are starting a new project, you should use the modern one.
This guide covers both generations and walks through everything you need to know about JWT handling in .NET in 2026: token creation and validation, JWKS with automatic key discovery, ASP.NET Core JWT Bearer authentication, claim mapping, the TokenValidationParameters system, and production best practices. Let's dive right in.
!!Need to inspect a token? Use the WorkOS JWT Debugger to decode and inspect your JWTs directly in the browser. It's a quick way to verify your token's iss, aud, sub, and other claims while debugging.!!
JWT 101
A JSON Web Token is a compact, URL-safe token format used to securely transmit information between systems. At a high level, a JWT lets one system make a signed statement about a user or service, and lets another system verify that statement without needing to look anything up in a database.
They are typically used to indicate a user's identity and/or assert permissions and roles.
A JWT is composed of three parts, each Base64URL-encoded and separated by dots:
Header
The header contains metadata about the token, most importantly the signing algorithm used to create the signature (e.g., HMAC, RSA, or ECDSA). This tells the verifier how the token was signed and how it should be validated.
A typical header before encoding:
In this example, alg is set to RS256, representing RSA with SHA-256, and typ identifies this as a JWT.
Payload
The payload contains the actual data the token encodes. These data points are called claims.
Claims are pieces of information about the subject of the token and additional context about how it should be used. Some claims are registered and standardized, like iss, sub, aud, and exp (for the full list check the JWT claims registry). Others are custom and application-specific.
Example payload:
It is important to note that the payload is not encrypted. Anyone who has the token can decode it and read the claims. Do not put passwords, secrets, or high-risk PII in JWT payloads.
Signature
The signature ensures the token's integrity and confirms that it was issued by a trusted source. It is created by hashing the Base64URL-encoded header and payload with a secret key (for symmetric algorithms like HS256) or a private key (for asymmetric algorithms like RS256). The resulting hash is then Base64URL-encoded and appended to the token.
When a JWT is received, the verifier recomputes the signature using the appropriate key and compares it to the signature included in the token. If they do not match, the token has been tampered with and must be rejected.
JWTs are protected via JSON Web Signature (JWS). JWS is used to share data between parties when confidentiality is not required, because the claims within a JWS can be read by anyone (they are simply Base64URL-encoded). The signature provides authentication, not encryption. Some of the cryptographic algorithms JWS uses are HMAC, RSA, and ECDSA.
JWT libraries in .NET
Unlike most languages where JWT handling comes from a community-maintained package, .NET's JWT libraries are built by Microsoft as part of the IdentityModel project. There are two generations:
Microsoft.IdentityModel.JsonWebTokens is the modern library. It provides JsonWebTokenHandler and JsonWebToken, offering async token validation (ValidateTokenAsync), 30% better performance than the legacy handler, full AOT (ahead-of-time compilation) compatibility, and a result-based API that returns TokenValidationResult instead of throwing exceptions on invalid tokens. This is what ASP.NET Core 8+ uses by default.
System.IdentityModel.Tokens.Jwt is the legacy library. It provides JwtSecurityTokenHandler and JwtSecurityToken. As of IdentityModel 7x, it is officially deprecated in favor of Microsoft.IdentityModel.JsonWebTokens. You will still find it in tutorials, older codebases, and the WorkOS .NET SDK. It works, but new projects should use the modern library.
Both libraries share the same TokenValidationParameters class from Microsoft.IdentityModel.Tokens for configuring validation rules.
For ASP.NET Core applications, the JWT Bearer middleware pulls in these dependencies automatically:
For standalone token handling outside of ASP.NET Core:
Generating your keys
First, you need a set of cryptographic keys to sign your tokens.
In this tutorial, we will be using RS256. This asymmetric algorithm requires two keys: a private key to sign the token and a public key to verify it. If you already have them, move along to the next section.
!!Asymmetric algorithms use a pair of public and private keys to sign and verify the tokens. They are more secure, scalable, and better for distributed systems but also more resource-intensive and complex. For more on the various algorithms see Which algorithm should you use to sign JWTs?!!
There are many ways to generate your keys. You could generate them using OpenSSL and save them as raw PEM files that your code would read:
However, this is not a best practice. Instead, you should use JSON Web Key Sets (JWKS), especially in distributed or cloud environments.
!!JWKS vs PEM: JWKS simplifies key rotation by allowing services to fetch the latest keys from a central endpoint, making updates easier and reducing the risk of errors. PEM files require manual distribution and updates, which can be cumbersome in large systems. JWKS centralizes key distribution, ensuring that all services or clients always have the correct keys without constant manual updates.!!
If you are using a third-party identity provider (like WorkOS), they automatically generate and expose a JWKS endpoint for you. This allows clients to dynamically fetch the public keys needed for JWT verification without you having to manage the keys manually. WorkOS offers a public JWKS endpoint:
Clients and APIs can use this endpoint to retrieve the public keys needed to validate JWTs signed by WorkOS. Key rotation, expiration, and distribution are handled automatically by the provider.
!!If you need something fast for a proof-of-concept, you can use a tool like mkjwk.org to generate a JWK.!!
Generating an RSA key pair in C#
.NET's System.Security.Cryptography namespace provides native RSA key generation:
The RsaSecurityKey class from Microsoft.IdentityModel.Tokens wraps the raw RSA parameters into the key type that the JWT libraries expect. The KeyId becomes the kid in your JWT headers.
Exporting as a JWK
To expose the public key at a JWKS endpoint:
Creating a JWT in .NET
Once you have your RSA keys, you can create and sign a token. Here is the modern approach using JsonWebTokenHandler:
A few things to note about the .NET approach:
Claims are .NET Claim objects. Unlike other languages where claims are just key-value pairs in a dictionary, .NET uses the ClaimsIdentity and Claim types from System.Security.Claims. This is because claims-based identity is a core concept in .NET, not just a JWT feature. The same ClaimsPrincipal that represents a JWT-authenticated user also works with cookie authentication, Windows authentication, and any other scheme.
Multi-valued claims are repeated, not arrays. To encode a claim like "roles": ["admin", "editor"], you add two separate Claim objects with the same type. The JWT handler serializes them into a JSON array automatically.
SecurityAlgorithms.RsaSha256 is a constant. .NET uses string constants from SecurityAlgorithms instead of raw strings like "RS256". This prevents typos and gives you IntelliSense support.
Sending the token as a Bearer token
Once the client has the JWT, it sends it in the Authorization header as a Bearer token:
On the server side, the ASP.NET Core JWT Bearer middleware extracts and validates this token automatically, which we will cover shortly.
Decoding a JWT
Decoding a JWT without verifying it can be useful for debugging and logging, but it should never be used for authorization decisions. The modern JsonWebTokenHandler provides ReadJsonWebToken:
The token is NOT validated at this point. Microsoft's documentation explicitly warns that no security decisions should be made about the contents of a token read with ReadJsonWebToken.
About the kid claim
The kid (key ID) appears in the JWT header, not the payload:
It tells your application which public key (from a set of keys) should be used to verify the signature. This is essential when your authentication provider uses key rotation, publishing multiple public keys at a JWKS endpoint and including kid in the JWT header to indicate which key was used to sign it.
In .NET, the JWT Bearer middleware and JsonWebTokenHandler handle kid resolution automatically when you configure an Authority or JWKS endpoint. The middleware fetches the JWKS, matches the kid from the incoming token to a key in the set, and uses that key for verification.
Verifying a JWT
Verification is where .NET's approach differs most from other languages. Instead of separate function calls for signature checking and claims validation, .NET uses a single TokenValidationParameters object that declaratively defines every aspect of what makes a token acceptable.
Verifying with TokenValidationParameters
The modern ValidateTokenAsync method returns a TokenValidationResult instead of throwing exceptions. This is a deliberate design choice: in high-throughput APIs, throwing and catching exceptions on every invalid token (expired, malformed, wrong audience) is expensive. The result-based pattern lets you check result.IsValid and branch accordingly.
Important: the default ClockSkew is 5 minutes. Unlike other languages where clock tolerance defaults to zero and you opt into a small leeway, .NET defaults to TimeSpan.FromMinutes(5). This means a token that expired up to 5 minutes ago will still be accepted. For most APIs, this is too generous. Set it explicitly:
Verifying with automatic JWKS discovery
For production applications that consume tokens from an identity provider, you can configure automatic OIDC metadata and JWKS discovery. The provider's /.well-known/openid-configuration endpoint tells the library where to find the JWKS, what algorithms are supported, and who the issuer is:
The ConfigurationManager fetches and caches the OIDC metadata (including the JWKS). It automatically refreshes when keys rotate, using a "last known good" strategy that provides resilience if the metadata endpoint is temporarily unavailable. This is the same infrastructure that ASP.NET Core's JWT Bearer middleware uses internally.
Integrating with ASP.NET Core
Most .NET applications in production use ASP.NET Core, and the framework provides first-class JWT Bearer authentication through its middleware pipeline. This is the recommended approach for API authentication.
Minimal API with JWT Bearer
For .NET 8+ minimal APIs:
When you set Authority, the middleware automatically fetches the OIDC discovery document at {Authority}/.well-known/openid-configuration, retrieves the JWKS from the endpoint specified in the metadata, caches and rotates keys as needed, and validates the issuer against the metadata. You do not need to manually configure signing keys or the issuer URI.
MapInboundClaims: an important detail
By default, the JWT Bearer middleware remaps claim types from their short JWT names to long URI-based .NET claim types. For example, sub becomes http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier. This is a holdover from WS-Federation and WIF (Windows Identity Foundation).
Setting MapInboundClaims = false preserves the original claim names from the JWT payload. This means context.User.FindFirst("sub") works as you would expect, instead of requiring the full URI. For new projects, always set this to false.
Controller-based APIs
If you are using controllers instead of minimal APIs, the configuration is the same. The difference is how you access the authenticated user:
Authorization policies
ASP.NET Core's policy-based authorization lets you define reusable rules that check claims:
Apply policies to endpoints or controllers with [Authorize(Policy = "RequireAdmin")] or .RequireAuthorization("RequireAdmin"). This keeps authorization logic centralized rather than scattered across individual action methods.
Configuration via appsettings.json
Store your JWT configuration in appsettings.json:
And bind it in Program.cs:
For sensitive values, use .NET User Secrets during development (dotnet user-secrets set "Jwt:Authority" "...") and environment variables or Azure Key Vault in production.
Handling JWT events
The JWT Bearer middleware exposes events that let you hook into the authentication pipeline:
Note that in ASP.NET Core 8+, the SecurityToken in OnTokenValidated is a JsonWebToken (from the modern library), not a JwtSecurityToken. If you are migrating from an older codebase that downcasts to JwtSecurityToken, you need to update those casts.
JWT best practices (.NET edition)
JWTs are simple in structure, but security lives in the details you enforce. Here are the practices that matter most in production .NET applications.
- Use the modern JsonWebTokenHandler.
JwtSecurityTokenHandleris deprecated as of IdentityModel 7x. TheJsonWebTokenHandleroffers async validation, better performance, AOT support, and a result-based API. For ASP.NET Core 8+, the middleware already uses it by default. - Set MapInboundClaims to false. The default claim type mapping transforms short JWT claim names into long URI-based .NET claim types. This is confusing, hard to debug, and serves no purpose for modern applications. Set
options.MapInboundClaims = falsein your JWT Bearer configuration. - Override the default ClockSkew. .NET defaults to 5 minutes of clock tolerance, which is far more generous than other platforms. Set
ClockSkew = TimeSpan.FromSeconds(30)orTimeSpan.Zeroto match the behavior you expect. - Use the Authority pattern for OIDC providers. When your tokens come from an OIDC-compliant provider (WorkOS, Entra ID, Auth0, Okta), set the
Authorityoption instead of manually configuring signing keys. The middleware will discover the JWKS endpoint, fetch and cache keys, and handle rotation automatically through theConfigurationManager. - Validate algorithms explicitly. Set
ValidAlgorithmsin yourTokenValidationParametersto accept only the algorithms you expect (typically just RS256). This prevents algorithm confusion attacks where an attacker sends a token signed with an unexpected algorithm. - Enforce Bearer token format. ASP.NET Core's JWT Bearer middleware extracts tokens from the
Authorization: Bearer <jwt>header automatically. Treat tokens in query parameters as a problem, because they leak into logs, browser history, and referrer headers. If you must accept tokens from query strings (for example, for WebSocket connections), use theOnMessageReceivedevent explicitly rather than loosening the default behavior. - Keep access tokens short-lived. Short
expvalues (5 to 15 minutes) reduce the blast radius of a leaked token. If you need long sessions, use refresh tokens and rotate them. - Use policy-based authorization. Prefer
[Authorize(Policy = "RequireAdmin")]over manual claim checks in action methods. Policies centralize authorization rules, make them testable, and keep controllers focused on business logic. - Use HTTPS everywhere. JWTs are bearer credentials. If someone can intercept the request, they can replay the token.
- Log auth events via JwtBearerEvents. Use
OnTokenValidatedandOnAuthenticationFailedto log authentication results. Log thesub,kid, and failure reason. Never log the full token string. - Test with bad tokens. Make sure your test suite covers expired tokens, tokens with wrong
issoraud, tokens signed with the wrong key, tokens with tampered payloads, missing required claims, wrong algorithm ornoneedge cases, and tokens that fall outside your configuredClockSkew.
Let WorkOS handle the heavy lifting
While handling JWTs with the IdentityModel libraries is often necessary at the API layer, it is worth stepping back and looking at the bigger picture: how those tokens are issued in the first place.
If you are building authentication flows, especially ones that involve Single Sign-On (SSO), SCIM provisioning, or multi-tenant identity, there is a lot more to solve than signing and verifying tokens. You need to support different identity providers, manage users and directories, rotate keys safely, and issue tokens that downstream services can trust.
WorkOS provides a modern API for enterprise-ready authentication features, letting you integrate SSO (SAML, OIDC, and more), manage users and directories, and issue secure tokens without building and maintaining a full auth stack from scratch. WorkOS has a .NET SDK (available on NuGet as WorkOS.net) that handles the OAuth flow, token exchange, and user management with idiomatic async C# code. It is especially useful if you need to support enterprise customers or want to offer a "Login with your company" experience. And it is free for up to 1,000,000 monthly active users.