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-09

The token can provide data used during verification, but trusted configuration defines the permitted set.

A useful invariant is:

accepted algorithm = configured algorithm

not:

accepted algorithm = token header choice

Reject 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 verification

A 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 secret

If 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 claims

Do 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 set

After 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_action

The correct sequence is:

parse required token structure
  -> apply trusted algorithm and key policy
  -> verify signature
  -> validate registered and application claims
  -> authorize the request

Any 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:

  • iss for the expected issuer;
  • aud for the intended recipient;
  • exp for expiration;
  • nbf when 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-09

The 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.
  • kid selects 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.