JSON Web Tokens (JWTs) have become a fundamental part of authentication and authorization in modern web applications.
They are widely used in REST APIs, mobile applications, Single Sign-On systems, microservice architectures, and OAuth/OpenID Connect environments.
However, the widespread adoption of JWT does not mean that JWT implementations are secure by default.
Weak signing secrets, incorrect algorithm handling, missing claim validation, excessive token lifetimes, insecure token storage, and broken authorization logic can turn an otherwise standard JWT implementation into a serious security vulnerability.
This guide explores JWT security from both a defensive and penetration-testing perspective, including how JWT works, common implementation mistakes, attack surfaces, token lifecycle problems, key management issues, and recommended security practices.
Ethical Use Notice: This article is intended for security education and authorized security testing. Only test systems that you own or have explicit permission to assess.
1. What Is JWT?
JWT stands for:
JSON Web Token
It is a compact format designed to securely transmit claims between different parties.
A typical JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0Iiwicm9sZSI6InVzZXIiLCJleHAiOjE3MDAwMDAwMDB9.signature
A JWT commonly consists of three sections:
HEADER.PAYLOAD.SIGNATURE
Conceptually:
Header
↓
Payload
↓
Signature
Each section is separated using a period:
xxxxx.yyyyy.zzzzz
The Header and Payload are normally encoded using Base64URL.
One of the most important things to understand is:
The Header and Payload of a signed JWT are normally encoded, not encrypted.
Anyone who obtains the token can usually decode these sections.
Therefore, sensitive information such as passwords, private keys, API secrets, or payment information should not be placed inside a normal signed JWT payload.
2. JWT Header
The JWT Header describes metadata related to the token.
A common header looks like:
{
"alg": "HS256",
"typ": "JWT"
}
The alg parameter specifies the signing algorithm.
Examples include:
HS256
RS256
ES256
The typ parameter commonly indicates the token type:
JWT
After Base64URL encoding, the header may look similar to:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
This value can easily be decoded.
Therefore, security must never depend on hiding information inside the JWT Header.
3. JWT Payload
The Payload contains the claims associated with the token.
For example:
{
"sub": "1234",
"username": "kutay",
"role": "user",
"iat": 1760000000,
"exp": 1760003600
}
JWT defines several registered claims.
| Claim | Meaning |
|---|---|
iss |
Issuer |
sub |
Subject |
aud |
Audience |
exp |
Expiration Time |
nbf |
Not Before |
iat |
Issued At |
jti |
JWT ID |
Applications can also define custom claims.
For example:
{
"user_id": 42,
"role": "admin",
"permissions": [
"read",
"write"
]
}
These custom claims often become extremely important during security assessments because applications may use them when making authorization decisions.
4. How JWT Signatures Work
Consider an HMAC SHA-256 JWT.
Conceptually, its signature can be generated using:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
The important input is:
header.payload
The server signs this data using a secret.
When the JWT is later received, the server calculates the signature again and compares it against the signature supplied with the token.
Suppose the original payload contains:
{
"role": "user"
}
If someone modifies it to:
{
"role": "admin"
}
the original signature should no longer match.
A correctly implemented server rejects the modified token.
This integrity protection is one of the primary purposes of JWT signatures.
5. Encoding Is Not Encryption
One of the most common JWT misconceptions is assuming that Base64URL encoding provides confidentiality.
Consider:
eyJ1c2VybmFtZSI6Imt1dGF5Iiwicm9sZSI6InVzZXIifQ
After decoding:
{
"username": "kutay",
"role": "user"
}
The information is immediately visible.
Therefore, avoid placing information such as:
password
API secret
private key
credit card information
database credentials
internal secrets
inside ordinary signed JWT payloads.
The signature protects integrity and authenticity when correctly implemented.
It does not automatically provide confidentiality.
6. Understanding the JWT Attack Surface
When encountering JWT authentication during an authorized security assessment, several questions should immediately be considered:
- Which signing algorithm is being used?
- Is the signature actually verified?
- Is the expected algorithm enforced server-side?
- Does the token expire?
- Is expiration actually validated?
- Is the issuer validated?
- Is the audience validated?
- Are roles or permissions stored inside the token?
- Are authorization decisions based entirely on token claims?
- How are signing keys managed?
- Can tokens be revoked?
- Are refresh tokens used?
- Can tokens intended for one service be reused against another?
- Where are tokens stored?
- Are tokens accidentally exposed through logs or URLs?
JWT security should therefore be examined as an entire trust model rather than simply as a cryptographic problem.
7. The alg: none Problem
One historically important JWT implementation flaw involves the none algorithm.
A JWT header could theoretically contain:
{
"alg": "none",
"typ": "JWT"
}
The payload could contain:
{
"username": "kutay",
"role": "admin"
}
The resulting structure may effectively become:
HEADER.PAYLOAD.
Some vulnerable or outdated implementations historically accepted unsigned tokens when the attacker changed the algorithm to:
none
The core security mistake is allowing an untrusted token to control whether cryptographic verification is required.
Secure Approach
The application should determine the expected algorithm independently.
Bad design:
Use whatever algorithm the token requests.
Better design:
This API accepts RS256 only.
Conceptually:
verify(
token,
publicKey,
algorithms = ["RS256"]
)
Strict algorithm allowlisting is an important JWT security control.
8. Algorithm Confusion
Another historically important JWT vulnerability class is algorithm confusion.
Understanding the difference between symmetric and asymmetric signing algorithms is essential.
HS256
HS256 uses HMAC.
The same secret is used for signing and verification:
SECRET
|
+---- SIGN
|
+---- VERIFY
The secret therefore must remain confidential.
RS256
RS256 uses asymmetric cryptography.
Two keys exist:
Private Key
Public Key
The private key signs tokens:
PRIVATE KEY
↓
SIGN
↓
JWT
The public key verifies them:
JWT
↓
VERIFY
↓
PUBLIC KEY
The public key does not need to remain secret.
Problems have historically occurred when vulnerable libraries or applications failed to properly distinguish these algorithm families.
For example, an application designed for RS256 could incorrectly accept a token using HS256 and treat public-key material as an HMAC secret.
Prevention
The expected algorithm should be explicitly configured:
algorithms = ["RS256"]
The application should not dynamically trust the algorithm selected by an untrusted token.
9. Weak HMAC Secrets
With HMAC algorithms such as HS256, security depends heavily on the strength of the signing secret.
Examples of dangerously weak secrets include:
secret
password
123456
jwtsecret
admin
secret123
companyname
If an attacker obtains a legitimate JWT, weak HMAC secrets may be susceptible to offline guessing.
Conceptually:
Candidate Secret
↓
HMAC(header.payload)
↓
Generated Signature
↓
Compare
↓
JWT Signature
Because the verification process can potentially be performed offline, rate limiting on the authentication endpoint does not necessarily protect a weak signing secret.
Secure Approach
HMAC secrets should be:
- cryptographically random,
- sufficiently long,
- high entropy,
- unique to the environment,
- securely generated,
- securely stored.
Avoid secrets such as:
company2026
supersecret
jwt_password
production_secret
Signing secrets should also not be hard-coded into application source code.
Bad:
const JWT_SECRET = "secret123";
Secrets should instead be managed through an appropriate secrets-management solution.
10. Excessive Trust in Role Claims
Consider a JWT containing:
{
"id": 15,
"username": "kutay",
"role": "user"
}
The backend may perform authorization based on:
role == admin?
A properly verified signature prevents users from simply modifying the claim.
However, another problem remains.
Suppose an administrator is demoted:
admin → user
Their previously issued JWT may still contain:
{
"role": "admin"
}
If that token remains valid for several hours or days, the old privileges could potentially remain available.
This demonstrates an important JWT principle:
Cryptographically valid does not necessarily mean currently authorized.
Applications should consider whether sensitive authorization information should be checked against current server-side state.
11. Missing exp Claim
Consider:
{
"sub": "123"
}
There is no:
exp
claim.
Without an appropriate expiration mechanism, the token may remain usable for an excessive amount of time.
This becomes especially dangerous when tokens leak through:
Application logs
Reverse proxy logs
Browser history
Monitoring platforms
Source code
Screenshots
Support tickets
Analytics systems
Shorter token lifetimes reduce the window in which stolen credentials remain useful.
For example:
{
"iat": 1760000000,
"exp": 1760000900
}
represents approximately a 15-minute lifetime.
The appropriate lifetime depends on the application's threat model and usability requirements.
12. exp Exists but Is Not Validated
Having an exp claim does not automatically make expiration enforcement secure.
Consider:
{
"sub": "123",
"exp": 1600000000
}
The token may already be expired.
If the backend only checks:
Is the signature valid?
without checking:
Has the token expired?
the token may still be accepted.
JWT validation therefore usually involves more than signature verification.
Depending on the architecture, validation may include:
Signature
Expiration
Not Before
Issuer
Audience
Token Type
13. nbf Validation
nbf means:
Not Before
For example:
{
"nbf": 1760000000
}
The token should not be accepted before the specified timestamp.
If the application ignores nbf, a token may become usable earlier than intended.
This becomes particularly relevant for time-restricted authorization workflows.
14. Issuer (iss) Validation
The iss claim identifies the entity that issued the token.
Example:
{
"iss": "https://auth.example.com"
}
A backend should not necessarily accept a token merely because its cryptographic signature can be verified.
It may also need to verify that the token came from the expected issuer.
Conceptually:
Signature valid?
↓
Issuer expected?
↓
Continue
If the application expects:
https://auth.example.com
then tokens from an unrelated issuer should not automatically be trusted.
This becomes especially important in environments involving multiple identity providers.
15. Audience (aud) Validation
The aud claim identifies the intended recipient of a token.
For example:
{
"aud": "payments-api"
}
Consider the following microservice architecture:
Authentication Server
|
-----------------
| | |
Profile Orders Payments
A token intended for:
profile-api
should not automatically be accepted by:
payments-api
Failing to validate the audience can enable cross-service token reuse.
Each service should validate that the token was actually issued for that service or an explicitly trusted audience.
16. Token Replay
JWTs are commonly used in stateless authentication architectures.
This provides scalability advantages but introduces an important consideration.
If a valid access token is stolen, it may potentially be reused until it expires.
This is known as:
Token Replay
For example, an attacker who obtains:
Authorization: Bearer <TOKEN>
may attempt to reuse the same token.
This demonstrates why JWT security is not simply about protecting the signing key.
Token confidentiality is also extremely important.
17. JWTs in URLs
Consider:
https://example.com/profile?token=eyJhbGciOi...
Placing authentication tokens inside URLs is dangerous.
URLs can appear in:
Browser history
Reverse proxy logs
Web server logs
Analytics systems
Monitoring platforms
Screenshots
Referrer information
Support systems
For APIs, bearer tokens are commonly transmitted through the HTTP Authorization header:
Authorization: Bearer <TOKEN>
Authentication credentials should generally not be placed inside query strings.
18. JWT and LocalStorage
Single-page applications sometimes store access tokens using:
localStorage.setItem("token", token);
The important security consideration is that JavaScript executing within the application's origin can access LocalStorage.
If the application contains an XSS vulnerability, malicious JavaScript may potentially access the stored token.
Conceptually:
localStorage.getItem("token");
Therefore, the combination of:
XSS
+
JavaScript-accessible authentication tokens
can significantly increase the impact of an XSS vulnerability.
Cookie-based approaches may instead use controls such as:
HttpOnly
Secure
SameSite
However, cookies introduce their own security considerations, including CSRF protections.
There is no universal storage mechanism that automatically solves every authentication threat.
19. HttpOnly Cookies
Consider:
Set-Cookie: access_token=TOKEN; HttpOnly; Secure; SameSite=Lax
The HttpOnly attribute prevents ordinary client-side JavaScript from directly reading the cookie.
This can make some token-stealing attacks more difficult.
However:
HttpOnly ≠ XSS protection
An attacker with JavaScript execution inside the application may still perform authenticated actions through the victim's browser.
Therefore, XSS remains a serious vulnerability even when authentication cookies are protected using HttpOnly.
20. Refresh Token Security
Modern authentication architectures frequently use:
Access Token
+
Refresh Token
Access tokens are normally relatively short-lived.
Refresh tokens may remain valid for much longer.
The workflow may look like:
LOGIN
↓
ACCESS TOKEN
+
REFRESH TOKEN
↓
Access Token Expires
↓
Refresh Endpoint
↓
NEW ACCESS TOKEN
Because refresh tokens can be used to obtain new access tokens, they are high-value credentials.
Refresh tokens should therefore be:
- securely stored,
- revocable,
- protected from unnecessary exposure,
- rotated where appropriate,
- monitored for suspicious reuse.
21. Refresh Token Rotation
Refresh token rotation can reduce the impact of refresh-token theft.
Suppose the client initially receives:
Refresh Token A
After using it:
A → invalid
B → new refresh token
The next refresh becomes:
B → invalid
C → new refresh token
If:
A
is later reused, the server knows that an already-consumed credential has appeared again.
Depending on the architecture, this may trigger:
Token family revocation
Session termination
Security logging
User notification
Reauthentication
This technique is often referred to as refresh token reuse detection.
22. Does Logout Actually Revoke the Token?
Stateless JWT systems introduce an interesting logout problem.
The frontend may simply delete the token:
Delete local token
But the JWT itself may remain cryptographically valid.
Consider:
1. User authenticates.
2. Server issues JWT.
3. JWT is stolen.
4. User logs out.
5. Attacker continues using the stolen JWT.
If the backend does not maintain revocation state, the token may continue working until:
exp
is reached.
Depending on the application's security requirements, possible strategies include:
Short-lived access tokens
Refresh token revocation
Session storage
Token versioning
Revocation lists
There is a trade-off between fully stateless authentication and immediate revocation capabilities.
23. The jti Claim
jti stands for:
JWT ID
It can provide a unique identifier for a particular token.
Example:
{
"jti": "550e8400-e29b-41d4-a716-446655440000"
}
The identifier may be useful for:
Revocation
Audit logging
Replay detection
Session tracking
For example, a backend could maintain:
revoked_jti
records.
When a JWT is received:
JWT
↓
Extract jti
↓
Is jti revoked?
↓
Reject / Continue
This introduces server-side state, partially reducing one of the advantages of stateless JWT authentication.
Whether this is appropriate depends on the application's architecture and security requirements.
24. Understanding the kid Header
JWT headers sometimes contain:
{
"alg": "RS256",
"kid": "key-2026"
}
kid means:
Key ID
It helps the verifier identify which key should be used.
For example:
key-2025
key-2026
key-2027
This becomes especially useful during key rotation.
The server can use:
kid = key-2026
to select the corresponding trusted public key.
However, insecure handling of attacker-controlled kid values can create additional vulnerabilities.
25. kid Manipulation
Consider a dangerous implementation:
key = load("/keys/" + kid)
If the kid parameter is taken directly from the JWT without validation, unexpected values could influence key selection.
For example, malicious input containing path manipulation sequences may interact with unsafe file-handling logic.
The broader security principle is:
Never directly concatenate untrusted JWT header values into sensitive operations.
A kid value should not directly control:
Filesystem paths
SQL queries
Shell commands
Remote requests
A safer design is:
kid
↓
Allowlisted identifier
↓
Trusted key mapping
For example:
key-1 → PUBLIC_KEY_1
key-2 → PUBLIC_KEY_2
Unknown identifiers should be rejected.
26. JWK and JWKS
Modern JWT infrastructure often uses:
JWK
JWKS
JWK means:
JSON Web Key
JWKS means:
JSON Web Key Set
A public JWKS endpoint may look like:
/.well-known/jwks.json
Example content:
{
"keys": [
{
"kty": "RSA",
"kid": "key1",
"use": "sig",
"n": "...",
"e": "AQAB"
}
]
}
Publishing public verification keys is not inherently a vulnerability.
In asymmetric cryptography:
Public Key → verification
Private Key → signing
The private key is the critical secret.
27. The jku Header
JWT may also contain:
jku
which can reference a JWK Set URL.
Example:
{
"alg": "RS256",
"jku": "https://keys.example.com/jwks.json",
"kid": "key1"
}
The critical security question is:
Does the application trust arbitrary
jkuURLs supplied by the token?
A secure application should normally restrict key sources to explicitly trusted locations.
Blindly retrieving attacker-controlled key URLs can introduce serious trust problems and, depending on the implementation and network environment, may also create server-side request risks.
28. The x5u Header
Similarly:
x5u
may identify a URL containing X.509 certificate material.
Example:
{
"alg": "RS256",
"x5u": "https://example.com/certs.pem"
}
The same principle applies:
A token should not be able to arbitrarily redefine the trust source used to verify itself.
Remote key and certificate sources should be tightly controlled.
29. Key Rotation
Cryptographic signing keys should have a managed lifecycle.
An organization might rotate keys periodically:
2025 → key-A
2026 → key-B
2027 → key-C
Tokens can identify their signing key using:
{
"kid": "key-B"
}
During rotation, systems may temporarily need to support multiple trusted verification keys.
Key lifecycle management should consider:
Generation
Storage
Distribution
Activation
Rotation
Revocation
Retirement
Destruction
Key rotation is not simply about generating another key.
The entire lifecycle must be managed securely.
30. Private Key Leakage
With asymmetric JWT algorithms such as RS256, the private signing key is a critical asset.
If the private key is compromised, an attacker may potentially generate tokens that pass signature verification.
Private keys should never accidentally appear in:
Git repositories
Docker images
Application logs
Public storage buckets
Backups
CI/CD artifacts
Configuration repositories
Developer workstations
Support tickets
A leaked key may resemble:
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
Secret scanning, repository protection, access control, and key rotation procedures are therefore important parts of JWT security.
31. Sensitive Data Inside JWTs
Because JWT payloads can normally be decoded, designs such as:
{
"username": "kutay",
"password": "MyPassword123",
"credit_card": "..."
}
are dangerous.
A useful design rule is:
Do not place information inside a signed JWT if disclosure of that information would itself create a security problem.
Often, minimal claims are preferable:
{
"sub": "user-123",
"role": "user"
}
Even then, privacy requirements should be considered.
32. Excessive Token Lifetime
Long-lived access tokens increase the useful lifetime of stolen credentials.
Consider a token with an extremely distant expiration timestamp.
If the token is stolen today and remains valid for months, the security impact of that credential leak becomes much larger.
A common architecture is:
Short-Lived Access Token
+
Longer-Lived Refresh Token
However, there is no universally correct lifetime.
The appropriate value depends on:
Application sensitivity
User experience
Threat model
Revocation capabilities
Authentication strength
Business requirements
A financial application may reasonably use different session policies from a low-risk content platform.
33. Authentication Is Not Authorization
A valid JWT proves only what the application has configured it to prove.
It does not automatically mean the authenticated user has permission to access every requested resource.
Consider:
GET /api/users/500/orders
Authorization: Bearer <VALID_TOKEN>
Suppose the token belongs to:
user_id = 100
If the backend checks only:
Is JWT valid?
the application may allow one user to access another user's data.
The correct authorization flow should resemble:
JWT valid?
↓
Who is the user?
↓
Which resource is requested?
↓
Does this user have permission?
↓
Allow / Deny
JWT does not prevent vulnerabilities such as:
IDOR
BOLA
Broken Access Control
Privilege Escalation
These require proper server-side authorization controls.
34. Token Type Confusion
Modern authentication ecosystems may use several token types:
Access Token
Refresh Token
ID Token
Password Reset Token
Email Verification Token
These tokens serve different purposes.
For example, in OpenID Connect:
ID Token
communicates information about authentication and user identity.
An:
Access Token
is intended to authorize access to protected resources.
An API should not automatically accept every correctly signed token as an access token.
Validation should consider:
Who issued this token?
Who is it intended for?
What type of token is it?
What operations is it allowed to authorize?
35. Cross-Service JWT Reuse
Consider:
AUTH
|
-----------------------
| | |
PROFILE ORDERS PAYMENTS
Suppose every service uses:
Same secret
Same issuer
No audience validation
A token intended for one service may potentially be accepted by another.
This weakens service boundaries.
Sensitive microservice environments should therefore consider:
Audience validation
Scopes
Service-specific authorization
Appropriate key architecture
Least privilege
36. Scope Validation
OAuth-based access tokens may contain scopes such as:
{
"scope": "profile:read"
}
Suppose an endpoint performs:
DELETE /users/123
and requires:
users:delete
A backend that validates only the token's signature without validating its scope may allow unauthorized operations.
A robust authorization pipeline may resemble:
Valid Token
↓
Correct Issuer
↓
Correct Audience
↓
Required Scope
↓
Resource-Level Authorization
↓
Allow
Every layer solves a different security problem.
37. JWT Security Testing Checklist
During an authorized security assessment, the following checklist can help structure JWT analysis.
Token Structure
[ ] Decode Header
[ ] Decode Payload
[ ] Identify algorithm
[ ] Identify token type
[ ] Identify security-sensitive claims
Signature Validation
[ ] Is the signature actually verified?
[ ] Is "none" rejected?
[ ] Is an algorithm allowlist enforced?
[ ] Could algorithm confusion exist?
HMAC Secret Security
[ ] Is the secret sufficiently strong?
[ ] Are default secrets used?
[ ] Is the secret hard-coded?
[ ] Does the secret appear in repository history?
[ ] Is the same secret reused across environments?
Claims
[ ] Is exp validated?
[ ] Is nbf validated?
[ ] Is iss validated?
[ ] Is aud validated?
[ ] Is the token type validated?
Authorization
[ ] Are role checks enforced server-side?
[ ] Are scopes validated?
[ ] Is object-level authorization enforced?
[ ] Are administrative endpoints independently protected?
[ ] Are tenant boundaries enforced?
Token Storage
[ ] Is the token stored in LocalStorage?
[ ] Are cookies HttpOnly?
[ ] Are cookies Secure?
[ ] Is SameSite configured appropriately?
[ ] Could tokens leak through URLs?
[ ] Are tokens written to logs?
Token Lifecycle
[ ] Is the access-token lifetime reasonable?
[ ] Are refresh tokens rotated?
[ ] Is refresh-token reuse detected?
[ ] Does logout revoke server-side credentials?
[ ] Is there a revocation mechanism?
Key Management
[ ] Is kid securely handled?
[ ] Are jku sources restricted?
[ ] Are x5u sources restricted?
[ ] Are signing keys rotated?
[ ] Are private keys securely stored?
[ ] Are secrets isolated between environments?
38. JWT Security Testing Workflow
A structured JWT assessment can be visualized as:
JWT Identified
|
v
Decode Header & Payload
|
v
Identify Algorithm
|
+------ HS256
| |
| v
| Secret Security
|
+------ RS256 / ES256
|
v
Key Handling
|
v
kid / jku / x5u
|
v
Claim Validation
|
-------------------
| | | |
exp iss aud nbf
|
v
Authorization Tests
|
v
Roles / Scopes / Objects
|
v
Token Lifecycle
|
v
Refresh / Logout / Revocation
This workflow highlights an important point:
JWT assessment should not stop after inspecting the signing algorithm.
39. What Does a Secure JWT Implementation Look Like?
A strong JWT architecture relies on several independent security layers:
Strong Signing Algorithm
↓
Secure Key Management
↓
Strict Algorithm Allowlist
↓
Signature Verification
↓
Issuer Validation
↓
Audience Validation
↓
Expiration Validation
↓
Token Type Validation
↓
Scope / Role Validation
↓
Object-Level Authorization
↓
Secure Token Storage
↓
Refresh Token Security
↓
Revocation Strategy
No individual layer should be considered sufficient on its own.
40. Example Secure Validation Logic
Conceptually:
function validateToken(token):
header = parseHeader(token)
if header.algorithm != EXPECTED_ALGORITHM:
reject()
key = getTrustedKey(header.kid)
if key == null:
reject()
claims = verifySignature(token, key)
if claims.expired:
reject()
if claims.notBefore > currentTime:
reject()
if claims.issuer != EXPECTED_ISSUER:
reject()
if EXPECTED_AUDIENCE not in claims.audience:
reject()
return claims
Authorization should then be handled separately:
claims = validateToken(token)
user = loadUser(claims.sub)
if !user.active:
reject()
if !user.canAccess(resource):
reject()
allow()
This distinction is extremely important:
Authentication != Authorization
Authentication answers:
Who are you?
Authorization answers:
Are you allowed to perform this action?
JWT does not eliminate the need for either process.
41. Defensive JWT Security Checklist
Production JWT implementations should consider the following controls:
✓ Use strong signing keys
✓ Explicitly allowlist signing algorithms
✓ Always verify signatures
✓ Validate expiration
✓ Validate nbf where applicable
✓ Validate the expected issuer
✓ Validate the expected audience
✓ Validate token purpose/type
✓ Keep access tokens reasonably short-lived
✓ Protect refresh tokens
✓ Consider refresh token rotation
✓ Implement an appropriate revocation strategy
✓ Never place authentication tokens in URLs
✓ Avoid sensitive information inside JWT payloads
✓ Perform authorization server-side
✓ Validate scopes and permissions
✓ Enforce object-level authorization
✓ Treat kid as untrusted input
✓ Restrict jku/x5u key sources
✓ Secure signing keys
✓ Rotate keys appropriately
✓ Never hard-code production secrets
✓ Avoid unnecessary token logging
✓ Use HTTPS
✓ Monitor authentication anomalies
42. JWT Does Not Automatically Make an Application Secure
A common misconception is:
We use JWT
=
Our authentication is secure
In reality:
JWT
+
Key Management
+
Claim Validation
+
Authorization
+
Secure Storage
+
Session Lifecycle
+
Application Security
all contribute to the security of the authentication system.
An application can use perfectly signed JWTs and still contain:
IDOR / BOLA
Broken Access Control
XSS
CSRF
SSRF
SQL Injection
Business Logic Vulnerabilities
Privilege Escalation
JWT only addresses part of the overall application-security problem.
43. The Most Important Pentester Mindset
When a penetration tester encounters a JWT, the question should not simply be:
Can I break the JWT?
A much more useful question is:
What security decisions does this application trust the JWT to make?
Suppose the token contains:
{
"sub": "123",
"role": "user",
"tenant": "company-a",
"scope": "read"
}
This creates several areas to investigate:
How is sub used?
How is role validated?
Is tenant isolation enforced server-side?
Are scopes actually checked?
What happens if the user is disabled?
What happens if the user's role changes?
Does the old token retain previous permissions?
Does logout invalidate the credential?
Can the same token be used against another API?
Are critical authorization decisions based on stale claims?
In many real-world applications, the most serious JWT-related vulnerabilities are not weaknesses in the cryptographic algorithm itself.
They originate from mistakes in the surrounding:
Trust Model
Authorization Logic
Key Management
Token Lifecycle
Service Architecture
44. Conclusion
JWT is a powerful and flexible standard, but secure implementation requires significantly more than simply generating and verifying a token.
Important areas include:
Algorithm Validation
Signature Verification
Strong Secrets
Key Management
Claim Validation
Expiration
Issuer Validation
Audience Validation
Authorization
Token Storage
Refresh Token Security
Revocation
JWT security should never be reduced to:
Is the token signed?
Instead, security teams should ask:
How is the token generated?
↓
How is it verified?
↓
Where is it stored?
↓
Which services accept it?
↓
Which security decisions depend on it?
↓
How is it refreshed?
↓
How is it revoked?
The fundamental principle of secure JWT design is:
Never trust a security-sensitive value from a token until the token, its claims, its intended purpose, and the resulting authorization decision have all been properly validated.