Guide 13 min read Updated

What is a JWT? A complete developer guide

What is a JWT? A developer guide to JSON Web Tokens: the three-part structure, how authentication works, validation vs verification, and safe implementation.

What is a JWT?

Every API that accepts an Authorization: Bearer <token> header is almost certainly reading a JWT. They’re in Google’s identity platform, GitHub’s OAuth flow, Auth0, Firebase, your company’s internal services - but most developers have spent years using them without ever seeing what’s inside.

A JWT (JSON Web Token, pronounced “jot”) is a compact, self-contained credential. That last word matters: unlike a session ID that points to data stored on a server, a JWT is the data. Everything the server needs to trust a request lives inside the token - encoded, signed, and handed to the client to carry around.

JWT full form and meaning: JWT stands for JSON Web Token. The “JSON” part means the token’s contents are a JSON object; the “Web” part means it is designed for HTTP-based exchange (typically sent in an Authorization header or a cookie); the “Token” part means it is a self-contained credential string. The full form is defined in RFC 7519, and the spec pronounces it “jot”. If you are searching for “JWT meaning” or “JWT full form”: it is a signed JSON object, carried by the client, that proves a claim (usually “who you are” or “what you may access”) without the server needing to look anything up.

The formal definition from RFC 7519:

JSON Web Token (JWT) is an open standard that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.

A more useful mental model: a JWT is a signed note. You write some facts - “this user is admin, this token is valid until 9pm” - seal it in a tamper-evident envelope, and hand it to the client. Later, when they present it back to you, you can verify the envelope wasn’t opened and trust everything inside.

The tamper-evidence comes from the signature. The server signs the token with a secret or private key when issuing it. Anyone can decode and read the payload - JWTs are not encrypted by default - but modifying even a single character breaks the signature, and the server rejects it.

When to use JWTs

JWTs are the right tool in two situations:

Authentication. After a user logs in, you issue a JWT. The client stores it and attaches it to every subsequent request. The server verifies the signature and reads the user’s identity directly from the token - no database lookup required. This is why JWTs are popular in distributed systems: any service with access to the secret key or JWKS endpoint can verify tokens independently, without querying a central session store.

Secure information exchange between services. Sometimes you need to pass a claim between systems - “this webhook came from Stripe,” “this password-reset link is valid for 15 minutes.” A signed JWT is a clean, stateless way to assert those facts without trusting the transport alone.

JWTs are a poor fit when you need instant revocation. A JWT signed with a 1-hour expiry is cryptographically valid for that full hour even if the user logs out. Solutions exist - short expiry + refresh tokens, denylist, token versioning - but they add operational complexity. If you need per-request session invalidation (banking, high-security systems), server-side sessions are the simpler answer.

The three-part structure

A JWT looks like this:

Plain text
        eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYzEyMyJ9.eyJzdWIiOiJ1c2VyXzEyMzQ1IiwibmFtZSI6IkphbmUgU21pdGgiLCJlbWFpbCI6ImphbmVAZXhhbXBsZS5jb20iLCJyb2xlIjoiZW5naW5lZXIiLCJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
      

Three Base64URL-encoded strings separated by dots:

Plain text
        HEADER . PAYLOAD . SIGNATURE
      

Paste any JWT into the JWT Toolkit decoder and you’ll see all three parts instantly.

The header identifies the token type and the algorithm used to sign it.

Plain text
        {
  "alg": "HS256",
  "typ": "JWT",
  "kid": "abc123"
}
      

alg is the most security-sensitive field in the entire token. Common values:

AlgorithmTypeNotes
HS256 / HS384 / HS512Symmetric (HMAC)Same secret signs and verifies
RS256 / RS384 / RS512Asymmetric (RSA)Private key signs, public key verifies
ES256 / ES384 / ES512Asymmetric (ECDSA)Smaller keys than RSA, increasingly preferred
noneNoneNo signature - a known attack vector

The optional kid (key ID) tells the verifier which key to use when the issuer has multiple active signing keys. You’ll see this in production systems that rotate keys without downtime.

Payload

The payload holds the claims - statements about the user or entity the token represents, plus metadata about the token itself.

Plain text
        {
  "sub": "user_12345",
  "name": "Jane Smith",
  "email": "jane@example.com",
  "role": "engineer",
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com",
  "iat": 1700000000,
  "exp": 1700003600,
  "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
      

Claims come in three flavors:

Registered claims are the standardized ones defined in RFC 7519 §4.1. They’re optional by spec but you should treat most as required in practice:

ClaimFull namePurpose
issIssuerWho created the token. Validate this.
subSubjectWho the token represents (usually a user ID)
audAudienceWhich service should accept this token
expExpiration TimeUnix timestamp after which the token is invalid
nbfNot BeforeToken is invalid before this Unix timestamp
iatIssued AtWhen the token was created
jtiJWT IDUnique identifier - useful for replay prevention

Public claims are registered in the IANA JWT Claims Registry. OpenID Connect adds a large set here - email, name, picture, email_verified, given_name, locale, and more.

Private claims are whatever your application defines - role, tenant_id, permissions, org. They work fine, but document them. A token with undocumented claims causes confusion six months later.

Signature

The signature is what makes JWTs trustworthy. For HS256:

Plain text
        HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)
      

For RS256, the signing uses a private key instead of a shared secret:

Plain text
        RSASHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  privateKey
)
      

The server runs this computation when issuing the token and appends the result as the third segment. On verification, it runs the same computation with its known key and compares the two signatures. If they match, the token is genuine and unmodified. If they differ, the token was tampered with, issued by someone else, or the wrong key was used for verification.

This is the entire security model of JWTs. The signature is everything.

How JWT authentication works in practice

Tracing a typical login-and-API-call flow:

1. Login. User submits credentials. The auth server validates them, builds the JWT payload with the user’s identity and claims, signs it with the private key or secret, and returns the token.

2. Storage. The client stores the token somewhere. The options have tradeoffs:

  • Memory (safest): Lost on page reload. Fine for SPAs that re-authenticate on load.
  • HttpOnly cookie: Can’t be read by JavaScript - immune to XSS. Vulnerable to CSRF if you’re not using SameSite. The most secure option for web apps.
  • localStorage: Readable by any script on the page. XSS vulnerabilities can exfiltrate tokens silently. Avoid for tokens with meaningful lifetimes.

3. Request. Client attaches the token to the Authorization header:

Plain text
        Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
      

4. Verification. The server splits the token at the dots. It re-computes the expected signature from the header and payload using its known key, then compares it to the provided signature. If they match, the token is authentic.

5. Claim validation. The server checks: Is exp in the past? Does iss match the expected issuer? Does aud include this service? Is nbf satisfied? Only after every check passes is the request allowed through.

The server stores nothing. No session table, no cache lookup. The entire user session lives inside the token. That’s the stateless architecture that makes JWTs horizontally scalable - any instance of the service can verify any token without coordination.

Validation vs verification - the distinction that matters

Most guides combine these into one step. That’s where production bugs come from.

Verification is cryptographic. It answers: “Did a trusted party sign this token, and has it been modified since?”

Validation is semantic. It answers: “Does this token actually authorize this request, right now, for this service?”

A token can pass verification and still fail validation. Consider: a perfectly valid JWT signed by Google, with aud: https://your-competitor.com. Your signature verification passes - Google’s signing key checks out. But the token was not issued for your service. If you don’t validate aud, you’ve just accepted a token that was meant for someone else.

The correct order:

Plain text
        1. Structural check - is this a parseable JWT?
2. Signature verification - is the signature valid?
3. Claim validation:
   - exp: is it expired?
   - nbf: is it active yet?
   - iss: is the issuer on your allowlist?
   - aud: does the audience include your service?
4. Authorization - do the claims permit this action?
      

The attack that exploits this is called a confused deputy: an attacker takes a legitimately issued token for service A and replays it against service B, which shares the same signing key but doesn’t check aud. The token passes verification. The claims look valid. The attacker gets in.

Signing algorithms compared

HMAC (HS256, HS384, HS512)

HMAC is symmetric: one secret both signs and verifies. This is fast and simple, but creates a key-sharing problem. Every service that needs to verify tokens must know the secret - meaning a compromise of any verifying service compromises the ability to forge tokens for all services.

When to use: monoliths, single-backend APIs, anywhere only one service needs to verify tokens.

When not to use: multi-service architectures where you’d need to share the secret across teams, or scenarios where third parties need to verify your tokens.

Secret requirements: minimum 32 bytes (256 bits) of cryptographically random data. Not a password. Not a UUID. Use openssl rand -base64 32 or equivalent. Shorter secrets are brute-forceable.

RSA (RS256, RS384, RS512)

RSA is asymmetric. The auth server holds a private key and signs tokens with it. The public key is published - often at a JWKS endpoint like /.well-known/jwks.json - and any service can verify tokens without access to the private key.

This is the architecture of every major identity provider. Google, Microsoft, Auth0, Okta, Firebase - they all publish a JWKS URL. Any service in the world can verify their tokens. Only Google can issue new ones.

The tradeoff: RSA keys are large (2048+ bits), and the cryptography is slower than HMAC or ECDSA.

ECDSA (ES256, ES384, ES512)

ECDSA is also asymmetric but uses elliptic curve cryptography. ES256 (P-256 curve) produces the same security guarantees as RS256 with a dramatically smaller key and signature. A 256-bit ECDSA key is roughly equivalent to a 3072-bit RSA key.

For new systems, ES256 is the recommended default: smaller tokens, faster verification, same asymmetric key distribution model as RSA.

Security best practices

These are the decisions that determine whether your JWT implementation will survive contact with reality.

Set expiry. Always. A JWT without exp is valid forever. If it’s ever exposed - in logs, in a URL, in an error message - you have no recovery. Set exp to the minimum lifetime your UX allows. Standard practice: 15 minutes for access tokens, 7-30 days for refresh tokens. Never issue non-expiring access tokens.

Generate secrets properly. For HS256, the secret must be at least 32 bytes of cryptographically random data. Not a password. Not "my-super-secret". An improperly generated secret is the single most common cause of JWT security vulnerabilities in practice.

Plain text
        # Generate a proper HS256 secret
openssl rand -base64 32
      

Validate iss and aud explicitly. Don’t trust that your library validates these by default. Read the documentation. Pass your expected issuer and audience to every verification call.

Hardcode the expected algorithm. Never read the algorithm from the token header and use it for verification. Decide what algorithm you use, hardcode that in your verification logic, and reject anything else.

Plan for key rotation from day one. You will need to rotate your signing keys eventually - security incident, compliance requirement, or just routine hygiene. Build in support for kid (key ID) and a JWKS endpoint from the start. Rotating keys in a system that wasn’t designed for it is painful.

Never log full tokens. Tokens in log files are a common source of credential theft. If you need to log JWT activity, log the jti, the sub, or a hash of the token - never the raw string.

Common mistakes

Trusting alg from the header. The JWT header is attacker-controlled. Early JWT library implementations let the token dictate its own verification algorithm. The attack: if the server uses RS256, the attacker knows the public key (it’s public), sets alg: HS256, and signs the token using the RSA public key as an HMAC secret. The server verifies it as valid. This is the algorithm confusion attack. Your library must have the expected algorithm hardcoded, not taken from the token.

Skipping aud validation. Covered above, but worth repeating because it’s so common. A token signed by your auth server but intended for your analytics service should not be accepted by your payments service, even though the signature is valid.

Storing tokens in localStorage for high-value sessions. localStorage is trivially readable by any JavaScript on the page. A single XSS vulnerability anywhere on your site exposes every token. HttpOnly cookies are the correct default for web applications.

Infinite-lifetime tokens. Tokens without exp, tokens with exp set years in the future, refresh tokens that never expire - these are all silent time bombs. Define a rotation and expiry strategy and enforce it.

Debugging production tokens in unknown tools. This one is behavioral, not technical, but it causes real incidents. An engineer pastes a production user’s token into a random online decoder. The tool makes a server request. The token is now potentially compromised. Use the JWT Toolkit - it never sends your token anywhere. Or decode locally with base64 -d.

Forgetting that JWKS endpoints can be unavailable. If your service verifies tokens by fetching a remote JWKS URL on every request, that service is now unavailable whenever the identity provider’s JWKS endpoint is down. Cache the keys with a reasonable TTL (typically 1 hour) and have a fallback for cache hits.

JWT vs session tokens

Both patterns solve the same problem. The right choice depends on your architecture.

JWTSession token
State locationIn the token (client carries it)On the server (database or cache)
Horizontal scalingNo shared state neededRequires shared session store
Instant revocationHard - must wait for expiry or use denylistEasy - delete from store
User data accessSelf-contained - no DB lookup requiredRequires DB/cache lookup per request
Token sizeLarger (base64 JSON)Small (20-40 char opaque string)
Best forAPIs, microservices, mobile, SSOWeb apps where instant logout is required

The session token has an undeserved reputation for being outdated. For applications where users need to be logged out immediately - banking, healthcare, anything with regulatory requirements around session termination - server-side sessions are the architecturally correct choice. The simplicity of instant revocation is worth the operational overhead of a session store.

JWTs shine in API-first architectures, single sign-on across multiple domains, mobile clients that can’t use cookies reliably, and anywhere you need stateless verification to scale horizontally without coordination overhead.

Neither is universally correct. The right pattern is the one that matches your revocation requirements, your deployment architecture, and your team’s operational capacity to run it correctly.


Continue reading