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)

🔬 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
: Issueraud
: Audienceexp
: Expiration (Unix timestamp)iat
: Issued atnbf
: 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:
- Signature verification
- Using the public key (for RS256)
- Ensures the token is from a trusted source
- Expiration check (
exp
)- Reject if token is expired
- Issuer and audience (
iss
,aud
)- Match expected values
- 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!