August 26, 2026
JWT Authentication Is Not Just Three Strings: How Spring Security Really Validates Your Token — and…
A deep dive into JWT authentication, Spring Security internals, token attacks, and production-grade defenses for Java backend developers
By Hitesh Laxman
18 min read
A deep dive into JWT authentication, Spring Security internals, token attacks, and production-grade defenses for Java backend developers
JWT authentication looks simple: a header, payload, and signature.
But production JWT security is much more than decoding three Base64URL strings.
This guide explains what actually happens inside Spring Security when a JWT reaches a Spring Boot API — from BearerTokenAuthenticationFilter and JwtAuthenticationProvider to signature verification, issuer and audience validation, authorization, key rotation, and the SecurityContext.
It also explores how attackers target JWT implementations through token theft, replay attacks, algorithm confusion, weak secrets, incorrect audience validation, manipulated kid values, and token leakage — and how to design a stronger production implementation.
You log into an application.
You receive something that looks like this:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NSIsInJvbGUiOiJVU0VSIiwiZXhwIjoxNzYwMDAwMDAwfQ.
signature...eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NSIsInJvbGUiOiJVU0VSIiwiZXhwIjoxNzYwMDAwMDAwfQ.
signature...You put it into:
Authorization: Bearer <token>Authorization: Bearer <token>The API accepts your request.
It feels almost magical.
But here is the important question:
What actually happens between the moment the API receives that token and the moment your controller executes?
And more importantly:
What happens if an attacker changes role=user to role=admin?
What if they change:
"alg": "RS256""alg": "RS256"to something else?
What if they steal a valid token?
What if the token is expired?
What if the token was issued by another application?
What if the aud claim is wrong?
What if the attacker manipulates kid?
What if your JWT signing secret is weak?
What if the JWT is valid but was issued for a completely different API?
This is where JWT security becomes much more interesting than:
"JWT consists of header, payload and signature."
This article goes inside the entire authentication pipeline.
1. First: What JWT Actually Is
JWT stands for JSON Web Token.
A JWT is a compact representation of claims that can be digitally signed and, in some designs, encrypted.
A signed JWT normally looks like:
HEADER.PAYLOAD.SIGNATUREHEADER.PAYLOAD.SIGNATUREFor example:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NSIsInNjb3BlIjoicmVhZCB3cml0ZSIsImV4cCI6MTc2MDAwMDAwMH0
.
MEUCIQ...eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NSIsInNjb3BlIjoicmVhZCB3cml0ZSIsImV4cCI6MTc2MDAwMDAwMH0
.
MEUCIQ...There are three components:
+--------------------+
| Header |
+--------------------+
.
+--------------------+
| Claims / Payload |
+--------------------+
.
+--------------------+
| Signature |
+--------------------++--------------------+
| Header |
+--------------------+
.
+--------------------+
| Claims / Payload |
+--------------------+
.
+--------------------+
| Signature |
+--------------------+The first important misconception:
JWT is not encrypted by default.
The header and payload are Base64URL encoded.
They are not secret.
Anyone who obtains the token can usually decode them.
So never put:
{
"password": "MyPassword123"
}{
"password": "MyPassword123"
}inside a JWT.
JWT signatures provide integrity/authenticity for signed JWTs; they do not automatically provide confidentiality. OWASP explicitly distinguishes signed JWTs from encrypted JWTs.
2. What Is Inside the Header?
A typical header looks like:
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-2026-01"
}{
"alg": "RS256",
"typ": "JWT",
"kid": "key-2026-01"
}Important fields include:
alg
The signing algorithm.
Examples:
RS256
RS384
RS512
ES256
ES384
ES512
HS256RS256
RS384
RS512
ES256
ES384
ES512
HS256typ
Token type.
Usually:
"typ": "JWT""typ": "JWT"kid
Key ID.
This allows the verifier to identify which public key should be used to verify the token.
For example:
"kid": "prod-key-2026-08""kid": "prod-key-2026-08"This becomes particularly important during key rotation.
And it can also become a security problem if developers blindly trust attacker-controlled key-selection data.
RFC 8725 recommends explicit algorithm verification and warns about blindly trusting values such as kid, jku, and x5u.
3. What Is Inside the Payload?
The payload contains claims.
Example:
{
"iss": "https://auth.example.com",
"sub": "user-123",
"aud": "orders-api",
"exp": 1780000000,
"iat": 1779996400,
"nbf": 1779996400,
"jti": "7f3e...",
"scope": "orders.read orders.write",
"roles": [
"USER"
]
}{
"iss": "https://auth.example.com",
"sub": "user-123",
"aud": "orders-api",
"exp": 1780000000,
"iat": 1779996400,
"nbf": 1779996400,
"jti": "7f3e...",
"scope": "orders.read orders.write",
"roles": [
"USER"
]
}Let's understand the important claims.
4. The iss Claim — Who Issued This Token?
"iss": "https://auth.example.com""iss": "https://auth.example.com"iss means issuer.
It answers:
Who created this token?
Imagine your company has:
auth.company.com
orders.company.com
payments.company.comauth.company.com
orders.company.com
payments.company.comThe Orders API should not simply ask:
"Is this JWT correctly signed?"
It should also ask:
"Was this token issued by the identity provider I trust?"
That is why issuer validation matters.
Spring Security's resource server configuration validates the issuer when configured with issuer-uri.
5. The sub Claim — Who Is This Token About?
"sub": "user-123""sub": "user-123"sub means subject.
It normally identifies the principal.
For example:
sub = 12345sub = 12345could represent:
Customer ID = 12345Customer ID = 12345Do not confuse:
subsubwith:
usernameusernameThe subject is an identifier.
Your application decides what that identifier represents.
6. The aud Claim — Who Is Allowed to Accept This Token?
This is one of the most frequently overlooked claims.
Suppose a token was issued for:
"aud": "payments-api""aud": "payments-api"An attacker might try to send that token to:
orders-apiorders-apiIf Orders API validates only the signature and expiration, it might accidentally accept a token intended for another service.
That is a token substitution / cross-service confusion problem.
The API should validate:
iss
audiss
audnot just:
signaturesignatureSpring Security supports audience validation in resource-server configuration.
7. exp — When Does the Token Expire?
Example:
"exp": 1780000000"exp": 1780000000This is an expiration timestamp.
Once:
current_time > expcurrent_time > expthe token should be rejected.
Short-lived access tokens dramatically reduce the useful lifetime of a stolen token.
For example:
Access token: 5–15 minutes
Refresh token: much longerAccess token: 5–15 minutes
Refresh token: much longerThere is no universal magic number.
The correct lifetime depends on:
- sensitivity of the API
- risk profile
- refresh architecture
- user experience
- ability to revoke
- token storage
- threat model
Spring Security validates exp and nbf by default in its JWT resource-server setup.
8. iat — When Was the Token Created?
"iat": 1779996400"iat": 1779996400iat means:
Issued At
It can help with:
- auditing
- token age
- detecting unusual tokens
- session invalidation strategies
But do not assume iat alone makes a token safe.
9. nbf — Not Before
"nbf": 1779996400"nbf": 1779996400This means:
Do not accept this token before this time.
It can be useful when tokens are created ahead of their activation time.
But distributed systems have clock differences.
Therefore, small clock-skew allowances may be appropriate.
Spring Security provides timestamp validation and supports configurable clock skew.
10. jti — Token Identifier
"jti": "7f3e6d8c...""jti": "7f3e6d8c..."jti identifies the token.
It becomes useful for:
- revocation
- deny lists
- replay detection
- incident investigation
For example:
User reports account compromise
↓
Invalidate session
↓
Add jti to revocation store
↓
Future requests rejectedUser reports account compromise
↓
Invalidate session
↓
Add jti to revocation store
↓
Future requests rejectedBut remember something important:
JWT is not automatically revocable.
This is one of the biggest trade-offs of JWT.
OWASP notes that JWT-based sessions require additional mechanisms if immediate invalidation is needed.
11. Now the Most Important Part: The Signature
Suppose the header and payload are:
HEADER.PAYLOADHEADER.PAYLOADThe issuer calculates:
SIGN(
HEADER.PAYLOAD,
PRIVATE_KEY
)SIGN(
HEADER.PAYLOAD,
PRIVATE_KEY
)The result becomes:
HEADER.PAYLOAD.SIGNATUREHEADER.PAYLOAD.SIGNATUREThe API receives all three.
It then verifies:
Verify(
HEADER.PAYLOAD,
SIGNATURE,
PUBLIC_KEY
)Verify(
HEADER.PAYLOAD,
SIGNATURE,
PUBLIC_KEY
)If someone modifies:
"role": "USER""role": "USER"to:
"role": "ADMIN""role": "ADMIN"the signature no longer matches.
The attacker cannot simply change the payload.
They need a valid signature generated with the appropriate signing key.
That is the fundamental security property of a signed JWT.
12. RS256 vs HS256
This is an important architecture decision.
HS256
Uses a shared secret.
Issuer
|
| shared secret
|
+---- API 1
|
+---- API 2
|
+---- API 3Issuer
|
| shared secret
|
+---- API 1
|
+---- API 2
|
+---- API 3Every service that verifies the token knows the secret.
But that means every service can potentially create valid tokens too.
If one service is compromised, the shared secret may compromise the trust boundary of every service using it.
OWASP highlights this exact trade-off with MAC-based JWTs.
13. RS256
RS256 uses asymmetric cryptography.
The issuer has:
Private KeyPrivate KeyThe resource server has:
Public KeyPublic KeyFlow:
Authorization Server
|
| private key
↓
Sign JWT
|
↓
JWT
|
↓
Resource Server
|
| public key
↓
Verify JWTAuthorization Server
|
| private key
↓
Sign JWT
|
↓
JWT
|
↓
Resource Server
|
| public key
↓
Verify JWTThe resource server cannot create valid RS256 tokens using only the public key.
This is one reason asymmetric signing is attractive in distributed microservice architectures.
14. The Real Authentication Flow
Now let's follow one request.
Imagine:
GET /api/orders
Authorization: Bearer eyJ...GET /api/orders
Authorization: Bearer eyJ...The request reaches:
Load Balancer
↓
API Gateway
↓
Spring Boot ApplicationLoad Balancer
↓
API Gateway
↓
Spring Boot ApplicationInside Spring Security, the request enters the security filter chain.
Conceptually:
HTTP Request
↓
SecurityFilterChain
↓
BearerTokenAuthenticationFilter
↓
AuthenticationManager
↓
JwtAuthenticationProvider
↓
JwtDecoder
↓
Signature Verification
↓
Claim Validation
↓
JwtAuthenticationToken
↓
SecurityContextHolder
↓
ControllerHTTP Request
↓
SecurityFilterChain
↓
BearerTokenAuthenticationFilter
↓
AuthenticationManager
↓
JwtAuthenticationProvider
↓
JwtDecoder
↓
Signature Verification
↓
Claim Validation
↓
JwtAuthenticationToken
↓
SecurityContextHolder
↓
ControllerSpring Security documents this flow explicitly.
15. Step 1 — Extract the Bearer Token
The client sends:
Authorization: Bearer <JWT>Authorization: Bearer <JWT>Spring Security's:
BearerTokenAuthenticationFilterBearerTokenAuthenticationFilterextracts the bearer token from the request.
The filter creates a:
BearerTokenAuthenticationTokenBearerTokenAuthenticationTokenand sends it into the authentication process.
16. Step 2 — AuthenticationManager
The token is passed to:
AuthenticationManagerAuthenticationManagerUsually this is backed by:
ProviderManagerProviderManagerThe provider manager delegates authentication to the appropriate:
AuthenticationProviderAuthenticationProviderFor JWT authentication, that provider is:
JwtAuthenticationProviderJwtAuthenticationProvider17. Step 3 — JwtAuthenticationProvider
The provider receives the bearer token.
It delegates JWT processing to:
JwtDecoderJwtDecoderThe decoder:
- Parses the token.
- Decodes the header.
- Decodes the claims.
- Determines the key.
- Verifies the cryptographic signature.
- Applies claim validation.
Spring Security's documentation describes JwtAuthenticationProvider as using JwtDecoder and JwtAuthenticationConverter for this process.
18. Step 4 — Where Does the Public Key Come From?
With:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.comspring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.comSpring Security can discover the authorization server metadata and JWKS endpoint.
Conceptually:
Resource Server
|
| discovery
↓
Authorization Server Metadata
|
↓
JWKS URI
|
↓
Public KeysResource Server
|
| discovery
↓
Authorization Server Metadata
|
↓
JWKS URI
|
↓
Public KeysThe JWKS endpoint contains public keys.
For example:
{
"keys": [
{
"kty": "RSA",
"kid": "key-2026",
"use": "sig",
"alg": "RS256"
}
]
}{
"keys": [
{
"kty": "RSA",
"kid": "key-2026",
"use": "sig",
"alg": "RS256"
}
]
}Spring Security can use these keys to validate incoming JWT signatures and can handle signing-key rotation.
19. Step 5 — Signature Verification
Suppose the JWT says:
{
"alg": "RS256",
"kid": "key-2026"
}{
"alg": "RS256",
"kid": "key-2026"
}The resource server finds:
kid = key-2026kid = key-2026and obtains the corresponding public key.
Then:
Verify(
encodedHeader + "." + encodedPayload,
signature,
publicKey
)Verify(
encodedHeader + "." + encodedPayload,
signature,
publicKey
)If verification fails:
401 Unauthorized401 UnauthorizedThe controller should never execute.
20. Step 6 — Claim Validation
A valid signature is not enough.
A production API should validate appropriate claims such as:
iss
aud
exp
nbf
iat
jti
scope / roles
typiss
aud
exp
nbf
iat
jti
scope / roles
typdepending on the application and token profile.
Think about JWT validation as:
Is the token structurally valid?
+
Was it signed correctly?
+
Was it issued by someone I trust?
+
Was it intended for me?
+
Is it currently valid?
+
Is it appropriate for this operation?Is the token structurally valid?
+
Was it signed correctly?
+
Was it issued by someone I trust?
+
Was it intended for me?
+
Is it currently valid?
+
Is it appropriate for this operation?Only then should you trust its identity/authorization information.
RFC 8725 specifically recommends algorithm verification, appropriate algorithms, explicit typing where needed, and defenses against cross-JWT confusion.
21. Step 7 — Convert Claims Into Authorities
Suppose the JWT contains:
{
"scope": "orders.read orders.write"
}{
"scope": "orders.read orders.write"
}Spring Security can map scopes into authorities:
SCOPE_orders.read
SCOPE_orders.writeSCOPE_orders.read
SCOPE_orders.writeThen:
@PreAuthorize("hasAuthority('SCOPE_orders.read')")
@GetMapping("/orders")
public List<Order> getOrders() {
...
}@PreAuthorize("hasAuthority('SCOPE_orders.read')")
@GetMapping("/orders")
public List<Order> getOrders() {
...
}This is authorization.
And this distinction is extremely important:
Authentication ≠ Authorization
Authentication asks:
Who are you?
Authorization asks:
What are you allowed to do?
JWT can carry information used for both, but your application must still enforce authorization.
22. What Happens Inside the SecurityContext?
After successful authentication:
JwtAuthenticationTokenJwtAuthenticationTokenis placed into:
SecurityContextHolderSecurityContextHolderConceptually:
SecurityContextHolder
|
↓
SecurityContext
|
↓
Authentication
|
↓
JwtAuthenticationToken
|
↓
Jwt principalSecurityContextHolder
|
↓
SecurityContext
|
↓
Authentication
|
↓
JwtAuthenticationToken
|
↓
Jwt principalYour controller can then access the authenticated principal.
For example:
@GetMapping("/me")
public String me(Authentication authentication) {
return authentication.getName();
}@GetMapping("/me")
public String me(Authentication authentication) {
return authentication.getName();
}Spring Security's default JWT principal is a Jwt, and Authentication#getName maps to sub when present.
23. Now Let's Talk About Attacks
This is where many JWT tutorials stop.
They shouldn't.
A JWT is not magically secure because it contains a signature.
The security depends on:
Algorithm
+
Key management
+
Validation
+
Token storage
+
Transport
+
Expiration
+
Revocation
+
AuthorizationAlgorithm
+
Key management
+
Validation
+
Token storage
+
Transport
+
Expiration
+
Revocation
+
AuthorizationLet's examine the major attack classes.
24. Attack #1 — Changing the Payload
Original:
{
"sub": "user123",
"role": "USER"
}{
"sub": "user123",
"role": "USER"
}Attacker changes it to:
{
"sub": "user123",
"role": "ADMIN"
}{
"sub": "user123",
"role": "ADMIN"
}The signature becomes invalid.
A correctly implemented verifier rejects the token.
Defense
Never trust decoded claims until signature verification and all required validations have succeeded.
Do not do:
decode(token)
trustClaims()decode(token)
trustClaims()Instead:
Parse
↓
Verify
↓
Validate
↓
AuthorizeParse
↓
Verify
↓
Validate
↓
Authorize25. Attack #2 — The alg: none Problem
Historically, vulnerable JWT implementations accepted:
{
"alg": "none"
}{
"alg": "none"
}meaning the token had no cryptographic signature.
An attacker could potentially create an unsigned token.
Modern libraries generally protect against this, but the underlying lesson remains:
Never let the token choose whatever cryptographic policy your server will accept.
The application should define the trusted algorithms.
RFC 8725 says libraries must allow callers to specify supported algorithms and must not accept algorithms outside that set.
OWASP also recommends ensuring unsecured JWTs are not accepted.
26. Attack #3 — Algorithm Confusion
Imagine the legitimate system uses:
RS256RS256with:
Private key → signing
Public key → verificationPrivate key → signing
Public key → verificationAn insecure implementation might be tricked into treating an RSA public key as an HMAC secret.
The attacker attempts to construct an HS256 token using material that the verifier incorrectly treats as a symmetric secret.
This is known as:
Algorithm ConfusionAlgorithm Confusionor:
Key Type ConfusionKey Type ConfusionDefense
Explicitly constrain algorithms.
For example:
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithms: RS256spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithms: RS256Do not allow arbitrary algorithms merely because the JWT header requests them.
Spring Security's resource server configuration allows trusted JWS algorithms to be constrained.
27. Attack #4 — Weak HMAC Secret
Suppose you use:
HS256HS256with:
secret = "password123"secret = "password123"This is dangerous.
If an attacker obtains a token, they may attempt offline guessing against a weak secret.
The problem is not JWT itself.
The problem is:
weak cryptographic keyweak cryptographic keyRFC 8725 specifically warns that weak symmetric keys can be vulnerable to offline brute-force/dictionary attacks.
Better
Generate a cryptographically random secret.
Never use:
password
companyname
secret123
springbootsecretpassword
companyname
secret123
springbootsecretas an HMAC signing key.
For distributed systems, strongly consider asymmetric signing when the architecture benefits from separating signing and verification capabilities.
28. Attack #5 — Token Theft
This is probably the most practical JWT attack.
An attacker does not need to forge a token.
They can simply steal a valid one.
For a bearer token:
Whoever possesses it
can potentially use it.Whoever possesses it
can potentially use it.That's why it is called:
Bearer TokenBearer TokenIf an attacker gets:
Authorization: Bearer eyJ...Authorization: Bearer eyJ...they may be able to impersonate the user until the token expires or is otherwise invalidated.
29. Where Can Token Theft Happen?
Potential locations include:
Browser storage
Browser extensions
Malicious JavaScript
XSS
Application logs
Reverse proxy logs
Tracing systems
Developer debugging
Browser history
Misconfigured monitoring
Compromised endpoint
Network interception without TLSBrowser storage
Browser extensions
Malicious JavaScript
XSS
Application logs
Reverse proxy logs
Tracing systems
Developer debugging
Browser history
Misconfigured monitoring
Compromised endpoint
Network interception without TLSTherefore:
JWT security is not only cryptography.
It's also operational security.
30. Never Log the Full JWT
This is one of the simplest production mistakes.
Bad:
log.info("Authorization token: {}", token);log.info("Authorization token: {}", token);Your logging system may send that token to:
ELK
Splunk
CloudWatch
Grafana
Datadog
SIEMELK
Splunk
CloudWatch
Grafana
Datadog
SIEMNow dozens of people and systems may have access to a valid credential.
Instead log:
requestId
userId
issuer
tokenId (carefully)
authentication resultrequestId
userId
issuer
tokenId (carefully)
authentication resultNever the full bearer token.
31. Attack #6 — XSS + Browser Storage
A common SPA architecture stores tokens in:
localStoragelocalStorageThe problem:
If malicious JavaScript executes in the application's origin, it may access:
localStorage.getItem("access_token")localStorage.getItem("access_token")and steal the token.
Better architecture
For browser applications, consider an architecture using:
HttpOnly
Secure
SameSiteHttpOnly
Secure
SameSitecookies where appropriate, combined with proper CSRF defenses and an architecture suited to the application.
HttpOnly prevents JavaScript from directly reading the cookie.
But remember:
HttpOnly does not magically prevent every attack.
If an attacker can execute JavaScript in your application's origin, the script may still cause authenticated requests.
That's why XSS prevention remains essential.
32. Attack #7 — CSRF
If authentication uses cookies, browsers automatically attach cookies to requests.
That creates CSRF considerations.
An attacker may attempt:
Malicious Website
↓
Victim Browser
↓
Your API
↓
Authenticated CookieMalicious Website
↓
Victim Browser
↓
Your API
↓
Authenticated CookieTherefore, cookie-based authentication needs appropriate CSRF protections.
Common controls include:
SameSite cookies
CSRF tokens
Origin/Referer validation where appropriate
strict CORS configurationSameSite cookies
CSRF tokens
Origin/Referer validation where appropriate
strict CORS configurationDo not blindly copy a CSRF configuration from another application.
The correct defense depends on your browser/API architecture.
33. Attack #8 — Wrong Audience
Imagine:
Identity Provider
|
+---- Orders API token
|
+---- Payments API token
|
+---- Profile API tokenIdentity Provider
|
+---- Orders API token
|
+---- Payments API token
|
+---- Profile API tokenA token for:
payments-apipayments-apishould not automatically become valid for:
orders-apiorders-apiValidate:
"aud": "orders-api""aud": "orders-api"This is one reason audience validation is so important in microservices.
Spring Security supports audience validation in its JWT resource server configuration.
34. Attack #9 — Wrong Issuer
Imagine two identity providers:
https://auth.company.com
https://attacker.examplehttps://auth.company.com
https://attacker.exampleA JWT with a valid signature is not necessarily a token your application should trust.
Your service needs to establish:
Who is allowed to issue tokens for this API?Who is allowed to issue tokens for this API?Then validate:
"iss": "https://auth.company.com""iss": "https://auth.company.com"35. Attack #10 — kid Manipulation
Suppose the JWT header contains:
{
"alg": "RS256",
"kid": "key-123"
}{
"alg": "RS256",
"kid": "key-123"
}The server uses kid to find a key.
If developers build an unsafe key lookup such as:
SELECT key FROM keys WHERE id = '<kid>'SELECT key FROM keys WHERE id = '<kid>'without safe parameterization, attacker-controlled header data can become an injection vector.
RFC 8725 specifically warns that received kid values can become SQL/LDAP injection vectors if used unsafely.
Better
Use:
trusted JWKS
+
safe key lookup
+
strict key identifiers
+
parameterized queries if a database is involvedtrusted JWKS
+
safe key lookup
+
strict key identifiers
+
parameterized queries if a database is involved36. Attack #11 — jku / x5u SSRF
Some JWT headers can contain URLs related to key material.
For example:
jku
x5ujku
x5uA dangerous implementation might blindly fetch:
https://whatever-url-the-attacker-providedhttps://whatever-url-the-attacker-providedNow the JWT becomes an SSRF primitive.
The server could potentially be tricked into requesting:
internal services
metadata endpoints
private network addressesinternal services
metadata endpoints
private network addressesRFC 8725 specifically warns against blindly following attacker-controlled jku or x5u URLs and recommends allowlisting trusted locations.
Production rule
Do not blindly trust key URLs supplied by the token.
Prefer:
Configured trusted issuer
↓
Trusted JWKS endpointConfigured trusted issuer
↓
Trusted JWKS endpointrather than:
Token
↓
"Go fetch this URL"Token
↓
"Go fetch this URL"37. Attack #12 — Replay Attack
Suppose a valid token is stolen:
Token A
exp = 15 minutesToken A
exp = 15 minutesThe attacker sends:
GET /accounts
Authorization: Bearer Token-AGET /accounts
Authorization: Bearer Token-AThe server sees:
Valid signature
Valid issuer
Valid audience
Not expiredValid signature
Valid issuer
Valid audience
Not expiredSo it accepts it.
The attacker may continue replaying it.
This is why:
Valid does not necessarily mean legitimate.
A stolen token can be cryptographically valid.
38. How Do You Reduce Replay Risk?
Several layers help.
Short expiration
For example:
Access token: 5–15 minutesAccess token: 5–15 minutesRefresh-token rotation
Rotate refresh tokens and detect reuse.
Revocation
Maintain server-side state for high-risk scenarios.
Sender-constrained tokens
Technologies such as DPoP can bind token usage to a key, making simple token theft less useful.
OWASP notes short expiration, deny lists, nonce/session binding, and sender-constrained approaches as replay/token-exfiltration mitigations.
39. JWT Revocation — The Stateless Myth
You often hear:
"JWT is stateless."
Not completely.
Signature verification can be stateless.
But operational requirements may not be.
Suppose:
User logs in
↓
JWT issued
↓
User changes password
↓
Account compromisedUser logs in
↓
JWT issued
↓
User changes password
↓
Account compromisedShould an existing token continue working for another 15 minutes?
Maybe.
Maybe not.
If you need immediate invalidation, you need server-side state or another mechanism.
For example:
JWT
|
+--- jti
|
↓
Revocation Store
|
↓
RedisJWT
|
+--- jti
|
↓
Revocation Store
|
↓
RedisThen:
Token valid cryptographically?
↓ yes
Token revoked?
↓ no
AcceptToken valid cryptographically?
↓ yes
Token revoked?
↓ no
AcceptThe price is additional infrastructure and latency.
40. Access Token vs Refresh Token
Don't make your access token live for:
30 days30 daysjust because you don't want users to log in again.
Instead:
Access Token
↓
Short lifetime
↓
API accessAccess Token
↓
Short lifetime
↓
API accessand:
Refresh Token
↓
Longer lifetime
↓
Obtain new access tokenRefresh Token
↓
Longer lifetime
↓
Obtain new access tokenConceptually:
Login
|
+---- Access Token ----> API
|
+---- Refresh Token --> Authorization Server
|
↓
New Access TokenLogin
|
+---- Access Token ----> API
|
+---- Refresh Token --> Authorization Server
|
↓
New Access TokenRefresh tokens require particularly careful protection because compromise can provide long-lived access.
41. A Production JWT Architecture
A typical architecture might look like:
┌─────────────────────┐
│ Authorization │
│ Server │
└──────────┬──────────┘
│
Private Key
│
↓
Sign JWT
│
↓
┌─────────────┐
│ Client │
└──────┬──────┘
│
Bearer Access Token
│
↓
┌─────────────────────┐
│ API Gateway │
└──────────┬──────────┘
│
↓
┌─────────────────────┐
│ Spring Boot API │
│ │
│ BearerTokenFilter │
│ ↓ │
│ AuthenticationMgr │
│ ↓ │
│ JwtAuthProvider │
│ ↓ │
│ JwtDecoder │
│ ↓ │
│ Signature │
│ ↓ │
│ Claims │
│ ↓ │
│ Authorization │
└──────────┬──────────┘
│
↓
Business Logic ┌─────────────────────┐
│ Authorization │
│ Server │
└──────────┬──────────┘
│
Private Key
│
↓
Sign JWT
│
↓
┌─────────────┐
│ Client │
└──────┬──────┘
│
Bearer Access Token
│
↓
┌─────────────────────┐
│ API Gateway │
└──────────┬──────────┘
│
↓
┌─────────────────────┐
│ Spring Boot API │
│ │
│ BearerTokenFilter │
│ ↓ │
│ AuthenticationMgr │
│ ↓ │
│ JwtAuthProvider │
│ ↓ │
│ JwtDecoder │
│ ↓ │
│ Signature │
│ ↓ │
│ Claims │
│ ↓ │
│ Authorization │
└──────────┬──────────┘
│
↓
Business Logic42. Spring Boot Configuration
For a modern Spring Boot resource server:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>Spring Security's resource-server support uses JWT decoding/verification facilities provided by its OAuth2 JOSE support.
Configuration:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.comspring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.comThen:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**")
.hasAuthority("SCOPE_admin")
.anyRequest()
.authenticated()
)
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(Customizer.withDefaults())
);
return http.build();
}
}@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**")
.hasAuthority("SCOPE_admin")
.anyRequest()
.authenticated()
)
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(Customizer.withDefaults())
);
return http.build();
}
}The current Spring Security documentation shows this same resource-server model: configure the issuer, configure JWT resource-server support, and let Spring Security create the appropriate JWT decoder and authentication components.
43. Don't Build Your Own JWT Filter Unless You Have a Good Reason
You will often see tutorials like:
@Component
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(...) {
String token = extractToken(request);
Claims claims = parse(token);
SecurityContextHolder
.getContext()
.setAuthentication(...);
}
}@Component
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(...) {
String token = extractToken(request);
Claims claims = parse(token);
SecurityContextHolder
.getContext()
.setAuthentication(...);
}
}It looks simple.
But now you own:
signature validation
algorithm validation
issuer validation
audience validation
clock skew
key rotation
JWK handling
error handling
authority mapping
security context managementsignature validation
algorithm validation
issuer validation
audience validation
clock skew
key rotation
JWK handling
error handling
authority mapping
security context managementThat is a lot of security-sensitive code.
For standard OAuth2/OIDC JWT resource-server use cases, use Spring Security's built-in resource-server support rather than reinventing the authentication pipeline.
44. Adding Audience Validation
For example:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com
audiences:
- orders-apispring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com
audiences:
- orders-apiNow the application checks:
iss == trusted issuer
AND
aud contains orders-apiiss == trusted issuer
AND
aud contains orders-apiThis is much stronger than:
signature validsignature validalone.
45. Custom Authority Mapping
Suppose your identity provider returns:
{
"roles": [
"ADMIN",
"REPORT_VIEWER"
]
}{
"roles": [
"ADMIN",
"REPORT_VIEWER"
]
}while Spring Security expects authorities.
You can use:
JwtAuthenticationConverterJwtAuthenticationConverterto map claims to authorities.
Conceptually:
JWT
|
| roles
↓
JwtAuthenticationConverter
|
↓
GrantedAuthority
|
↓
AuthorizationJWT
|
| roles
↓
JwtAuthenticationConverter
|
↓
GrantedAuthority
|
↓
AuthorizationThis keeps authentication and authorization concerns explicit.
Spring Security provides JwtAuthenticationConverter specifically for adapting JWT claims into granted authorities.
46. Authentication Is Not Enough
Consider:
@GetMapping("/users/{id}")
public User getUser(@PathVariable String id) {
return userService.find(id);
}@GetMapping("/users/{id}")
public User getUser(@PathVariable String id) {
return userService.find(id);
}Suppose the JWT says:
{
"sub": "user-123"
}{
"sub": "user-123"
}The request is authenticated.
But can:
user-123user-123request:
/users/user-999/users/user-999?
Authentication does not answer that.
You need authorization/business rules.
For example:
@PreAuthorize(
"#id == authentication.name or hasAuthority('SCOPE_users.read.all')"
)@PreAuthorize(
"#id == authentication.name or hasAuthority('SCOPE_users.read.all')"
)The exact policy depends on your domain.
47. JWT Does Not Replace Authorization
This is a very common architecture mistake:
{
"role": "ADMIN"
}{
"role": "ADMIN"
}and then:
if ("ADMIN".equals(role)) {
allowEverything();
}if ("ADMIN".equals(role)) {
allowEverything();
}Roles can become stale.
Suppose:
10:00 AM
User = ADMIN
10:05 AM
Admin role removed
10:06 AM
Old JWT still says ADMIN10:00 AM
User = ADMIN
10:05 AM
Admin role removed
10:06 AM
Old JWT still says ADMINThe token still contains the old claim.
This is one reason sensitive authorization decisions sometimes require additional server-side checks.
48. Don't Put Too Much Data in JWTs
JWTs travel with requests.
If you put:
{
"permissions": [
"...hundreds..."
],
"customerProfile": "...",
"preferences": "...",
"accountData": "..."
}{
"permissions": [
"...hundreds..."
],
"customerProfile": "...",
"preferences": "...",
"accountData": "..."
}your token becomes huge.
Problems:
larger HTTP requests
larger headers
proxy limits
gateway limits
logging exposure
caching issueslarger HTTP requests
larger headers
proxy limits
gateway limits
logging exposure
caching issuesKeep tokens focused on identity and authorization context.
49. Don't Put Secrets in JWTs
Never store:
password
API secret
database password
private key
refresh token
credit card detailspassword
API secret
database password
private key
refresh token
credit card detailsin a normal signed JWT.
Remember:
Base64URL ≠ encryptionBase64URL ≠ encryption50. Token Storage Is an Architecture Decision
For browser applications, there is no universal:
"Always use localStorage."
or:
"Always use cookies."
The decision depends on your architecture and threat model.
localStorage
Advantages:
easy SPA accesseasy SPA accessRisks:
JavaScript can access it
XSS can steal itJavaScript can access it
XSS can steal itHttpOnly Cookie
Advantages:
JavaScript cannot directly read itJavaScript cannot directly read itRisks/considerations:
CSRF
cookie configuration
SameSite behavior
cross-site architecture
CORSCSRF
cookie configuration
SameSite behavior
cross-site architecture
CORSFor high-security applications, design token storage together with the complete browser security model rather than treating it as an isolated JWT decision.
51. HTTPS Is Mandatory
Never send bearer tokens over plain HTTP.
Bad:
http://api.example.comhttp://api.example.comGood:
https://api.example.comhttps://api.example.comA bearer token is effectively a credential.
If an attacker intercepts it:
attacker
↓
valid token
↓
APIattacker
↓
valid token
↓
APIThe attacker may be able to impersonate the user.
52. CORS Is Not Authentication
Another common misconception:
"We enabled CORS, so our API is secure."
No.
CORS controls browser cross-origin behavior.
JWT authentication controls identity/authentication.
They solve different problems.
Think:
CORS
↓
Browser cross-origin policy
JWT
↓
Authentication
Authorization
↓
What can this principal do?CORS
↓
Browser cross-origin policy
JWT
↓
Authentication
Authorization
↓
What can this principal do?You may need all three.
53. Key Rotation
Your signing key should not live forever.
Imagine:
2026-01
key-A
2026-08
key-B2026-01
key-A
2026-08
key-BDuring rotation:
Authorization Server
|
+---- signs new tokens with key-B
|
+---- publishes key-A + key-BAuthorization Server
|
+---- signs new tokens with key-B
|
+---- publishes key-A + key-BResource servers can continue validating older tokens while accepting new ones.
Then:
old tokens expire
↓
remove key-Aold tokens expire
↓
remove key-AThis is why kid and JWKS are important.
Spring Security supports automatic rotation of verification keys made available by the authorization server.
54. A Strong Production Validation Model
Think of your JWT validator as a checklist:
JWT
|
↓
┌───────────────┐
│ Structure OK? │
└───────┬───────┘
↓
┌───────────────┐
│ Algorithm OK? │
└───────┬───────┘
↓
┌───────────────┐
│ Signature OK? │
└───────┬───────┘
↓
┌───────────────┐
│ Issuer OK? │
└───────┬───────┘
↓
┌───────────────┐
│ Audience OK? │
└───────┬───────┘
↓
┌───────────────┐
│ Time OK? │
│ exp / nbf │
└───────┬───────┘
↓
┌───────────────┐
│ Token type OK?│
└───────┬───────┘
↓
┌───────────────┐
│ Revoked? │
└───────┬───────┘
↓
┌───────────────┐
│ Authorized? │
└───────┬───────┘
↓
ALLOW JWT
|
↓
┌───────────────┐
│ Structure OK? │
└───────┬───────┘
↓
┌───────────────┐
│ Algorithm OK? │
└───────┬───────┘
↓
┌───────────────┐
│ Signature OK? │
└───────┬───────┘
↓
┌───────────────┐
│ Issuer OK? │
└───────┬───────┘
↓
┌───────────────┐
│ Audience OK? │
└───────┬───────┘
↓
┌───────────────┐
│ Time OK? │
│ exp / nbf │
└───────┬───────┘
↓
┌───────────────┐
│ Token type OK?│
└───────┬───────┘
↓
┌───────────────┐
│ Revoked? │
└───────┬───────┘
↓
┌───────────────┐
│ Authorized? │
└───────┬───────┘
↓
ALLOWNot every application needs every check, but the important lesson is:
JWT validation is a policy, not just signature verification.
55. 401 vs 403
This distinction matters in Spring Security.
401 Unauthorized
Usually means:
You are not successfully authenticated.You are not successfully authenticated.Examples:
missing token
invalid signature
expired token
invalid issuer
invalid audiencemissing token
invalid signature
expired token
invalid issuer
invalid audience403 Forbidden
Usually means:
You are authenticated, but not allowed to perform this action.You are authenticated, but not allowed to perform this action.Example:
JWT valid
user authenticated
missing required authorityJWT valid
user authenticated
missing required authoritySo:
Invalid JWT → 401
Valid JWT + insufficient permission → 403Invalid JWT → 401
Valid JWT + insufficient permission → 40356. JWT Security Checklist
Before shipping a production Spring Boot API, ask:
Cryptography
- Are only approved algorithms accepted?
- Are signing keys cryptographically strong?
- Are private keys protected?
- Are keys rotated?
- Is asymmetric signing appropriate for the architecture?
Token validation
- Is the signature validated?
- Is
issvalidated? - Is
audvalidated? - Is
expvalidated? - Is
nbfvalidated where appropriate? - Is token type validated where required?
- Are authorities mapped safely?
Token lifecycle
- Are access tokens short-lived?
- Are refresh tokens protected?
- Is refresh-token rotation implemented where appropriate?
- Is revocation available for high-risk operations?
- Is replay risk considered?
Transport
- HTTPS everywhere?
- Secure cookies where cookies are used?
- Appropriate SameSite policy?
- CORS configured narrowly?
Logging
- Are JWTs excluded from logs?
- Are Authorization headers excluded from tracing?
- Are error responses free of sensitive token information?
Infrastructure
- Are JWKS endpoints trusted?
- Is
kidhandled safely? - Are
jku/x5uvalues never blindly followed? - Are proxy/header limits understood?
- Are signing keys stored in a proper secret/key-management system?
Application authorization
- Is authentication separated from authorization?
- Are object-level permissions enforced?
- Are stale claims considered?
- Are high-risk operations protected with additional controls?
57. The Most Important Mental Model
Don't think:
JWT = secure loginJWT = secure loginThink:
JWT
|
+-- Cryptographic integrity
|
+-- Identity
|
+-- Token lifetime
|
+-- Intended audience
|
+-- Issuer trust
|
+-- Authorization context
|
+-- Replay risk
|
+-- Storage security
|
+-- Transport security
|
+-- Key management
|
+-- Revocation strategyJWT
|
+-- Cryptographic integrity
|
+-- Identity
|
+-- Token lifetime
|
+-- Intended audience
|
+-- Issuer trust
|
+-- Authorization context
|
+-- Replay risk
|
+-- Storage security
|
+-- Transport security
|
+-- Key management
|
+-- Revocation strategyJWT is only one component of the security architecture.
58. The Five Questions Every Backend Developer Should Ask
Whenever you see a JWT implementation, ask:
1. Who issued this token?
Check:
ississ2. Who is this token intended for?
Check:
audaud3. Can someone modify this token?
Check:
signature
algorithm
keysignature
algorithm
key4. Can someone replay this token?
Think about:
expiration
revocation
sender-constraining
token theftexpiration
revocation
sender-constraining
token theft5. What can this user actually do?
Check:
scope
roles
permissions
resource ownership
business authorizationscope
roles
permissions
resource ownership
business authorizationIf you can answer all five confidently, you're already thinking about JWT at a much higher level than simply:
"Header + payload + signature."
59. Final Takeaway
JWT is not dangerous.
JWT is not automatically secure either.
It is a tool.
A correctly implemented JWT architecture gives you:
Fast verification
+
Distributed authentication
+
No database lookup for every signature check
+
Good microservice scalabilityFast verification
+
Distributed authentication
+
No database lookup for every signature check
+
Good microservice scalabilityBut you pay for that simplicity with responsibilities around:
Key management
Token lifetime
Revocation
Replay
Storage
AuthorizationKey management
Token lifetime
Revocation
Replay
Storage
AuthorizationAnd this is the biggest lesson:
A valid JWT is not automatically a trustworthy authorization decision.
Your backend needs to establish:
Valid structure
↓
Trusted algorithm
↓
Valid signature
↓
Trusted issuer
↓
Correct audience
↓
Valid time window
↓
Correct token type
↓
Not revoked / acceptable replay risk
↓
Correct authorization
↓
Business-level permissionValid structure
↓
Trusted algorithm
↓
Valid signature
↓
Trusted issuer
↓
Correct audience
↓
Valid time window
↓
Correct token type
↓
Not revoked / acceptable replay risk
↓
Correct authorization
↓
Business-level permissionOnly then should your application say:
REQUEST ALLOWEDREQUEST ALLOWEDThat is what production-grade JWT authentication looks like.
And in Spring Boot, the safest starting point is usually not writing another custom JWT filter.
It is understanding and correctly configuring the security machinery that Spring Security already provides.
Spring Security's current resource-server implementation gives you the core pipeline — bearer-token extraction, authentication management, JWT decoding, signature verification, claim validation, authority conversion, and SecurityContext population — so your application can focus on authorization and business rules rather than reimplementing cryptographic plumbing.
JWT is only three strings separated by dots.
Let's dicuss question if any related JWT implementation.