A JSON Web Token can carry an alg header that names a signing algorithm. That field describes the token, but it must not control the verifier’s security policy. An attacker can edit untrusted token bytes before verification, including header fields.
The safe model is: the application chooses acceptable algorithms and keys from trusted configuration, then checks whether the token fits that policy.
Put policy outside the token
Suppose an API expects tokens signed with RS256. Its verifier should be configured for RS256 and the issuer’s trusted public key. It should not inspect alg and dynamically choose any cryptographic routine the token requests.
trusted configuration
issuer: https://id.example
algorithm: RS256
key: trusted public key
untrusted token
alg: RS256
kid: key-2026-09The token can provide data used during verification, but trusted configuration defines the permitted set.
A useful invariant is:
accepted algorithm = configured algorithmnot:
accepted algorithm = token header choiceReject unsecured tokens
JWT supports an unsecured form whose algorithm value is none. An authentication service expecting signed tokens must reject that form.
Do not implement fallback behavior such as:
if alg == "none":
skip signature verificationA library may support unsecured JWTs for specialized uses. General library capability does not make that mode suitable for an authenticated API. Configure the verifier so unsigned input cannot enter the authenticated path.
Do not mix symmetric and asymmetric verification casually
HS256 uses a shared secret. RS256 uses an RSA private key for signing and a public key for verification. Those key models are different.
A dangerous design accepts both families through one loosely configured verification path. Historical algorithm-confusion flaws have appeared when verifier code treated an RSA public key as though it were an HMAC secret after a token changed its declared algorithm.
Keep algorithm families explicit:
service A:
accepted: RS256
key type: RSA public key
service B:
accepted: HS256
key type: shared secretIf a system genuinely accepts multiple algorithms, define each valid algorithm-and-key-type pair in trusted configuration. Do not infer a valid pair from attacker-controlled header data.
Treat kid as a selector, not authority
The kid header can identify which trusted key should verify a token during key rotation. It must select from a controlled key set.
A safe conceptual flow is:
1. parse enough header data to obtain kid
2. find kid in the trusted issuer key set
3. reject unknown kid values
4. confirm key type and configured algorithm
5. verify the signature
6. validate claimsDo not turn kid directly into a filesystem path, SQL fragment, shell argument, or unrestricted network location. A key identifier is untrusted input until matched against a trusted registry.
Bind keys to issuers
Applications that accept tokens from several issuers need more than a global bucket of verification keys. A key valid for one issuer should not automatically become valid for another.
Model trust as a tuple:
issuer -> allowed algorithm -> trusted key setAfter parsing a token, select an issuer configuration only from an application-maintained registry. Then verify the signature using keys assigned to that issuer.
Do not fetch keys from an arbitrary URL supplied by the token. Remote key discovery must be anchored to trusted issuer configuration and constrained according to the JWT library and identity architecture in use.
Verify the signature before trusting claims
JWT payload fields are readable before signature verification. Parsing is not authentication.
Code must not make access decisions from an unverified payload:
payload = parse(token)
if payload.role == "admin":
permit_admin_actionThe correct sequence is:
parse required token structure
-> apply trusted algorithm and key policy
-> verify signature
-> validate registered and application claims
-> authorize the requestAny data extracted before signature verification remains attacker-controlled.
Validate claims after cryptographic verification
A valid signature says that a trusted key signed the bytes. It does not establish that the token is suitable for every endpoint.
Check the claims required by the application, commonly including:
issfor the expected issuer;audfor the intended recipient;expfor expiration;nbfwhen present and relevant;- application-specific scopes, roles, or permissions.
Apply a small, documented clock tolerance only where operationally necessary. Do not disable expiration checks to accommodate clock drift.
Authorization still belongs to the application. A token with a valid signature must not receive access beyond the permissions represented by validated claims and current server-side policy.
Handle key rotation without widening trust
Key rotation often requires accepting an old and a new public key for a bounded period. That does not require accepting additional algorithms.
issuer: https://id.example
algorithm: RS256
trusted keys:
- key-2026-08
- key-2026-09The verifier can use kid to choose among those trusted keys while the algorithm remains pinned.
Remove retired keys after the intended overlap period, accounting for the maximum lifetime of tokens that were legitimately signed with them. Keep rotation procedures explicit so emergency key revocation can remove a compromised key promptly.
Fail closed on malformed input
Reject tokens with malformed segments, unsupported algorithms, unknown key identifiers, invalid signatures, invalid claim types, or missing required claims.
Avoid recovery logic that silently switches algorithms or key sources after a verification error. A failed cryptographic contract should end token processing for that authentication path.
Return generic authentication errors to clients. Detailed parser or key-selection diagnostics belong in protected operational telemetry, not public error bodies.
Test hostile algorithm cases
Positive tests are not enough. Build negative cases that exercise the policy boundary.
A compact test matrix includes:
| Case | Expected result |
|---|---|
| valid token, configured algorithm | accept if claims also pass |
alg changed to none |
reject |
alg changed to another family |
reject |
unknown kid |
reject |
| valid signature from another issuer’s key | reject |
| altered payload with original signature | reject |
| expired token with valid signature | reject |
| wrong audience with valid signature | reject |
These tests should run through the same verification component used by production requests.
Keep library configuration narrow
Prefer a maintained JWT library that exposes explicit verification settings. Configure the exact accepted algorithm or a minimal allowlist, trusted issuer data, expected audience, and required claim checks.
Avoid generic decode helpers whose main purpose is to inspect token contents. Authentication code needs a verification API with a clear failure result.
When upgrading the library, rerun hostile-token tests. Security defaults, supported algorithms, and key-resolution behavior can change between major versions.
Use a verification checklist
Before accepting JWTs for authentication, confirm these properties:
- Accepted algorithms come from trusted application configuration.
- Unsecured tokens are rejected.
- Symmetric and asymmetric key types cannot be confused.
kidselects only from a controlled key set.- Keys are bound to the intended issuer.
- Signature verification occurs before claims influence access decisions.
- Issuer, audience, expiration, and required application claims are validated.
- Key rotation does not widen the accepted algorithm set.
- Malformed or unsupported tokens fail closed.
- Negative tests cover algorithm substitution and wrong-key cases.
JWT verification is a cryptographic boundary, not a negotiation with the token. Keep algorithm choice, issuer trust, and key selection anchored in server-controlled configuration. Then validate the signature and claims as separate gates before authorization decisions are made.