Choose your stack

Node.js · Express

Fix JWT validation in Node.js and Express

Verify every bearer token with jose's jwtVerify(): pass the issuer's public key from configuration, algorithms: ['RS256'], issuer and audience, requiredClaims: ['exp', 'sub'] and a small clockTolerance.

Open the fix card
Python · Flask

Fix JWT validation in Python and Flask

Verify every bearer token with PyJWT's jwt.decode(token, key, algorithms=["RS256"], issuer=..., audience=..., leeway=30, options={"require": ["exp", "iss", "aud", "sub"]}), with the key from configuration.

Open the fix card
Java · Spring Boot

Fix JWT validation in Java and Spring Boot

Build the JwtDecoder bean with NimbusJwtDecoder.withPublicKey(key).signatureAlgorithm(RS256) (or withIssuerLocation for rotating keys) and set its validators: JwtValidators.createDefaultWithValidators(timestamps, new JwtIssuerValidator(issuer), new JwtAudienceValidator(audience)), with a JwtTimestampValidator that requires exp.

Open the fix card
C# · ASP.NET Core

Fix JWT validation in C# and ASP.NET Core

Configure JwtBearer's TokenValidationParameters with the issuer's signing key from configuration (or options.Authority), ValidAlgorithms = [RS256], ValidIssuer, ValidAudience and a ClockSkew of seconds, leaving ValidateIssuer, ValidateAudience, ValidateLifetime, RequireExpirationTime and RequireSignedTokens true.

Open the fix card
TypeScript · Next.js

Fix JWT validation in TypeScript and Next.js

Verify the bearer token inside the route handler with jose's jwtVerify(): the issuer's public key from configuration, algorithms: ['RS256'], issuer and audience, requiredClaims: ['exp', 'sub'] and a small clockTolerance.

Open the fix card
Python · FastAPI

Fix JWT validation in Python and FastAPI

Put verification in one dependency that every protected route declares: HTTPBearer() reads the token, then jwt.decode(token, key, algorithms=["RS256"], issuer=..., audience=..., leeway=30, options={"require": ["exp", "iss", "aud", "sub"]}) with the key from configuration, and any PyJWTError becomes 401.

Open the fix card
Go · net/http

Fix JWT validation in Go and net/http

Verify with jwt.ParseWithClaims, a keyfunc that returns the issuer's public key from configuration, and options: jwt.WithValidMethods([]string{"RS256"}), jwt.WithIssuer, jwt.WithAudience, jwt.WithExpirationRequired and jwt.WithLeeway(30*time.Second).

Open the fix card

Stacks without a card

How it happens

  • Decoding is mistaken for verifying. jose's decodeJwt(), PyJWT's options={"verify_signature": False}, a Spring JwtDecoder that only parses, and an ASP.NET Core SignatureValidator that returns the token unchecked all hand back claims the caller wrote. It usually ships because a gateway is supposed to have verified the token, until a route is reachable without it.
  • The token chooses how it is checked. The header names the algorithm, and libraries that trusted it accepted alg none or verified an HS256 token with the RSA public key as the HMAC secret: CVE-2015-9235 in jsonwebtoken before 4.2.2, CVE-2022-29217 in PyJWT before 2.4.0, and PyJWT advisories again in 2026. Keys taken from the token's own jwk, jku or x5u header do the same.
  • A valid signature is treated as enough. Identity providers sign tokens for many APIs, and multi-tenant providers for many tenants, with the same keys: Microsoft Entra ID's tenant-independent keys sign tokens for every tenant, and Microsoft's documentation says the same sub in two tenants describes two different users. Without issuer and audience checks, a token issued for another API, or by another tenant, is accepted as one for yours.
  • Expiry is weaker than it looks. jose, PyJWT and Spring Security check exp only when the token has one, so a token without exp never expires, and ASP.NET Core keeps accepting tokens for five minutes after they expire because ClockSkew defaults to five minutes.
  • Checks are switched off to make errors go away: verify_aud False for PyJWT's InvalidAudienceError, ValidateIssuer = false and ValidateAudience = false for IDX10204 and IDX10208, and a custom Spring JwtDecoder bean that silently drops the issuer-uri and audiences validation Spring Boot would have added.

Fix principles

  • Verify every token with the library's verify function before reading any claim, and fail closed with 401 and WWW-Authenticate: Bearer error="invalid_token" (RFC 6750).
  • Take keys from configuration: the issuer's public key or its JWKS from the discovery document. Never use a key or key URL the token supplies (jwk, jku, x5u), and select keys by kid only within the configured issuer's key set.
  • Allow only the algorithm the issuer uses (RS256 here). Never alg none, and never HS256 with key material meant for RS256: each key gets exactly one algorithm (RFC 8725, section 3.1).
  • Require iss to equal the configured issuer exactly and aud to contain this API's identifier. When the provider serves several tenants with shared keys, tie the issuer to the tenant, and identify users by iss and sub together.
  • Require exp, and nbf when present, with a clock skew of seconds. RFC 7519 allows leeway of usually no more than a few minutes; library defaults range from none (jose, PyJWT) to 60 seconds (Spring) to five minutes (ASP.NET Core).
  • For OAuth access tokens in the RFC 9068 profile, also require typ at+jwt so that an ID token or another JWT from the same issuer cannot be used as an access token.
  • Test validation with real signed tokens sent through the HTTP stack, including forged and misdirected ones; test helpers that inject an authenticated principal skip the decoder entirely.

The scenario

Every card runs the same accounts API: GET /api/account returns the account whose sub is in a valid bearer token, and otherwise answers 401 with a WWW-Authenticate: Bearer challenge. The trusted identity provider is https://login.example.test/tenant-a and the API's audience is https://api.example.test/accounts. The same provider also signs tokens for tenant-b and for a reports API with the same key, which is how a correctly signed token can be meant for someone else. Alice and Mallory have accounts; every exploit token claims to be Alice. The tests generate a new RSA key pair for the provider, and one for the attacker, on every run, and build each token byte by byte with the standard library rather than with the library under test.

How the cards prove it

  • Run each card's tests in three modes: the fixed implementation passes, and the vulnerable and plausible-but-wrong implementations fail. run-all.mjs does this for every card.
  • Nine exploit tests, identical in every card, send tokens claiming to be Alice: alg none, HS256 signed with the provider's public key as the HMAC secret, the attacker's own key embedded in a jwk header, Mallory's genuine token with its payload changed, a token that expired an hour ago, one that expired two minutes ago, one without exp, one the provider issued for another API (aud) and one from another tenant that shares the signing key (iss). Each must get 401 and none of Alice's data. Five functional tests keep valid tokens for Alice and Mallory, a missing token's Bearer challenge, 10 seconds of clock skew and a multi-valued aud working.
  • The wrong implementations fail different exploit tests because the libraries' defaults differ: jose, PyJWT and Spring Security accept the token without exp, and ASP.NET Core accepts the token that expired two minutes ago; every wrong implementation accepts the other API's and the other tenant's tokens.
  • Static analysis catches the crude cases at best. Semgrep flags PyJWT's verify_signature False and ASP.NET Core's ValidateLifetime = false, and .NET's CA5404 (off by default) flags ValidateIssuer and ValidateAudience set to false; nothing checked here flags jose's decodeJwt(), a parse-only Spring JwtDecoder, a SignatureValidator delegate, a missing issuer or audience check, or a lenient clock skew. Each card's scannerRules records what ran and what it found.

Identifiers

CWE-347 (Improper Verification of Cryptographic Signature) describes the vulnerable implementations, which read claims without checking the signature, and the alg none, key-confusion, attacker-key and edited-payload tokens; OWASP Top 10:2025 maps it to A04 Cryptographic Failures. CWE-287 (Improper Authentication) covers the rest: a correctly signed token accepted from another issuer, for another API or after it expired authenticates a principal the API should not trust; it maps to A07 Authentication Failures. MITRE marks CWE-287 as discouraged for mapping individual vulnerabilities because it is a class, and the expiry tests are closer to CWE-613 (Insufficient Session Expiration, also A07). CAPEC-475 (Signature Spoofing by Improper Validation) is the attack pattern MITRE links to CWE-347; CAPEC-196 (Session Credential Falsification through Forging) describes writing a token by hand. ASVS 5.0.0 9.1.1 to 9.1.3 cover the signature, the algorithm allowlist and trusted key sources, 9.2.1 exp and nbf, 9.2.3 and 10.3.1 the audience, and 10.3.3 identifying users by iss and sub together.

Attack patterns:

References

  1. RFC 8725: JSON Web Token Best Current Practices
  2. RFC 7519: JSON Web Token (iss, aud and exp claims)
  3. RFC 9068: JWT Profile for OAuth 2.0 Access Tokens
  4. RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage
  5. OWASP JSON Web Token Cheat Sheet
  6. OWASP ASVS 5.0.0, V9 Self-contained Tokens
  7. OWASP ASVS 5.0.0, V10 OAuth and OIDC (10.3 resource server)
  8. OWASP Top 10:2025 A07 Authentication Failures
  9. OWASP Top 10:2025 A04 Cryptographic Failures
  10. CWE-347: Improper Verification of Cryptographic Signature
  11. CWE-287: Improper Authentication
  12. CAPEC-475: Signature Spoofing by Improper Validation
  13. CAPEC-196: Session Credential Falsification through Forging
  14. Microsoft identity platform: access tokens (validating the issuer in multitenant applications)
  15. PortSwigger Web Security Academy: JWT attacks

Practice in the browser

Guides on this weakness

Guide

Verify webhook deliveries before acting

Verify the provider’s signature over the correct bytes, then apply replay, authorization, and idempotency controls before a business action.

Reference

Hash collisions and digital signatures: MD5, SHA-1 and what to use

Distinguish collisions from preimages, follow a colliding pair through signing and verification, and retire MD5 and SHA-1 from the verifiers that still accept them.

Guide

Design firmware updates that can recover safely

Connect trusted boot, update authorization, rollback policy and end-of-life behavior to the device’s actual failure modes.

Guide

Verify artifact signatures against an explicit trust policy

Require the expected artifact, signer and release context before allowing a signed package or image to proceed.

Guide

JWT validation: algorithms, keys, claims and key rollover

Validate a JWT against a fixed algorithm, trusted keys and the expected issuer, audience and type, reject the known header attacks, and rotate signing keys without accepting arbitrary keys or breaking valid tokens.

Guide

SAML single sign-on: verify signed assertions without losing the identity boundary

Bind a validated SAML assertion to its trusted issuer, the exact signed element, your service, the browser request and a replay record before creating a local session, and keep your SAML library ahead of its advisories.

Reference

Choose cryptographic algorithms and parameters by job

A sourced quick reference for October 2026: which algorithm and parameters to use for encryption, hashing, passwords, MACs, signatures, key agreement, key derivation, randomness and TLS, the matching APIs in Node.js, Python, Java, Go and .NET, and where post-quantum migration stands.

Guide

Defend JWTs against forgery, confusion and replay

Verify a JWT as an issuer-, type- and audience-bound assertion with an algorithm allowlist and trusted keys, closing the alg "none", key-confusion, header-injection, weak-secret and replay attacks.

Guide

Defend OAuth and OIDC against redirect and token attacks

Apply RFC 9700 and RFC 10017: exact redirect matching, PKCE with downgrade protection, state and iss binding, refresh-token rotation, sender-constrained tokens, and a backend-for-frontend so browser tokens cannot be stolen.

Guide

Publish security advisories with CSAF, VEX, CVE records and OSV

What a useful security advisory contains and how to publish it in the formats tools read: a validated CSAF 2.0 VEX document, the CSAF provider files, CVE records through a CNA (CVE Record Format 5.2), and OSV records for open-source packages. Covers the status of CSAF 2.1 as of October 2026.

Guide

DeFi attack patterns: oracles, flash loans, proxies and signatures

The four smart-contract attack classes beyond reentrancy: price-oracle manipulation, flash-loan amplification, upgradeable-proxy storage collisions and uninitialized implementations, and signature replay, each with vulnerable and fixed Solidity, Foundry tests and verified incidents.

Case study

Case study: Storm-0558 and the forged Microsoft tokens (2023)

How a stolen 2016 consumer signing key and a token validation gap let Storm-0558 read enterprise Exchange Online mail in 2023, and what the review board found about rotation, logging and culture.