Is My JWT Secure? How Token Verification Gets Bypassed
Most JWT security failures do not break cryptography; they exploit a verifier that accepts an attacker-written token or never verifies it at all. The root mistake is letting unverified token data influence how code checks it. This guide explains the main attack paths and the validation rules that stop them.
You verified the signature, so you feel safe. That instinct is reasonable, but “verified” and “verified correctly” are not the same thing. Many JWT security incidents arise in that gap.
One key fact frames the whole topic: many JWT security failures do not break cryptography. Attackers exploit verifier configuration, key handling, or application logic rather than reverse SHA-256 or factor an RSA key. Their goal is to make your code mark an attacker-created token as valid.
As the first article explains, a token’s header and payload are attacker-controlled input until its signature is verified. The header can contain fields such as alg and kid that affect the verification flow. A verifier that treats those unverified fields as instructions instead of input to check can make the wrong trust decision. Once signature verification succeeds, the protected header is covered by the signature; the risk exists only before verification.
So do not ask only whether JWT is inherently safe. Ask instead: has my verifier handed a decision that belongs to the server to the token? The attacks below share that pattern: the token influences a decision the verifier should make itself. The defense is simple: treat the token as data to validate, not as instructions to execute.
The core problem: why does the verifier accept a token it should reject?
Return to the two questions from the first article: what does the token say, and should I trust it? You can read the first directly; the second requires verification. The attacks in this article target the second question in five common ways:
- Skip verification — make the verifier conclude there is nothing to verify.
- Use the wrong key — make it use a key the attacker can obtain.
- Control key selection — make it select or fetch a key the attacker controls.
- Guess the key — exploit a weak secret instead of bypassing verification.
- Use a valid token at the wrong service — the token is genuine, but not issued for that service.
Four of these attacks do not weaken the signature algorithm. They bypass or misdirect the verification process. There is also a more basic failure: verification never happens at all.
The most basic error: no signature verification
The most common and easiest-to-miss error is never calling the verification function. In code, reading a token and verifying one can look very similar. Many libraries provide separate decode and verify APIs: decoding reads claims and may return the full payload, but does not authenticate it; verification checks the signature with the expected key and rules. If an application reads user.role directly from decoded data, its tests may pass without showing that signature verification was skipped. An attacker then only needs to change the payload.
This is the first article’s most important conclusion: decoding is not trusting. When reviewing JWT security, first confirm that the application calls verification, supplies the correct key, and bases later decisions on the verified result. Paste any token into the JWT inspector without a key and it becomes readable JSON, but its status remains “Decoded, not verified”. Decoding is not a trust decision; the same boundary applies in your backend.
Attack 1: bypass verification through alg: none
JWA defines an unsecured JWS: its alg is none and its signature is empty. It is intended only for special cases where another protocol layer guarantees integrity. If a verifier selects its verification method from the header’s alg without checking the server allow-list and key type, an attacker can change alg to none, remove the signature, and construct any payload. The verifier then mistakes “no signature” for “no verification required” and accepts a forged token.
A conformant modern library rejects an unsecured JWS unless the application explicitly allows it. In practice, risk usually comes from old dependencies, wrappers that hide algorithm configuration, or settings that disable this protection. Do not treat “no error” as evidence that verification completed securely.
The verifier may read alg from the unverified header, but only as input to compare. It should continue only when the value matches the server’s configured algorithms and key type; otherwise it must reject the token, including none. This site’s JWT inspector offers no none verification mode for the same reason.
Attack 2: algorithm and key-type confusion
RS256–HS256 confusion treats a public RSA key as an HMAC secret.
With RS256, the issuer signs using an RSA private key and the service verifies with its public key. That public key may be distributed openly. If a verifier lets the token’s alg choose the algorithm, an attacker can change RS256 to HS256 and calculate a MAC using the public-key bytes as the HMAC secret. If the service then incorrectly runs HMAC with that same public key, the MAC matches and the forged token is accepted.
The root cause is that the server did not fix the algorithm and key type. Limit accepted algorithms in configuration and bind every key to one algorithm use. For example, an RSA public key may verify RS256 but must never be an HS256 HMAC secret. Modern libraries usually require an explicit allow-list; a library or wrapper that still infers algorithms from the header remains a risk. The RSA key generator makes the distinction clear: a public key is meant to be shared, so it cannot safely be used as a symmetric secret.
Attack 3: unsafe key lookup with kid, jku, and x5u
The first two attacks make the verifier misuse a key it already has. This group instead tries to influence which key the verifier uses.
kid (key ID) locates a candidate key from a key set, but before verification it is attacker-controlled text. Building a file path, SQL query, or LDAP query directly from it can introduce path traversal or injection. Use it only as an opaque index into a fixed, trusted key set or cached JWKS. Reject an unknown kid; never use it to construct a path, query, or URL.
jku (JWK Set URL) and x5u (X.509 URL) can point to remote key material. Supporting them is not inherently unsafe; blindly following any URL supplied by a token is. Most services should ignore them and use only preconfigured issuer metadata. If they are needed, restrict trusted hosts, require HTTPS and certificate validation, and make requests without cookies or credentials. You can inspect these fields in the JWT inspector; it displays the header but never fetches its URLs. If a token references an X.509 certificate, inspect its subject and validity with the certificate decoder rather than trusting the header’s destination.
Attack 4: weak HMAC secrets
Sometimes no bypass is needed: the HMAC secret is simply weak. HS256 is only as strong as its shared secret, and values such as secret, password, changeme, or an app name are easy to guess. With one valid token, an attacker can calculate HMACs against a wordlist offline until one matches. There is no server-side rate limit or lockout. Once the secret is known, the attacker can issue arbitrary tokens because the HMAC verification secret is also the signing secret.
Length and entropy are different. RFC 7518 requires an HMAC key at least as long as the hash output — 256 bits for HS256 — but a long human-created secret can still be guessable. Generate at least 32 random bytes with a CSPRNG, encode them as your library requires, and store the result as a managed secret rather than source code. The password generator uses Web Crypto randomness for high-entropy development or test strings; production keys should follow your platform’s approved key-management process.
Also consider who holds the key. Where a verifier must not be able to issue tokens, or verification crosses a trust boundary you do not fully control, prefer public-key signatures (RS/ES/EdDSA). A leak of verification material then cannot forge tokens. To test whether a candidate key verifies a token, paste both into the JWT inspector; “Signature verified” only means that key verifies that token. It is a debugging aid, not a reason to test production keys in an unapproved environment.
Attack 5: missing audience, issuer, or type checks
This is not a forgery: the token has a valid, unexpired signature but should not be accepted by the current service.
A valid signature proves only that the token was not changed after signing and that its signature verifies with the expected key. It does not establish the caller’s identity or permissions. Those depend on claims such as iss, sub, and aud, the token type, and server rules. If you skip aud and iss after verifying the signature, you can create a confused deputy: a token legitimately issued for service A is replayed to service B, which accepts it because the signature is valid. After signature verification, compare aud and a trusted iss exactly; do not use prefix matching or a loose regular expression for the audience.
The concrete case from the first article is an OIDC ID token presented to an API that should accept only an access token. Because aud alone does not always separate them, check the token type as well. JWT access tokens following RFC 9068 use at+jwt; in other cases, define and enforce the type or profile your protocol expects. An endpoint requiring an access token must not accept an ID token, and vice versa. Different credentials need mutually exclusive validation rules, not audience matching alone.
A stolen token also needs no forgery. A verifier cannot distinguish its legitimate holder from an attacker who copied it from a log or compromised device; both pass verification while it remains valid. A jti (token ID) does not stop replay by itself. It helps only with server-side state, such as an atomically checked store of consumed or revoked IDs. Sender-constrained tokens, including DPoP and mutual TLS, bind a token to a client-held key through cnf. They do not automatically invalidate a stolen token, but they reduce the value of replaying a copied bearer token when the client private key remains protected. Short lifetimes and revocation are important as well.
Build a verification process you can trust
Instead of patching one issue at a time, build a verification process in which server configuration makes decisions from start to finish:
- Resolve keys from configuration, not from the token. Know each issuer’s signing keys ahead of time — a pinned JWKS URI from trusted issuer metadata, or keys you hold directly. Use the header’s
kidonly to select among those; on an unknownkid, refresh the JWKS once and, if it’s still missing, reject rather than reach outward. A failed rotation should fail closed. - Pin algorithms and bind them to keys. Accept only the algorithms you configured, and only with the key type each is bound to — the two rules from Attacks 1 and 2, enforced before any signature math.
- Bound the input. Reject oversized tokens, and reject any
critheader whose extensions you don’t understand — the spec requires it, and it stops a token from demanding processing you never signed up for. - Verify, then validate the claims. Only after the signature checks out, validate
expandnbf(allowing modest clock skew), theniss,aud, and the tokentyp, plus any claims your endpoint requires — comparing each exactly. - Authenticate at the door, authorize on every request. A verified token is authenticated, not authorized; enforce permissions server-side per request. And when authorization must change immediately — a revoked role, a disabled account — don’t lean on a
roleclaim that’s frozen until the token expires; check live state.
Some of these patterns first appeared as library CVEs; others arise entirely in application configuration and key-lookup code. Use a well-maintained JWT library and keep it patched instead of hand-rolling verification, then configure it narrowly. Keep accepted algorithms, key sources, and token size tightly bounded. A smaller surface leaves fewer corners for the next flaw to hide in.
Production hardening checklist
These attacks share one cause: the token influences decisions that server code should make. The following eight controls address that cause:
- Actually verify. Call the verifying function with a key and act on its result;
decodeis notverify, and “it decoded fine” is not a security check. - Constrain the algorithm. Read
algfrom the header only to compare it against your configured allow-list, and bind each key to a single algorithm; rejectnoneand anything off the list. - Never let the token choose the key’s location. Resolve
kidas an opaque index into pre-configured keys or a pinned JWKS. Ignorejku/x5uunless you pin trusted hosts, require HTTPS with certificate validation, and send no credentials. - Keep asymmetric public keys verify-only. Key–algorithm binding (rule 2) is what stops a public key from being accepted as an HMAC secret.
- Make secrets high-entropy. At least 32 bytes from a CSPRNG, held as a managed secret; prefer public-key signatures where the verifier shouldn’t be able to issue tokens.
- Validate claims exactly. After the signature passes, check
exp,nbf,iss,aud, and the tokentyp, comparing each exactly — a perfect signature on a token meant for someone else, or of the wrong type, is still a rejection. - Remember that authenticated isn’t authorized. Enforce permissions server-side per request, and check live state for anything that must revoke immediately.
- Assume tokens leak. Keep them short-lived, back
jtiwith server-side state to catch replay, and consider sender-constrained tokens for high-value APIs — a valid stolen token verifies just fine.
And one rule carried over from the first article: the payload is readable by anyone, so never put a secret in it. For permissions that must change or be revoked immediately, do not rely only on a long-lived token claim; use short lifetimes, revocation, or a live authorization check when the risk requires it.
This does not require out-thinking a cryptographer. Let the verifier use its own configuration to decide whether to verify, which algorithms and keys it accepts, and where keys come from. The token is evidence to validate. This closes the main validation-bypass paths, but token theft, authorization, and key operations still require their own controls.
Verification cannot solve one problem: a real, valid token in an attacker’s hands. The next two articles take that on — first where to keep a token in the browser (localStorage, cookies, and the XSS/CSRF trade-off), then short-lived access tokens, refresh-token rotation, and revocation. A signature proves that a token was not altered; lifecycle controls limit the damage after a real token is stolen.