Inside JSON Web Tokens (JWT): Anatomy, Validation & Expiry Pitfalls

Share this

JSON Web Tokens (JWT) have become the go-to format for access and ID tokens in modern authentication systems — especially when using OAuth 2.0 or OpenID Connect.
Note: This article belongs to Part 4.2: Token Lifecycle & Retry Logic in our Application Security series.

The truth behind those “self-contained” tokens we love to hate

They’re compact, self-contained, and easy to pass around. But with great power comes great confusion.

  • What exactly is inside a JWT?
  • How are they validated?
  • Why do expired or invalid tokens sometimes still “work”?
  • And is stateless always the best choice?

In this post, we’re unboxing JWTs to understand how they work, how to validate them properly, and where things tend to go wrong — especially in production environments.

What is a JWT?

A JSON Web Token (JWT) is a compact, URL-safe token format used for:

  • Authentication (ID tokens)
  • Authorization (access tokens)
  • Information exchange (claims)

A JWT consists of three parts, separated by dots:

<Header>.<Payload>.<Signature>

Example (shortened for clarity):

eyJhbGciOiJIUzI1NiIs... (header)
eyJzdWIiOiIxMjM0NTY3ODkw... (payload)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c (signature)
Application Security Series
Application Security Series

🔬 JWT Anatomy: Header, Payload & Signature

1. Header

Describes the token type and signing algorithm:

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

⚠️ Always prefer asymmetric algorithms (RS256) over symmetric (HS256) for access tokens.

2. Payload

Contains claims (info about the user, app, or token). Example:

{
"sub": "user123",
"iss": "https://login.example.com",
"aud": "my-api",
"exp": 1719852000,
"scope": "read:profile write:settings"
}

Common claims:

  • sub: Subject (user ID)
  • iss: Issuer
  • aud: Audience
  • exp: Expiration (Unix timestamp)
  • iat: Issued at
  • nbf: Not before
  • Custom claims: scopes, roles, org, etc.

3. Signature

The result of signing the header and payload using a private key (RS256) or secret (HS256). This ensures the token hasn’t been tampered with.

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

🔐 JWT Validation Flow

When a backend receives a JWT, it must validate it thoroughly:

  1. Signature verification
    • Using the public key (for RS256)
    • Ensures the token is from a trusted source
  2. Expiration check (exp)
    • Reject if token is expired
  3. Issuer and audience (iss, aud)
    • Match expected values
  4. Other claim validation
    • nbf, iat, scope, etc.

Spring Security handles this under the hood with JwtDecoder, but it’s critical to understand that validation isn’t optional — it’s your last line of defense.

🧨 Expiry Pitfalls

1. Clock Skew Issues

Different systems may have slightly different clocks. If your server is ahead of the token issuer, tokens may appear invalid even though they’re fresh.

✅ Solution: Use a small grace window (30–60 seconds) for exp, nbf, and iat.

2. Stale Tokens Accepted

Some systems decode JWTs but forget to verify the signature. Anyone can modify the payload and re-encode it — without verification, you’re just trusting user input.

❌ Never trust a decoded JWT unless it’s verified!

3. Misuse as Session Tokens

JWTs are often used for stateless session management — but long-lived tokens without rotation mean no way to revoke access (even after logout).

✅ Use short-lived access tokens and refresh tokens to maintain sessions.

🧰 Working with JWTs in Spring Security

Spring Boot makes handling JWTs easy using spring-boot-starter-oauth2-resource-server.

spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://your-idp.com

Behind the scenes:

  • It downloads the public key (.well-known/jwks.json)
  • Validates tokens automatically
  • Maps claims to security context

Custom claim mapping example:

@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
var converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(new MyCustomRoleMapper());
return converter;
}

Best Practices for JWTs

✅ Always verify the signature using a trusted key
✅ Keep access tokens short-lived (5–15 mins)
✅ Use refresh tokens with rotation for long-lived sessions
✅ Validate exp, iss, aud, nbf, and iat claims
✅ Prefer RS256 or ES256 over HS256
✅ Avoid putting sensitive data in JWT payloads — they’re just base64-encoded, not encrypted

When Not to Use JWTs

JWTs are great, but not always necessary. Avoid them:

  • For simple session-based apps (use cookies + session store instead)
  • If you need easy revocation (consider opaque tokens)
  • If you don’t control your audience list (confused deputy problem)

Conclusion

JWTs are powerful tools — but they’re not magic. When used wisely, they offer performance and portability. When misused, they create invisible vulnerabilities that are hard to trace.

In this post, you’ve seen:

  • How JWTs are built and validated
  • What makes them expire (and fail)
  • And how to avoid common traps in real-world deployments

Check out https://en.wikipedia.org/wiki/JSON_Web_Token Wikipedia link for more information on the current topic.

What’s Next

Up Next in our Application Security Series is:
Part 5.1 👉 PKCE and the Death of Implicit Flow: How to Secure SPAs & Mobile Apps

We’ll explore why PKCE is the preferred approach for browser-based apps — and why implicit flow is fading into history.

Have a Question?

Still confused about signing algorithms? Want help validating tokens in Spring Boot? Curious how refresh tokens pair with JWTs? Drop a comment — happy to chat!

Share this

Leave a comment

Your email address will not be published. Required fields are marked *