DevLab
Encoding

JWT Structure and How It Works

Understand JSON Web Tokens: their structure, how to decode them, and when to use (and not use) them.

What is a JWT?

A JSON Web Token (JWT) is a compact, URL-safe token format for transmitting claims between parties. It's most commonly used for authentication and authorization in web APIs.

A JWT looks like: xxxxx.yyyyy.zzzzz — three parts separated by dots. Each part is Base64URL-encoded.

The Three Parts

1. Header — Specifies the token type and signing algorithm:

{"alg": "HS256", "typ": "JWT"}

2. Payload — Contains the claims (statements about the user):

{"sub": "1234567890", "name": "John Doe", "iat": 1516239022, "exp": 1516242622}

Standard claims include: sub (subject), iat (issued at), exp (expiry), iss (issuer), aud (audience).

3. Signature — Verifies the token hasn't been tampered with:

HMACSHA256(base64url(header) + "." + base64url(payload), secret)

Decoding vs. Verifying

Anyone can decode a JWT — the payload is just Base64, not encrypted. Decoding is just reading the data. Verifying means checking the signature against a secret key to ensure the token is legitimate and hasn't been modified. Client-side decoding is fine for reading claims; never skip server-side verification for security decisions.

Common JWT Pitfalls

  • Don't store sensitive data in the payload — it's readable by anyone with the token
  • Always verify the signature server-side before trusting any claim
  • Check the exp claim — expired tokens should be rejected
  • Use short expiry times — JWTs can't be invalidated after issue (unless you maintain a denylist)
  • Don't store JWTs in localStorage — use httpOnly cookies to prevent XSS attacks

Frequently Asked Questions

Can a JWT be decoded without the secret key?

Yes. The header and payload of a JWT are Base64URL-encoded, not encrypted. Anyone with the token string can decode and read the payload claims. The secret key is only used for signature verification — confirming the token has not been tampered with. This is why you should never put sensitive data (passwords, API keys, personal information) in the JWT payload.

How do I invalidate a JWT before it expires?

JWTs are stateless by design and cannot be invalidated once issued. The common workarounds are: (1) use very short expiry times (15 minutes) combined with refresh tokens, (2) maintain a server-side denylist of revoked token IDs (jti claim), or (3) rotate the signing secret, which invalidates all tokens at once. If you need immediate revocation frequently, consider using session tokens instead.

Where should I store JWTs on the client?

Store JWTs in httpOnly, Secure, SameSite cookies — never in localStorage or sessionStorage. localStorage is accessible to any JavaScript on the page, making tokens vulnerable to XSS attacks. httpOnly cookies cannot be read by JavaScript, and the Secure flag ensures they are only sent over HTTPS. The SameSite attribute provides additional CSRF protection.

What is the difference between HS256 and RS256 JWT algorithms?

HS256 (HMAC-SHA256) uses a shared secret — both the issuer and verifier need the same key. RS256 (RSA-SHA256) uses a public/private key pair — the issuer signs with a private key and verifiers check with the public key. RS256 is preferred for distributed systems (microservices) because you can share the public key without exposing the signing key. HS256 is simpler and faster for single-server setups.

What claims should I include in a JWT payload?

At minimum, include: sub (subject — user ID), iat (issued at — creation time), and exp (expiration time). For multi-tenant systems, add iss (issuer) and aud (audience) to prevent tokens from being used across services. Add a jti (JWT ID) if you need to implement a revocation denylist. Avoid including large amounts of data — every claim increases the token size and gets sent with every request.

Practice with these tools

More Learning Topics

RegexRegex Basics: A Complete Beginner's GuideRegexRegex Special Characters: Complete ReferenceRegexRegex Groups and Captures ExplainedRegexRegex Quantifiers: Complete GuideCSSCSS Selectors: The Complete GuideCSSCSS Specificity: Why Your Styles Aren't ApplyingJSONJSONPath Syntax: Query JSON Like XPathTimeUnix Timestamps ExplainedEncodingBase64 Encoding ExplainedEncodingJWT vs Session Tokens: Which Should You Use?EncodingJWT Refresh Tokens ExplainedCryptoHash Functions Explained: MD5, SHA-256, and When to Use EachEncodingURL Encoding Explained: What %20 Actually MeansJSONJSON Schema Explained: Validate Your JSON DataJSONJSON vs YAML: Which Should You Use?JSONJSON.stringify and JSON.parse: Edge Cases You Should KnowRegexRegex Lookahead and Lookbehind: Match Without ConsumingRegexRegex for Email Validation: The Right ApproachCSSThe CSS Box Model: margin, padding, border, and contentCSSFlexbox vs CSS Grid: When to Use EachCSSCSS Custom Properties (Variables) ExplainedTimeISO 8601 Explained: The Right Way to Format DatesTimeUnix Timestamps vs ISO 8601: Which to Use in Your API?EncodingUTF-8 Explained: How Computers Store TextTextCORS Explained: Why Your API Call is BlockedTextHTTP Status Codes: A Practical Developer GuideRegexNamed Capture Groups in Regex: Clean ExtractionsColorsColor Spaces Explained: RGB, HSL, HEX, and BeyondColorsColor Contrast for Developers: WCAG Rules and How to CheckNumbersNumber Bases Explained: Binary, Octal, Decimal, and HexNumbersBitwise Operations for Web DevelopersHTMLHTML Semantic Elements: A Complete ReferenceTextText Encoding for Developers: ASCII, UTF-8, and UnicodeRegexWhat is Regex? Complete Guide for DevelopersJSONJSON Format Explained: Structure, Syntax, and Common ErrorsEncodingHow JWT Works: Header, Payload, Signature DecodedHTMLHow to Convert Any Website to Markdown (for LLMs, RAG & Docs)TextPreparing Website Content for RAG: Clean Markdown Pipelines