padding: 1rem; }
API & Auth Security

JWT Security: Common Vulnerabilities & How to Fix Them

A practical, in-depth guide to understanding how JSON Web Tokens fail in real applications, how attackers abuse weak implementations, and how developers can build JWT-based authentication securely.

JWT Security Vulnerabilities Banner

JSON Web Tokens (JWTs) are widely used in modern APIs, web applications, mobile applications, and identity systems. They are particularly common in OAuth 2.0 and OpenID Connect environments, where signed tokens can carry information about an authenticated user, client, issuer, audience, and authorization context.

JWTs are useful, but they are also frequently misunderstood.

The biggest mistake developers make is treating a JWT as if it were a magic authentication mechanism. It isn't. A JWT is a standardized way of representing claims. Whether those claims are trustworthy depends on how the token is created, signed, transported, stored, and validated.

A token can have a perfectly valid cryptographic signature and still be unacceptable to your application.

For example, a token might:

That is why JWT security is not simply a matter of calling verify().

This guide explains the major JWT security problems developers and penetration testers encounter, why they happen, and what a robust implementation should actually validate.


What Is a JWT?

JWT stands for JSON Web Token.

A JWT is a compact, URL-safe representation of claims that can be digitally signed and, in a different form called JWE, encrypted.

A common signed JWT looks like this:

HEADER.PAYLOAD.SIGNATURE

For example:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjMiLCJpc3MiOiJodHRwczovL2lkcC5leGFtcGxlLmNvbSIsImF1ZCI6ImFwaS5leGFtcGxlLmNvbSIsImV4cCI6MTc2NTU1NTU1NX0
.
SIGNATURE

The three sections have different purposes.

1. Header

The header contains metadata describing how the token is represented and signed.

A typical header might contain:

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

The alg parameter identifies the cryptographic algorithm used for the JWS signature.

The typ parameter can identify the token type.

Some systems may also use parameters such as kid, which identifies a particular signing key.

Importantly, the header is attacker-controlled input.

Even though the legitimate server creates it, anyone holding a JWT can decode it and create a modified header.

Your application must therefore never assume that a value in the header is trustworthy simply because it appears inside a JWT.


2. Payload

The payload contains claims.

For example:

{
  "sub": "123456",
  "iss": "https://identity.example.com",
  "aud": "https://api.example.com",
  "iat": 1765550000,
  "exp": 1765550900,
  "role": "user"
}

Claims can describe things such as:

The payload of an ordinary signed JWT is not encrypted.

The JSON is Base64URL-encoded, which is an encoding mechanism rather than encryption.

Anyone who possesses the token can generally decode the header and payload.

Therefore:

Never treat a signed JWT as a secret container.

Do not place passwords, API keys, private keys, payment-card data, session secrets, or other confidential information inside an ordinary signed JWT.


3. Signature

The signature protects the integrity of the signed token.

Conceptually, the signature is calculated over the encoded header and encoded payload.

For a JWS, the signed input is effectively:

BASE64URL(header) + "." + BASE64URL(payload)

The cryptographic signature allows a verifier with the appropriate key to determine whether the signed content was altered.

If an attacker changes:

"role": "user"

to:

"role": "admin"

the original signature should no longer validate.

That is the fundamental security property provided by a signed JWT.

But there is an important distinction:

A valid signature does not automatically mean the token is valid for your application.

The application must also validate the token's context and claims.


JWT Is Not Encryption

One of the most persistent misconceptions about JWTs is that they hide information.

They don't, unless you are specifically using an encrypted JWT format such as JWE.

Consider:

{
  "sub": "123",
  "email": "[email protected]",
  "role": "admin"
}

If this is inside a normal signed JWT, the contents can be decoded by anyone who obtains the token.

The signature protects against unauthorized modification. It does not provide confidentiality.

This distinction is critical:

Property Signed JWT
Integrity Yes
Authenticity of signer Yes, assuming correct key validation
Confidentiality No
Tamper resistance Yes
Secret storage No

If confidentiality is required, use an appropriate encryption mechanism rather than assuming JWT signing provides it.


Vulnerability 1: Accepting alg: none

The none algorithm represents an unsecured JWT.

An unsecured JWT does not contain a cryptographic signature.

The problem occurs when an application's JWT validation logic accepts an attacker-controlled algorithm choice without enforcing the algorithms the application actually intends to support.

An attacker may modify the header from:

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

to:

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

If the application then accepts a token without performing the required cryptographic verification, an attacker could potentially modify claims without possessing the legitimate signing key.

For example, changing:

{
  "sub": "1001",
  "role": "user"
}

to:

{
  "sub": "1",
  "role": "admin"
}

would become a serious privilege-escalation vulnerability.

The real lesson

The problem isn't simply that "none" exists.

The real problem is trusting an attacker-controlled algorithm selection or incorrectly configuring the JWT library.

Modern JWT libraries generally do not accept unsecured tokens by default, but developers should never depend on accidental library defaults for security.

Fix

Explicitly configure the algorithms your application expects.

For example, with Node.js jsonwebtoken:

jwt.verify(token, publicKey, {
  algorithms: ["RS256"]
});

The application should reject tokens using algorithms outside the expected allowlist.

Do not dynamically select the verification algorithm based solely on the token's alg value.


Vulnerability 2: Algorithm Confusion

Algorithm confusion is a different and particularly important JWT implementation flaw.

It can occur when a system supports both symmetric and asymmetric algorithms but incorrectly handles keys according to the algorithm specified by the token.

For example:

With RS256:

Private key → signs
Public key  → verifies

With HS256:

Shared secret → signs
Shared secret → verifies

The security model is completely different.

A badly designed verification implementation may accidentally allow an attacker to manipulate the token's declared algorithm and cause the server to use a public key in a manner intended for HMAC verification.

The result can be catastrophic if the implementation accepts the wrong algorithm/key combination.

Fix

Do not merely check that an algorithm exists.

Bind the expected algorithm to the expected key type and trust context.

For example:

Expected token:
RS256 + RSA public key

should not silently become:

HS256 + RSA public key

Your application should have an explicit cryptographic policy such as:

Issuer A
    ↓
RS256
    ↓
Approved RSA public key
    ↓
Allowed audiences

Do not allow arbitrary combinations.


Vulnerability 3: Improper Signature Validation

A signed JWT should never be accepted merely because it has the correct structure.

The application must cryptographically verify the signature.

A common conceptual mistake looks like this:

decode(token)
↓
read payload
↓
trust user ID

That is not authentication.

Decoding a JWT and verifying a JWT are completely different operations.

A decoder answers:

"What data is inside this token?"

A verifier answers:

"Was this data validly signed by a trusted signer under the expected cryptographic rules?"

Never confuse the two.

Fix

The validation pipeline should look more like:

Receive token
      ↓
Parse safely
      ↓
Determine whether token type is expected
      ↓
Apply allowed algorithm policy
      ↓
Select trusted verification key
      ↓
Verify cryptographic signature
      ↓
Validate registered claims
      ↓
Validate application-specific requirements
      ↓
Authorize requested operation

Every stage matters.


Vulnerability 4: Weak HS256 Secrets

HS256 is a symmetric HMAC-based signing algorithm.

The same secret is used to create and verify the signature.

That means the security of the system depends heavily on the secrecy and strength of that secret.

Weak examples include:

secret
password
12345678
jwtsecret
companyname
admin123

If an attacker obtains a valid HS256 token, they may be able to perform an offline guessing attack against a weak secret.

Unlike an online password attack, offline guessing does not require repeatedly interacting with the target server.

The attacker can test candidate secrets locally.

If the secret is discovered, the attacker can potentially create valid-looking tokens that pass signature verification.

Fix

Use a cryptographically random secret with sufficient entropy.

Do not derive the signing secret from:

Store signing secrets in an appropriate secret-management system rather than committing them to source control.

For larger systems, consider a dedicated key-management service or hardware-backed key infrastructure where appropriate.


Vulnerability 5: Hardcoded Signing Secrets

A strong secret is useless if it is publicly exposed.

One common mistake is putting something like this directly into application code:

const JWT_SECRET = "super-long-secret";

The secret may eventually appear in:

Better approach

Use a dedicated secrets-management mechanism.

Examples include:

Environment / secret manager
        ↓
Application
        ↓
JWT signing operation

Do not expose the secret to frontend code.

Do not put signing secrets inside mobile applications.

Do not assume that obfuscating a secret inside an Android or desktop application makes it secret.

If a secret is embedded in software distributed to users, assume a determined attacker can eventually extract it.


Vulnerability 6: Trusting the Payload Without Authorization Checks

A JWT may contain:

{
  "sub": "12345",
  "role": "admin"
}

The presence of "role": "admin" does not mean your application should automatically authorize every administrative operation.

Authentication and authorization are different decisions.

Authentication asks:

Who is this principal?

Authorization asks:

Is this principal allowed to perform this particular operation on this particular resource?

Suppose an API receives:

GET /users/500/profile

The fact that the JWT is valid does not automatically mean the authenticated user can access user 500.

Your application still needs an authorization decision.

Fix

Perform authorization based on the application's security model.

For example:

Valid JWT
   ↓
Authenticated principal = user 123
   ↓
Requested resource = user 500
   ↓
Authorization policy
   ↓
DENY

JWT validation should never replace proper authorization logic.


Vulnerability 7: Missing exp

The exp claim defines the expiration time of a token.

If an application issues tokens without meaningful expiration controls, a stolen token may remain usable for an unnecessarily long period.

Imagine an access token being stolen from a compromised endpoint.

If that token remains valid indefinitely, the attacker may continue using it until another control invalidates it.

That creates a much larger attack window.

Fix

Use expiration for access tokens.

For example:

{
  "sub": "123",
  "iat": 1765550000,
  "exp": 1765550900
}

The exact lifetime should depend on the application.

There is no universal "correct" lifetime.

High-risk applications may require shorter lifetimes, while other environments may tolerate longer ones.

The important point is that expiration should be deliberate rather than accidental.


Vulnerability 8: Incorrect exp, nbf, and Clock Validation

Including time-based claims isn't enough.

The application must actually validate them.

Important registered claims include:

exp

Expiration time.

The token should not be accepted after the expiration time, subject to carefully controlled clock-skew handling.

nbf

Not-before time.

A token should generally not be accepted before the specified time.

iat

Issued-at time.

This identifies when the token was issued, although its presence alone does not make a token valid.

Developers should also avoid allowing excessive clock skew.

A huge tolerance can effectively extend token lifetimes.

Fix

Use a trusted JWT library and configure reasonable clock tolerance.

Do not implement time validation with ad-hoc string comparisons or custom logic unless there is a strong reason to do so.


Vulnerability 9: Missing Issuer Validation

The iss claim identifies the issuer of a token.

Consider an environment with multiple identity providers or multiple applications.

A token might have a valid signature but originate from an issuer your API does not trust.

If the API verifies only the signature and ignores the issuer, it may accept a token outside its intended trust relationship.

Fix

If your architecture depends on an issuer, explicitly validate it.

For example:

{
  "iss": "https://identity.example.com"
}

The API should compare the claim against its configured trusted issuer.

Do not treat any validly signed token from any issuer as automatically acceptable.


Vulnerability 10: Missing Audience Validation

The aud claim identifies the intended audience of a token.

This is especially important when one identity provider issues tokens for multiple services.

Imagine:

Identity Provider
       │
       ├── API A
       ├── API B
       └── API C

A token intended for API A should not automatically be accepted by API C merely because it was signed by the same trusted identity provider.

Example

{
  "iss": "https://identity.example.com",
  "aud": "payments-api"
}

The payments API should verify that the token is actually intended for it.

Fix

Validate the expected audience according to your architecture.

This is one of the easiest JWT checks to overlook and one of the most important in multi-service environments.


Vulnerability 11: Incorrect kid Handling

The kid claim can identify which signing key was used.

For example:

{
  "alg": "RS256",
  "kid": "key-2026-01"
}

This becomes important when a system rotates signing keys.

However, kid is also attacker-controlled input.

A dangerous implementation may treat it as an arbitrary filename, database identifier, URL, or filesystem path without proper validation.

The problem isn't kid itself.

The problem is using attacker-controlled key identifiers in unsafe ways.

Fix

Maintain a trusted mapping:

kid
 ↓
trusted key

Do not allow arbitrary user-controlled paths or URLs to determine where verification keys are loaded from.

If using JWKS, fetch keys only from trusted, configured locations and apply appropriate caching and validation.


Vulnerability 12: Token Confusion

A common mistake in larger systems is treating different token types as interchangeable.

For example:

Access token
ID token
Refresh token
Email verification token
Password reset token
API token

These tokens may all use JWT-like structures, but their intended purposes are different.

An ID token is intended to communicate authentication information to a client application in an OpenID Connect flow.

An access token is intended for access to protected resources.

A refresh token has a different lifecycle and purpose.

Accepting one token type where another is expected can create serious security problems.

Fix

Define token purpose explicitly.

Validate not only the cryptographic signature but also the token's intended use and relevant claims.

Do not build a generic:

verifyAnyJWT()

function and use it everywhere without considering token type and trust context.


Vulnerability 13: Putting Sensitive Information in the Payload

A JWT payload may be easy to decode.

Therefore, avoid storing:

{
  "password": "...",
  "credit_card": "...",
  "private_key": "...",
  "api_secret": "..."
}

Even if the JWT is signed correctly, those values remain readable to anyone who obtains the token.

A better payload contains only the information actually needed.

For example:

{
  "sub": "123456",
  "iss": "https://identity.example.com",
  "aud": "https://api.example.com",
  "exp": 1765550900
}

Application-specific claims can be included where justified, but unnecessary data increases exposure and makes token management harder.

Important distinction

A signed JWT protects integrity.

It does not automatically provide confidentiality.


Vulnerability 14: Excessive Information in JWTs

Even non-secret information can become a privacy problem if the token contains too much.

Developers sometimes put entire user profiles inside JWTs:

{
  "name": "...",
  "email": "...",
  "phone": "...",
  "address": "...",
  "department": "...",
  "permissions": [...],
  "preferences": [...],
  "internal_flags": [...]
}

That creates unnecessary exposure.

JWTs are often copied into:

The more information inside the token, the more information can leak through those secondary systems.

Better principle

Put only what the verifier actually needs.


Vulnerability 15: Storing JWTs in localStorage

Browser applications sometimes store access tokens like:

localStorage.setItem("jwt", token);

This is convenient but creates a major concern.

JavaScript running in the application's origin can generally access localStorage.

Therefore, if an attacker achieves XSS, malicious JavaScript may be able to read the token.

For example, conceptually:

const token = localStorage.getItem("jwt");

If that credential is a bearer access token, possession may be enough to authenticate as the victim.

Fix

The first priority is still:

Prevent XSS.

Use:

For browser authentication, an HttpOnly cookie can also prevent JavaScript from directly reading the authentication cookie.

But that leads to another security consideration: CSRF.


Vulnerability 16: Misunderstanding HttpOnly

An HttpOnly cookie cannot be read directly through normal JavaScript APIs such as document.cookie.

That is useful against token extraction through XSS.

But HttpOnly does not stop XSS.

Suppose malicious JavaScript executes inside the victim's origin.

It may not be able to read the cookie, but the browser may still automatically attach that cookie to requests.

The attacker may therefore be able to perform actions as the victim even without seeing the token.

This distinction is critical.

Think of it this way

HttpOnly helps protect:

Token confidentiality from JavaScript

It does not guarantee:

Application integrity against XSS

XSS remains a serious vulnerability and should be fixed directly.


Vulnerability 17: CSRF When Using Cookies

When JWTs are stored in cookies, the browser can automatically send them with matching requests.

That introduces a CSRF consideration.

An attacker may attempt to cause the victim's browser to send an authenticated request to the target application.

Therefore, switching from localStorage to cookies is not a complete security solution.

Cookie defenses

Depending on the application, configure cookies with appropriate attributes such as:

HttpOnly
Secure
SameSite

For example:

Set-Cookie: access_token=...; HttpOnly; Secure; SameSite=Lax

The correct SameSite setting depends on the application's architecture.

For state-changing operations, additional CSRF defenses may also be required.

Possible approaches include:

Do not blindly copy one cookie configuration into every application.


Vulnerability 18: Long-Lived Access Tokens

A token valid for several days may be convenient, but convenience increases the impact of theft.

Consider:

15-minute access token

versus:

30-day access token

If both are stolen, the second credential potentially provides a much larger window for abuse.

Short-lived access tokens reduce the lifetime of a stolen credential.

But extremely short lifetimes can also create unnecessary complexity and refresh traffic.

The correct lifetime is therefore a risk decision.

Better architecture

A common approach is:

Short-lived access token
          +
Longer-lived refresh token
          +
Refresh-token rotation
          +
Reuse detection

This provides a better balance between usability and security than making the access token itself extremely long-lived.


Vulnerability 19: Poor Refresh Token Handling

Refresh tokens deserve special attention.

A refresh token is effectively a credential that can be exchanged for new access tokens.

If an attacker steals one, the attacker may be able to continue obtaining access tokens even after an individual access token expires.

Refresh-token rotation

A stronger architecture uses rotation.

Conceptually:

Refresh Token A
      ↓
Exchange
      ↓
Access Token B
Refresh Token B

The previous refresh token becomes invalid.

If the server later sees Refresh Token A being reused, that can indicate theft or replay.

The server can then invalidate the associated refresh-token family/session and require reauthentication.

Important point

Refresh-token rotation is not mandatory for every architecture, but it is an important security pattern when long-lived refresh credentials are used.


Vulnerability 20: No Revocation Strategy

JWTs are often described as "stateless."

That is useful, but it creates a trade-off.

If a server issues a self-contained access token that remains valid until expiration, simply deleting something from a database does not automatically invalidate the token.

The token may continue to pass cryptographic verification.

This becomes a problem during:

Possible approaches

Depending on your architecture, you can use:

There is no universal revocation mechanism.

The important thing is to understand the trade-off between stateless validation and immediate invalidation.


Vulnerability 21: Assuming Logout Automatically Invalidates a JWT

This is a common misconception.

Suppose a user clicks:

Logout

The browser removes the token.

That does not necessarily invalidate a copy of the token that an attacker already stole.

If the API has no server-side revocation mechanism and the token has not expired, the attacker may still be able to use it.

Better approach

For browser applications, invalidate the client-side session and handle refresh-token/session revocation server-side where appropriate.

Use short-lived access tokens to limit the usefulness of stolen access credentials.

For high-risk applications, implement stronger server-side session controls.


Vulnerability 22: Key Rotation Problems

Signing keys should not remain unchanged forever.

Eventually you may need to:

A production system should be designed for key rotation before it becomes an emergency.

A typical asymmetric setup may look like:

Old private key ── signs/previously signed tokens
Old public key  ── verifies existing tokens

New private key ── signs new tokens
New public key  ── verifies new tokens

During a controlled rotation, the service may temporarily publish both old and new verification keys.

The kid claim can identify which key was used.

Important principle

Key rotation should be planned, tested, monitored, and reversible where possible.

Do not wait until a key is compromised to discover that your architecture has no practical rotation mechanism.


Vulnerability 23: Hardcoding Trust in the Token

A dangerous mindset is:

"If the JWT says the user is an administrator, the user is an administrator."

The JWT is only one input into the authorization system.

The application should define its own security policy.

For example:

JWT:
sub = 123
role = admin

Request:
DELETE /users/456

The application should still determine:

A token claim should not become a shortcut around authorization.


Vulnerability 24: Using One JWT Validation Policy Everywhere

Different services may have different trust requirements.

For example:

Web application
Mobile API
Admin API
Payment API
Internal service

Using one generic JWT validation function everywhere can create dangerous assumptions.

An admin service may require:

issuer = X
audience = admin-api
algorithm = RS256
scope = admin

while another service may require:

issuer = X
audience = mobile-api
algorithm = RS256
scope = user

Better approach

Define explicit validation policies per trust boundary.

The question should not simply be:

"Is this JWT valid?"

It should be:

"Is this particular token valid and acceptable for this particular security context?"

That is a much stronger security model.


JWT Validation: What Should the Server Actually Check?

A robust JWT validation process should consider several independent properties.

A simplified model is:

1. Receive token
        ↓
2. Parse safely
        ↓
3. Apply token-type policy
        ↓
4. Enforce expected algorithm
        ↓
5. Select trusted key
        ↓
6. Verify signature
        ↓
7. Validate issuer
        ↓
8. Validate audience
        ↓
9. Validate expiration
        ↓
10. Validate not-before
        ↓
11. Validate required claims
        ↓
12. Apply authorization policy
        ↓
13. Allow or deny request

Not every application requires every claim, but the validation policy should be deliberate.


Example Secure Policy

Suppose an API expects access tokens issued by:

https://identity.example.com

and intended for:

https://api.example.com

A reasonable policy could be:

Algorithm:
RS256

Issuer:
https://identity.example.com

Audience:
https://api.example.com

Required:
sub
iss
aud
exp

Optional:
iat
nbf
scope

The exact values depend on the application.

The important point is that they are configured by the server, not discovered dynamically from attacker-controlled token content.


HS256 vs RS256

Both algorithms can be secure when correctly implemented.

HS256

HS256 uses one shared secret.

        Shared Secret
          /       \
       Sign      Verify

Advantages:

Disadvantages:


RS256

RS256 uses asymmetric cryptography.

Private key → Sign
Public key  → Verify

Advantages:

Disadvantages:

Important conclusion

Do not say:

"RS256 is always more secure."

Instead:

Choose an algorithm based on your architecture, key-management model, ecosystem support, and security requirements.


Should You Use JWT at All?

This question is often ignored.

JWTs are not automatically better than server-side sessions.

For a traditional web application, a server-side session may be simpler:

Browser
   ↓
Session cookie
   ↓
Server-side session
   ↓
User state

The server can immediately invalidate the session.

JWT-based systems often move more state into the token:

Client
   ↓
JWT
   ↓
API validates token

This can be useful for distributed systems, but it also creates additional responsibilities around:

The right question

Don't ask:

"How can I use JWT?"

Ask:

"Do I actually need JWT for this security architecture?"

Sometimes the answer is yes.

Sometimes a conventional server-side session is the better engineering choice.


JWT and OAuth 2.0 Are Not the Same Thing

Another common misunderstanding is treating JWT and OAuth as interchangeable.

They are not.

JWT is a token format.

OAuth 2.0 is an authorization framework.

OpenID Connect builds an identity layer on top of OAuth 2.0.

A system can use OAuth 2.0 with JWT access tokens, but OAuth does not require every token to be a JWT.

Likewise, using JWT does not automatically mean an application is implementing OAuth correctly.

This distinction matters when designing authentication and authorization systems.


JWT and OpenID Connect

OpenID Connect introduces the concept of an ID token.

An ID token is intended to communicate authentication information to the client application.

An access token is intended for access to a protected resource.

They serve different purposes.

A common architectural mistake is taking an ID token and sending it to an API as if it were an access token.

That can create token-confusion vulnerabilities.

General rule

Validate tokens according to their intended purpose.

Do not assume:

"JWT" = "usable everywhere"

Secure JWT Storage in Browser Applications

There is no universal storage mechanism that eliminates every browser security risk.

Two commonly discussed approaches are:

JavaScript-accessible storage

Examples:

localStorage
sessionStorage
in-memory application state

The major concern is token exposure if malicious JavaScript executes within the application origin.

HttpOnly cookies

The browser can send the cookie automatically while JavaScript cannot directly read an HttpOnly cookie.

This can reduce token extraction through XSS.

However, cookie-based authentication introduces CSRF considerations.

Practical principle

Don't ask only:

"Where is the token stored?"

Also ask:

Security is about the complete system, not one storage API.


JWTs in Mobile Applications

Mobile applications have a different threat model.

Do not embed a JWT signing secret inside the application and assume it is protected.

A mobile application distributed to users should be treated as potentially inspectable.

For authentication credentials, use the platform's secure credential-storage mechanisms where appropriate.

Also consider:

A JWT design that is acceptable for a browser application may not be appropriate for a mobile application without modification.


Logging JWTs Is a Security Mistake

Developers sometimes log complete authorization headers during debugging:

Authorization: Bearer eyJ...

This is dangerous.

A bearer token should generally be treated as a credential.

If it appears in:

anyone who gains access to those systems may potentially obtain usable credentials.

Better practice

Redact credentials.

Instead of:

Authorization: Bearer eyJhbGciOi...

log something like:

Authorization: [REDACTED]

If token correlation is needed, use carefully designed identifiers rather than storing the complete credential.


JWTs in URLs

Avoid putting bearer tokens in URLs whenever possible.

For example:

https://example.com/download?token=eyJ...

URLs can leak through:

Use appropriate authorization headers or secure cookie-based mechanisms instead, depending on the architecture.


Don't Put JWTs in Error Messages

Error handling should never accidentally expose credentials.

Avoid responses such as:

{
  "error": "Invalid token",
  "token": "eyJ..."
}

or stack traces that include:

Authorization: Bearer ...

Production error messages should contain enough information for the client to understand the failure without exposing authentication material.


Secure Transport Still Matters

JWT security does not replace HTTPS.

A correctly signed JWT can still be stolen if the transport or endpoint is compromised.

Always protect authentication traffic using TLS.

For browser cookies, the Secure attribute should be used so the browser sends the cookie only over secure connections.

Also consider the security of:

"Internal network" does not automatically mean "trusted network."


What Happens If the Signing Key Is Compromised?

This is one of the most important questions in JWT security.

If an attacker obtains an HS256 signing secret, they may be able to forge tokens.

If an attacker obtains an asymmetric private signing key, they may also be able to create valid tokens.

That means signing-key compromise can be much more serious than the theft of a single access token.

A mature architecture should therefore have a key-compromise plan.

That plan may include:

Detect compromise
      ↓
Stop signing with compromised key
      ↓
Publish replacement verification key
      ↓
Rotate signing key
      ↓
Invalidate/reject affected credentials
      ↓
Investigate exposure
      ↓
Reauthenticate affected users where necessary

The exact procedure depends on the architecture.


JWT Security Testing

From a penetration-testing perspective, JWT testing should focus on the application's actual trust assumptions.

A security assessment should examine questions such as:

Algorithm handling

Signature verification

Claims

Key management

Authorization

Token lifecycle

Token exposure

The goal isn't simply to find a malformed JWT.

The goal is to determine whether an attacker can turn a JWT implementation weakness into authentication bypass, account takeover, privilege escalation, unauthorized access, or persistent session abuse.


A Secure JWT Architecture

A reasonably mature architecture may look like this:

                    Identity Provider
                           │
                           │
                     Authenticate
                           │
                           ▼
                  Issue access token
                           │
                           ▼
                ┌────────────────────┐
                │ Short-lived JWT    │
                │                    │
                │ iss                │
                │ aud                │
                │ sub                │
                │ exp                │
                │ iat                │
                │ scope              │
                └────────────────────┘
                           │
                           ▼
                       API Gateway
                           │
                 Validate JWT policy
                           │
          ┌────────────────┴────────────────┐
          ▼                                 ▼
      Authentication                    Authorization
          │                                 │
          │                                 │
          └──────────────┬──────────────────┘
                         ▼
                   API operation

Refresh tokens can be handled separately through a dedicated token lifecycle mechanism.


A Practical JWT Validation Checklist

Before accepting a JWT, your application should have an explicit answer to these questions:

Cryptographic validation

Registered claims

Token purpose

Authorization

Lifecycle

Key management

Storage and exposure


Example Secure Policy

A production API might define a policy similar to:

Token type:
Access token

Algorithm:
RS256

Issuer:
https://identity.example.com

Audience:
https://api.example.com

Required claims:
sub
iss
aud
exp

Optional claims:
iat
nbf
scope

Access-token lifetime:
Short-lived

Refresh tokens:
Rotated

Refresh-token reuse:
Detected and revoked

Transport:
TLS

Cookie storage:
HttpOnly + Secure + appropriate SameSite policy

CSRF:
Protected where cookie-based authentication requires it

Signing keys:
Managed centrally and rotated periodically

This is an example architecture, not a universal configuration.

Your actual values should be determined by your application's threat model and identity architecture.


Common Developer Mistakes

These are some of the JWT mistakes worth looking for during code reviews:

Mistake 1

Decode JWT → trust payload

Problem: decoding is not verification.


Mistake 2

Accept whatever `alg` the token specifies

Problem: attacker-controlled algorithm selection.


Mistake 3

Use "secret123" as HS256 secret

Problem: weak signing key.


Mistake 4

JWT_SECRET = "..."

inside source code.

Problem: secret exposure through source control and build artifacts.


Mistake 5

Check signature only

Problem: a correctly signed token can still have the wrong issuer, audience, token type, or authorization context.


Mistake 6

Use ID token as API access token

Problem: token-purpose confusion.


Mistake 7

Put JWT into URL

Problem: credential leakage through logs, history, analytics, and referrers.


Mistake 8

Log Authorization header

Problem: credentials can end up in centralized logs.


Mistake 9

Store long-lived bearer token in localStorage

Problem: increased impact if XSS exposes the token.


Mistake 10

HttpOnly cookie = XSS solved

Problem: HttpOnly prevents JavaScript from reading the cookie; it does not eliminate XSS or necessarily prevent authenticated actions initiated by malicious JavaScript.


The Most Important Principle

JWT security can be summarized in one sentence:

Never trust anything inside a JWT until the token has been validated according to an explicit security policy.

That includes:

alg
kid
sub
iss
aud
exp
nbf
role
scope
permissions

Some values may become trustworthy after cryptographic verification, but that still doesn't mean they automatically authorize every action.

A secure system separates:

Token authenticity
        ↓
Claim validation
        ↓
Authentication
        ↓
Authorization
        ↓
Application security policy

Each layer has a different job.


Final JWT Security Checklist

Before deploying a JWT-based authentication system, verify all of the following:


Conclusion

JWTs are neither inherently insecure nor inherently secure.

They are a tool.

Used correctly, signed JWTs can work well in distributed API architectures, especially when combined with well-designed identity systems, strong key management, explicit validation policies, short-lived access credentials, and carefully implemented refresh-token handling.

Used incorrectly, JWTs can turn a small implementation mistake into a complete authentication bypass.

The biggest lesson is that signature verification is only one part of JWT security.

A secure API should determine:

Who issued this token?
        ↓
Was it signed correctly?
        ↓
Was the expected algorithm used?
        ↓
Is the signing key trusted?
        ↓
Is the token intended for this API?
        ↓
Is it still valid?
        ↓
Is it the correct type of token?
        ↓
What principal does it represent?
        ↓
Is that principal authorized for this operation?

That is the mindset developers should adopt.

Don't build authentication around the assumption that:

"The JWT is valid, therefore everything inside it can be trusted."

Instead, build a clearly defined trust boundary around every token.

Verify the cryptography. Validate the claims. Confirm the token's purpose. Enforce authorization. Protect the token throughout its lifecycle.

That is what turns JWT from a convenient token format into a properly engineered authentication component.

For security professionals and developers, the goal should not be to simply make a JWT difficult to forge. The goal is to make the entire authentication and authorization system resilient when tokens are malformed, stolen, replayed, misissued, expired, or deliberately manipulated.