Skip to main content
C
CodeUtil

JWT Tokens Explained - Authentication for Modern Web Apps

Understand JSON Web Tokens (JWT) from the ground up: how they work, their three-part structure, when to use them, security best practices, refresh token strategies, and common implementation mistakes to avoid.

2024-12-0614 min
Related toolJWT Decoder

Use the tool alongside this guide for hands-on practice.

My love-hate relationship with JWTs

I'll be honest: I used to hate JWTs. They seemed overcomplicated compared to simple session cookies. Then I built my first microservices architecture at Šikulovi s.r.o. and suddenly understood why everyone uses them. When you have five services that all need to verify a user is authenticated, sharing session state becomes a nightmare. JWTs solve that problem elegantly.

But here's the thing they don't tell you in the tutorials: JWTs are easy to implement wrong. I've seen tokens with 24-hour expiration (way too long), secrets like 'password123', and payloads containing full user profiles. This guide is everything I wish someone had told me before my first JWT security audit.

Three parts, one token

A JWT looks like gibberish: eyJhbGci...eyJzdWIi...SflKxw. But there's a structure to it. Three Base64-encoded parts separated by dots. Header tells you how it's signed, payload contains your data, signature proves nobody tampered with it.

  • Header: Token type and algorithm (e.g., HS256, RS256)
  • Payload: Your claims - user ID, permissions, expiration time
  • Signature: Cryptographic proof the token is valid
  • Format: header.payload.signature
  • IMPORTANT: Encoded, not encrypted - anyone can read the payload

The header (first part)

The header is just JSON saying what type of token this is and how it's signed. After all the stuff I said about encryption - the header is just base64 encoded. Anyone can read it. The only interesting parts are the algorithm (HS256, RS256) and optionally a key ID if you're doing key rotation.

  • typ: Always "JWT" (I have never seen anything else)
  • alg: The algorithm - HS256 for shared secret, RS256 for public/private keys
  • kid: Key ID - useful when you rotate signing keys
  • CRITICAL: Never accept alg: "none" - this disables signature verification

The payload (middle part)

This is where your actual data lives. The spec calls these 'claims' - statements about the user. Some claims are standardized (sub, exp, iat), others you make up. Remember: this is readable by anyone. I once saw a JWT with the user's SSN in it. Don't be that developer.

  • iss (issuer): Who created this token
  • sub (subject): Who this token is about (usually user ID)
  • aud (audience): Who should accept this token
  • exp (expiration): When this token dies - ALWAYS SET THIS
  • iat (issued at): When the token was created
  • nbf (not before): Token is invalid before this time
  • jti (JWT ID): Unique ID for revocation tracking
  • Custom claims: Your app-specific data (roles, permissions)

The signature (third part)

The signature is what makes JWTs secure. It's created by signing the header and payload with a secret (HMAC) or private key (RSA). If anyone modifies the payload, the signature won't match. If they don't have the secret, they can't create a valid signature.

  • HMAC: Sign with shared secret, verify with same secret
  • RSA: Sign with private key, verify with public key
  • Signature proves integrity, not confidentiality
  • Invalid signature = reject immediately, no exceptions

The authentication flow I use

This is the flow I implement for most projects. Simple, standard, works everywhere. The key insight is that after login, the server never needs to look up the token in a database - it just verifies the signature and reads the claims.

  • 1. User sends username/password to /login
  • 2. Server validates against database
  • 3. Server creates JWT with user claims, signs it
  • 4. Server returns JWT to client
  • 5. Client stores JWT (I prefer HttpOnly cookies)
  • 6. Client sends JWT in Authorization: Bearer <token>
  • 7. Server verifies signature, reads claims
  • 8. Server grants/denies access based on claims

When JWTs actually make sense

JWTs are not always the answer. I see people using them for simple login forms where session cookies would be simpler. Here's when I actually reach for JWTs:

  • Microservices: Multiple services need to verify auth independently
  • SSO: One login works across several apps
  • Mobile apps: Cookies are awkward, tokens are natural
  • Cross-domain: When cookies face restrictions
  • Stateless APIs: No session storage needed

When I don't use JWTs

Yep, sometimes I recommend against JWTs. For a simple server-rendered app with one database, session cookies are simpler and arguably more secure. JWTs add complexity, and complexity breeds bugs.

  • Simple web apps: Server sessions are just easier
  • Need instant logout: JWT revocation is a pain
  • Long sessions needed: Long-lived JWTs are risky
  • You don't understand the security implications: Sessions are safer defaults

Access tokens vs refresh tokens

Modern JWT implementations use two types of tokens: short-lived access tokens for API requests and long-lived refresh tokens to obtain new access tokens. This pattern balances security with user experience.

  • Access token: Short-lived (5-15 minutes), used for API authentication
  • Refresh token: Longer-lived (days to weeks), used only to get new access tokens
  • Access tokens are sent with every request—short lifetime limits exposure
  • Refresh tokens are stored securely and used infrequently
  • When the access token expires, the client uses the refresh token to get a new one
  • Refresh token rotation: Issue a new refresh token with each use
  • If a refresh token is stolen, it can only be used until the next rotation

Refresh token flow

The refresh token flow extends user sessions without requiring re-authentication while maintaining security through short-lived access tokens.

  • 1. User logs in and receives both access token and refresh token
  • 2. Client uses access token for API requests
  • 3. Access token expires (after 5-15 minutes)
  • 4. Client sends refresh token to the token refresh endpoint
  • 5. Server verifies refresh token (check signature, expiration, revocation)
  • 6. Server issues new access token (and optionally rotates refresh token)
  • 7. Client continues using the new access token
  • 8. If refresh token is invalid or expired, user must re-authenticate

Storing JWTs securely

Where you store JWTs significantly impacts your application's security. Each storage option has trade-offs between security and convenience.

  • HttpOnly cookies: Protected from JavaScript, vulnerable to CSRF (use CSRF tokens)
  • localStorage: Accessible to JavaScript, vulnerable to XSS attacks
  • sessionStorage: Same as localStorage but clears when browser closes
  • Memory only: Most secure but lost on page refresh
  • For web apps: HttpOnly cookies with SameSite=Strict and CSRF protection
  • For SPAs: Consider memory storage with silent refresh
  • Never store tokens in URLs—they appear in browser history and server logs

JWT security: The "none" algorithm attack

One of the most critical JWT vulnerabilities is the "none" algorithm attack. Some JWT libraries accept tokens with "alg": "none", which means no signature verification.

  • Attack: Attacker changes header to {"alg": "none"} and removes signature
  • Vulnerable servers accept the token without verification
  • Always explicitly specify allowed algorithms when verifying
  • Never accept "none" as a valid algorithm in production
  • Configure your JWT library to reject unsigned tokens
  • Example (Node.js): jwt.verify(token, secret, { algorithms: ["HS256"] })
  • This attack has compromised real applications—take it seriously

JWT security: Algorithm confusion attacks

Algorithm confusion occurs when an attacker tricks the server into using a different algorithm than intended, particularly switching between symmetric (HMAC) and asymmetric (RSA) algorithms.

  • Attack scenario: Server expects RS256 (RSA) but accepts HS256 (HMAC)
  • Attacker signs token with HMAC using the public RSA key as the secret
  • Server mistakenly verifies the HMAC signature with its public key
  • Prevention: Always specify the expected algorithm in verification
  • Use separate keys for different algorithms
  • Keep RSA public keys out of reach if possible
  • Validate the "alg" header matches expected value before verification

JWT security: Token expiration and rotation

Proper token lifecycle management is essential for JWT security. Tokens that never expire or live too long create significant security risks.

  • Set reasonable expiration times (5-15 minutes for access tokens)
  • Include "exp" claim in every token—never issue tokens without expiration
  • Verify expiration on every request, not just at initial authentication
  • Implement refresh token rotation to detect token theft
  • Consider "jti" (JWT ID) for token revocation tracking
  • Reject tokens with "exp" too far in the future
  • Handle clock skew with small tolerance (e.g., 30 seconds)

Revoking JWTs: The stateless dilemma

JWTs are stateless by design—the server does not track issued tokens. This creates a challenge when you need to invalidate tokens (user logout, password change, security breach).

  • Problem: Valid JWTs work until they expire, even after logout
  • Token blacklist: Store revoked token IDs in a database or cache
  • Short expiration: Minimize damage window with 5-15 minute tokens
  • Refresh token revocation: Invalidate refresh tokens to prevent new access tokens
  • Version claim: Include a "token version" and increment it on logout
  • Redis for blacklists: Fast lookups with automatic expiration
  • Trade-off: Revocation adds state, reducing the benefits of stateless JWTs

Common JWT implementation mistakes

JWT implementations often contain security vulnerabilities due to misunderstanding how tokens work. Avoid these common mistakes.

  • Storing sensitive data in the payload (it is not encrypted)
  • Using weak secrets (use at least 256 bits of entropy for HMAC)
  • Not validating all claims (exp, iss, aud, nbf)
  • Accepting any algorithm instead of whitelisting expected ones
  • Storing tokens in localStorage without XSS protection
  • Not implementing refresh token rotation
  • Setting expiration times too long (hours or days for access tokens)
  • Trusting the client to handle token expiration correctly

Choosing between HMAC and RSA

JWTs can be signed with symmetric algorithms (HMAC) or asymmetric algorithms (RSA, ECDSA). The choice depends on your architecture and security requirements.

  • HMAC (HS256/HS384/HS512): Same secret for signing and verification
  • RSA (RS256/RS384/RS512): Private key signs, public key verifies
  • HMAC: Simpler, faster, but secret must be shared with verifiers
  • RSA: More complex, but verifiers only need the public key
  • Use HMAC when: Single service issues and verifies tokens
  • Use RSA when: Multiple services verify tokens from a central issuer
  • ECDSA (ES256): Similar to RSA but with smaller keys and signatures
  • Never share HMAC secrets across trust boundaries

JWT best practices summary

Following these best practices will help you implement JWTs securely. Security requires attention to detail—one mistake can compromise your entire authentication system.

  • Always verify the signature before trusting any claims
  • Whitelist allowed algorithms—never accept arbitrary algorithms
  • Use short expiration times for access tokens (5-15 minutes)
  • Implement refresh token rotation for long sessions
  • Store tokens securely (HttpOnly cookies or memory)
  • Validate all relevant claims (exp, iss, aud, nbf)
  • Use strong secrets (256+ bits for HMAC)
  • Never store sensitive data in the payload
  • Implement token revocation for logout and security events
  • Keep your JWT library updated to patch vulnerabilities

JWT libraries by language

Use established, well-maintained libraries for JWT handling. Never implement JWT signing or verification yourself—the cryptographic details are easy to get wrong.

  • JavaScript/Node.js: jsonwebtoken (most popular), jose (modern, full-featured)
  • Python: PyJWT, python-jose, Authlib
  • PHP: firebase/php-jwt, lcobucci/jwt
  • Java: jjwt, Nimbus JOSE+JWT, Auth0 java-jwt
  • Go: golang-jwt/jwt, lestrrat-go/jwx
  • Ruby: ruby-jwt
  • C#/.NET: System.IdentityModel.Tokens.Jwt, Microsoft.AspNetCore.Authentication.JwtBearer
  • Rust: jsonwebtoken, frank_jwt

Debugging JWTs

When troubleshooting JWT issues, you need to inspect the token contents. Remember that anyone can decode a JWT—only the signature provides security.

  • Use jwt.io to decode and inspect tokens (do not paste production tokens)
  • Check the "exp" claim to ensure the token has not expired
  • Verify the "iss" and "aud" claims match your configuration
  • Confirm the algorithm in the header matches what your server expects
  • Look for clock skew issues between servers
  • Test with our JWT Decoder tool for quick inspection
  • In development, log verification errors with full details
  • Never log full tokens in production—they are credentials

Conclusion

JWTs provide a powerful mechanism for stateless authentication in modern web applications. Their self-contained nature makes them ideal for microservices, APIs, and single sign-on scenarios. However, JWTs require careful implementation to be secure.

Remember: JWTs are encoded, not encrypted—anyone can read the payload. Security comes from the signature verification and proper claim validation. Use short-lived access tokens with refresh token rotation, store tokens securely, and always verify signatures with explicit algorithm whitelists. Use our JWT Decoder tool to understand token structure and debug authentication issues.

FAQ

What does JWT stand for?

JWT stands for JSON Web Token. It is an open standard (RFC 7519) for securely transmitting information between parties as a JSON object. JWTs are commonly used for authentication and information exchange in web applications.

Is the JWT payload encrypted?

No, the JWT payload is encoded (Base64URL), not encrypted. Anyone with access to the token can decode and read the payload contents. Never store sensitive information like passwords or credit card numbers in JWT payloads. If you need encrypted tokens, use JWE (JSON Web Encryption).

How long should a JWT access token live?

Access tokens should have short lifetimes, typically 5-15 minutes. This limits the damage if a token is stolen—the attacker has a small window to use it. Use refresh tokens (with longer lifetimes) to issue new access tokens without requiring users to log in again.

What is the difference between access tokens and refresh tokens?

Access tokens are short-lived tokens (minutes) sent with every API request to authenticate the user. Refresh tokens are longer-lived tokens (days to weeks) stored securely and used only to obtain new access tokens when the current one expires. This separation improves security by limiting exposure of the frequently-used access token.

Can JWTs be revoked or invalidated?

JWTs are stateless by design and cannot be directly revoked. To invalidate tokens, you need additional infrastructure: a token blacklist (stored in Redis or a database), short expiration times to minimize the revocation window, or refresh token revocation to prevent new access tokens. Each approach adds some state to your otherwise stateless system.

Should I store JWTs in localStorage or cookies?

For web applications, HttpOnly cookies with SameSite=Strict are generally more secure because JavaScript cannot access them, protecting against XSS attacks. localStorage is vulnerable to XSS but easier to implement for APIs. For SPAs, consider storing tokens in memory only with silent refresh mechanisms. Never store tokens in URLs.

What is the "none" algorithm vulnerability?

The "none" algorithm attack exploits JWT libraries that accept tokens with {"alg": "none"}, which means no signature. Attackers can forge tokens without knowing the secret. Always configure your JWT library to reject the "none" algorithm and explicitly whitelist only the algorithms you expect (e.g., ["HS256"] or ["RS256"]).

When should I use RS256 vs HS256?

Use HS256 (HMAC) when a single service both issues and verifies tokens—it is simpler and faster. Use RS256 (RSA) when multiple services need to verify tokens but only one service should issue them—verifiers only need the public key, keeping the private signing key secure. RS256 is common in OAuth/OpenID Connect scenarios.

Martin Šikula

Founder of CodeUtil. Web developer building tools I actually use. When I'm not coding, I experiment with productivity techniques (with mixed success).

Related articles