ASVS 5.0 V9.1.1 requires that self-contained tokens are validated using their digital signature or MAC before the application accepts anything inside them. It is a Level 1 requirement. In one line: never read claims from a token you have only decoded. Decode and verify are different operations, and only verify is safe.
Why it matters
A JWT is three base64url strings. Anyone can read the payload, and anyone can write a new one. Without a signature check, {"sub":"1","role":"admin"} is as good as a real token. Many real incidents come from code that called decode where it needed verify.
How to implement it
Use a maintained library, call its verify function, and handle failure as "reject the request".
import { jwtVerify, createRemoteJWKSet } from "jose";
const jwks = createRemoteJWKSet(new URL("https://idp.example.com/.well-known/jwks.json"));
export async function authenticate(token: string) {
const { payload } = await jwtVerify(token, jwks, {
issuer: "https://idp.example.com/",
audience: "https://api.example.com",
algorithms: ["RS256"],
});
return payload; // only now are the claims trustworthy
}
import jwt # PyJWT
def authenticate(token, public_key):
return jwt.decode(
token, public_key,
algorithms=["RS256"],
audience="https://api.example.com",
issuer="https://idp.example.com/",
options={"require": ["exp", "iss", "aud"]},
)
Rules to follow: verify before reading any claim, including for routing or logging decisions. Verify on every service that accepts the token, not only at the gateway, unless the internal network really is a trust boundary you can prove. Fail closed on any exception.
Keep the library up to date. Token libraries have had serious verification bugs, and your dependency list is part of this control. You can check yours in the browser with the free dependency scan and see how an inventory looks in the SCA demo.
How to test it
- Take a valid token, change one character in the payload, keep the old signature. Expect 401.
- Strip the signature entirely. Expect 401.
- Sign a token with a different key and the right header. Expect 401.
- Change the
algheader tonone. Expect 401. - Grep the code for
decode(,jwt_decode,verify=False,ignoreExpirationandverify_signature: False. Each hit needs a justification.
Common mistakes
- Using a decode helper to read the
subclaim. - Disabling verification "temporarily" in a test config that ships.
- Catching the verification error and continuing as anonymous with the claims anyway.
- Verifying in the gateway and trusting any internal caller that sends a token.
Try it yourself
Click through it at your own pace, or open it full screen.
Quick check
What is the difference between decoding and verifying a JWT?
Decoding only reads the base64 content. Verifying checks the signature or MAC with the right key. Only verified content may be trusted.
Which tamper test is the quickest to run?
Edit the payload and resend with the original signature. A correct implementation returns 401.