Introducing the next era of Duende IdentityServer.

Read our CEO’s announcement

What Is a JWT? Structure, Validation, and Best Practices

Khalid Abuhakmeh
Two blue circles

A JWT (pronounced "jot") is a signed JSON object used to transmit claims between systems. It is the token format used for ID tokens in OpenID Connect and commonly used for access tokens in OAuth 2.0. A JWT has three parts: header, payload, and signature. It is self-contained, tamper-evident, and compact enough to pass in an HTTP header or URL.

Updated: August 2026

What Is a JSON Web Token?

A JSON Web Token is a compact, URL-safe means of representing claims between two parties. RFC 7519 defines the format. JWTs can be signed using JSON Web Signature (JWS, RFC 7515) or encrypted using JSON Web Encryption (JWE, RFC 7516).

In identity systems, JWTs serve two primary roles:

  • ID tokens carry user identity claims back to a client application after authentication.
  • Access tokens carry authorization grants that APIs use to enforce permissions.

The signing mechanism guarantees that the token has not been tampered with since the issuer created it. Any party with access to the issuer's public key can verify the token independently, without calling back to the issuer.

What Does a JWT Look Like?

A JWT is three Base64URL-encoded strings separated by dots:

eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6IkFCQzEyMyJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo1MDAxIiwic3ViIjoidXNlcjEyMyIsImF1ZCI6ImFwaTEiLCJleHAiOjE3MjM0NDk2MDAsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgYXBpMSIsImNsaWVudF9pZCI6IndlYmFwcCJ9.signature_bytes_here

Decoded, the three parts are:

Header

Json

{
  "alg": "RS256",
  "typ": "at+jwt",
  "kid": "ABC123"
}
Field Purpose

alg

The signing algorithm (RS256 = RSA with SHA-256)

typ

The token type (at+jwt for access tokens per RFC 9068)

kid

Key ID, used to look up the correct public key from the JWKS endpoint

Payload

Json

{
  "iss": "https://localhost:5001",
  "sub": "user123",
  "aud": "api1",
  "exp": 1723449600,
  "scope": "openid profile api1",
  "client_id": "webapp"
}

The payload contains claims about the subject and the token itself. See the claims table below for details on each field.

Signature

The signature is created by signing the encoded header and encoded payload with the issuer's private key:

RS256(base64url(header) + "." + base64url(payload), privateKey)

Any consumer can verify this signature by retrieving the public key from the issuer's JWKS endpoint (RFC 7517).

Try decoding a JWT yourself with the Duende JWT Decoder Tool.

What Are JWT Claims?

Claims are name-value pairs in the payload. They assert facts about the subject or the token itself.

Registered Claims

Claim Full Name Purpose

iss

Issuer

Identifies the token issuer (your IdentityServer URL)

sub

Subject

Identifies the user or entity the token represents

aud

Audience

Identifies the intended recipient (your API)

exp

Expiration

Unix timestamp after which the token is invalid

nbf

Not Before

Unix timestamp before which the token is invalid

iat

Issued At

Unix timestamp when the token was created

jti

JWT ID

Unique identifier for the token (useful for replay detection)

scope

Scope

Space-delimited list of granted scopes

client_id

Client ID

The application that requested the token

You can include custom claims (like department, role, or tenant_id) to carry application-specific data.

ID tokens and access tokens contain different claims for different purposes. ID tokens focus on user identity (name, email, email_verified). Access tokens focus on authorization (scope, aud, client_id).

How Does JWT Validation Work?

Every API that receives a JWT must validate it before trusting its claims. The process follows these steps:

  1. Parse the token into its three Base64URL-encoded parts.
  2. Decode the header to identify the signing algorithm (alg) and key ID (kid).
  3. Retrieve the issuer's public key from the JWKS endpoint. The API discovers this via /.well-known/openid-configuration, which contains the jwks_uri.
  4. Verify the signature against the public key.
  5. Check the payload claims: exp (not expired), nbf (valid now), iss (trusted issuer), aud (intended for this API).
  6. Extract claims and apply your authorization logic.

Validating JWTs in ASP.NET Core

The JWT Bearer middleware handles the first five validation steps for you. It fetches the discovery document, retrieves the JWKS, and validates signature, issuer, audience, and lifetime on every request.

Csharp

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = "https://localhost:5001";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidAudience = "api1",
            ValidateLifetime = true
        };
    });

Set Authority to your IdentityServer URL. The middleware caches the JWKS and does not fetch keys on every request.

What Is the Difference Between JWTs and Reference Tokens?

Aspect JWT (Self-Contained) Reference Token

Format

JSON, signed

Opaque string

Validation

Local, no network call to issuer

Requires introspection endpoint call

Revocation

Difficult, must wait for expiry

Immediate, server-side

Size

Grows with claims

Fixed small size

Performance

Faster validation

Extra network hop per request

Best for

APIs with many consumers

High-security scenarios needing instant revocation

Duende IdentityServer supports both formats. Choose based on your revocation requirements. If you need to revoke access within seconds, use reference tokens. If you need fast, distributed validation, use JWTs with short lifetimes.

Learn more about reference tokens in the Duende docs.

What Are Common JWT Security Mistakes?

  1. Storing JWTs in localStorage. This exposes tokens to cross-site scripting (XSS) attacks. Use HttpOnly cookies or the BFF pattern to keep tokens out of browser JavaScript.
  2. Not validating the signature. A token without signature verification is just a JSON blob anyone can forge. Always verify against the issuer's public key.
  3. Ignoring the aud claim. A token issued for one API should not grant access to another. Validate that the audience matches your API's identifier.
  4. Using long-lived JWTs. Keep access token lifetimes short (5 to 15 minutes). Use refresh tokens for longer sessions. Short lifetimes limit the window of damage from a stolen token.
  5. Trusting the alg header blindly. Always enforce expected algorithms server-side. Algorithm confusion attacks trick validators into using the wrong verification method. Set ValidAlgorithms in your TokenValidationParameters.
  6. Putting sensitive data in the payload. JWTs are signed, not encrypted (unless you use JWE). Anyone who intercepts the token can decode and read the payload. Never include passwords, secrets, or PII that should remain confidential.

How Does Duende IdentityServer Issue JWTs?

Duende IdentityServer issues JWTs as both access tokens and ID tokens. It manages signing key rotation, populates claims via IProfileService, and publishes the JWKS endpoint that API consumers use for validation.

You control which claims appear in access tokens through API scope definitions:

Csharp

using Duende.IdentityServer.Models;

var apiScopes = new List<ApiScope>
{
    new ApiScope("api1", "My API")
    {
        UserClaims = { "email", "department" }
    }
};

When a client requests the api1 scope, IdentityServer includes email and department claims in the access token (provided the user has those claims).

IdentityServer handles signing key rotation through its key management system. Keys rotate on a schedule, and the JWKS endpoint always contains the current and recent keys so APIs can validate tokens signed with either.

When Should You Use JWTs vs Sessions?

  • Use JWTs for API protection. When multiple services or third parties need to validate tokens independently, JWTs let each consumer verify locally without calling your identity provider.
  • Use server-side sessions (cookies) for browser applications. When you control the server and the client is a browser, session cookies are simpler and revocable.
  • Use the BFF pattern to combine both. The browser authenticates with a session cookie to your backend. Your backend attaches a JWT when calling downstream APIs. This gives you the security of cookies in the browser and the flexibility of JWTs for API communication. Learn more in the Duende BFF documentation.

Get Started

Related Articles

Specifications