Blog

How to Verify a JWT: Signature, Claims, JWKS

·4 min read

Verification is a checklist, not a library call

“Verify the JWT” is four separate checks. Libraries hide them behind one function, which is why teams ship a verifier that accepts any issuer, any audience, or alg: none. Do the checks in this order. Stop at the first failure.

1. Parse header.payload.signature
2. Reject alg=none and algorithms you did not allow
3. Resolve the key (JWKS + kid, or HMAC secret)
4. Verify the signature over header.payload
5. Check nbf / exp (with a small clock-skew window)
6. Check iss, aud, sub only if you configured them
7. Then – and only then – read custom claims for authorization

Step 7 is not verification. Routing or denying on roles after a valid signature is extracting JWT claims. Wiring Auth0’s JWKS URL into a proxy is the Auth0 walkthrough. This post is the mechanics those two posts assume.

If you need the token format first, start with What is a JWT.

Step 1–2: parse and pin the algorithm

Split on the two dots. Base64URL-decode the header. If alg is none, or not in your allow-list, reject. An attacker who can pick alg can sometimes convince a sloppy verifier to treat an HMAC secret as an RSA public key, or to skip the signature.

RequestRocket’s jwtVerify secret requires algorithms as a non-empty array (["RS256"], ["ES256"], ["HS256"], or a small set). There is no implicit “accept whatever the header says.”

Step 3–4: resolve a key and check the signature

Two mutually exclusive sources. Never configure both.

JWKS (jwksUri). Fetch the JSON Web Key Set, find the key whose kid matches the header, and verify. This is the RS*/ES* path. On first use we fetch and cache keys on the credential; the same token string can then pass a fast equality check against lastVerifiedToken without running RSA again. A new token (SPIRE rotation, user refresh) forces a full verify.

HMAC (sharedSecret). HS* only. The same secret that signed the token verifies it. Anyone who can verify can mint, so this belongs on a closed pair of services, not on a public JWKS.

The API rejects a jwtVerify credential that has neither source, and rejects one that has both. An unused HMAC secret sitting next to a JWKS URL is an accident waiting for the wrong code path. Failures look identical (“invalid signature”) until you know which key was used.

Optional JWKS request shape: jwksMethod (GET | POST), jwksHeaders, jwksQueryParams, jwksBody – some SPIFFE trust bundles are not a naked GET.

Step 5: time

Check nbf and exp as unix seconds. clockSkewSeconds (default 30 in our verifier) absorbs clock drift between issuer and gateway. A zero-skew verifier will bounce valid tokens from a host whose clock is 12 seconds slow. A five-minute skew turns expired tokens into current ones. Keep it small.

Step 6: iss, aud, sub are optional on purpose

If you omit issuer, any issuer is accepted. Same for audience and subject. That is a deliberate schema choice: SPIFFE-style verifiers sometimes want signature + trust domain without pinning a single aud. It is also the easiest way to build an open relay. Set them unless you can explain why not.

  • issuer – exact match on iss (for example spiffe://acmecorp.com or https://idp.example.com/).
  • audience – one string or an array; the token passes if any listed value matches aud. An empty array is treated as “no audience check” and stripped on save.
  • subject – exact sub, with an optional wildcard suffix (spiffe://acmecorp.com/agents/*).

requiredClaims adds extra exact claim checks beyond those three.

Microsoft Graph tokens cannot be verified this way. If aud is 00000003-0000-0000-c000-000000000000, you have an opaque Graph credential. Microsoft does not guarantee those are standard JWTs. Request a token for your own app (scope=api://<client-id>/.default), not .default with no resource prefix. Tokens with a nonce in the JWT header are Continuous Access Evaluation tokens and also fail standard libraries.

Azure v1 versus v2 JWKS URIs follow the iss prefix: https://sts.windows.net/ uses /discovery/keys; https://login.microsoftonline.com/<tenant>/v2.0 uses /discovery/v2.0/keys.

Fail-closed versus fail-open

failPolicy applies when the JWKS URL is unreachable, not when a signature is bad. fail-closed (the default) denies. fail-open serves from a stale cache. Fail-open is an availability choice that accepts the risk of verifying against rotated-away keys. Signature failures never fail open.

jwtVerify is proxy-only. It authenticates callers to the gateway. Outbound minting is jwtSigned; outbound static presentation is jwt. Putting verification on a target credential would check a token the gateway itself attached, which is a no-op.

{
  "credentialType": "proxy",
  "credentialAuthType": "jwtVerify",
  "credentialName": "inbound-jwt",
  "credentialRegion": "us-east-1",
  "credentialSecret": {
    "algorithms": ["RS256"],
    "jwksUri": "https://idp.example.com/.well-known/jwks.json",
    "issuer": "https://idp.example.com/",
    "audience": "https://api.example.com"
  }
}

Next steps

After verification succeeds, enforce claims with rules as in How to extract and use JWT claims, or follow the vendor path in Add Auth0 JWT authentication. Field-level options live in the credentials guide. RequestRocket is runtime access control for AI agents and apps calling APIs you don’t own – every call gets a least-privilege credential, a policy check, and an audit record, with no code changes.

Enhance ISO 27001
Enhance SOC 2
Enhance GDPR
Enhance HIPAA

Add outbound API security
without changing code

Start on your own or talk to our team about improving the security of every API call you make.