A JSON Web Token can carry identity and authorization claims across service boundaries. Its compact format also carries a header that describes cryptographic processing, including an alg field.
That field comes from the token itself. It is attacker-controlled input until verification succeeds.
A verifier therefore must not treat alg as permission to select any cryptographic mode a library happens to support. The application must decide which algorithm, key type, issuer, audience, and other verification rules are acceptable. The token may identify a candidate within that fixed policy, but it must not define the policy.
A strong rule is: configure each verification boundary with a narrow set of accepted algorithms and compatible keys, then reject tokens outside that set before trusting claims.
The token header is input, not policy
A JWT has three base64url-encoded segments:
header.payload.signatureA decoded header might contain:
{
"alg": "RS256",
"kid": "signing-key-2026-09"
}It is tempting to read alg, find a matching verification routine, and continue. That reverses the trust relationship. The sender is then influencing the cryptographic rule used to judge the sender’s own token.
The safer model separates two things:
token metadata -> candidate information
server configuration -> permitted verification policyFor example, a service that accepts only RS256 tokens from one issuer should configure its JWT library accordingly. A token declaring another algorithm is rejected even if the library supports that algorithm elsewhere.
Algorithm confusion is a policy failure
JWT libraries can support several algorithm families because different applications have different requirements. Broad library capability does not mean every algorithm is interchangeable at one verification boundary.
Symmetric algorithms such as HMAC use a shared secret. Asymmetric algorithms such as RSA and ECDSA use distinct private and public key roles. Treating a key from one family as if it belonged to another can create dangerous verification behavior in poorly configured or vulnerable implementations.
Historical JWT flaws have included acceptance of unsigned tokens and algorithm-confusion cases. Modern libraries have added safeguards, but application policy still matters. A dependency upgrade cannot compensate for a verifier configured to accept more cryptographic choices than the protocol requires.
Do not write logic shaped like this:
header = decode_header(token)
verifier = choose_verifier(header.alg)
key = load_key(header.kid)
claims = verifier.verify(token, key)The first line is not inherently unsafe; software often needs unverified metadata to locate a candidate key. The problem is allowing untrusted metadata to expand the set of acceptable verification methods.
Prefer this policy shape:
policy.algorithms = {"RS256"}
policy.issuer = "https://id.example.test/"
policy.audience = "payments-api"
header = decode_header(token)
if header.alg not in policy.algorithms:
reject
key = approved_key_for(policy.issuer, header.kid, header.alg)
claims = verify_with_fixed_policy(token, key, policy)This is conceptual pseudocode. Use the maintained verification API provided by your JWT library rather than implementing cryptographic parsing or signature checks yourself.
Keep key selection inside the same trust boundary
Pinning the algorithm is necessary, but key selection also needs constraints.
Many systems use kid to support key rotation. A verifier can read that value before signature validation because it needs some way to select a candidate public key. That does not make kid trustworthy.
Treat it as an index into a server-controlled key set:
kid from token
|
v
approved keys for configured issuer
|
v
matching key or rejectionDo not turn kid directly into a filesystem path, database expression, shell argument, or arbitrary network location. Do not let a token select a key belonging to an unrelated issuer or tenant.
If keys come from a remote JSON Web Key Set, bind the key-set location to trusted issuer configuration. The token should not supply an arbitrary URL for the verifier to fetch.
Key metadata should also be compatible with the configured algorithm and intended key use. Reject a candidate that does not meet the verifier’s fixed requirements.
Verify claims only after signature validation
Decoding is not verification.
JWT payloads are encoded for transport, not encrypted or authenticated merely by being base64url text. An attacker can construct a syntactically valid header and payload without possessing a signing key.
Code that needs authorization data should receive claims only from a verification path that has already established the signature and protocol constraints.
A useful application boundary looks like this:
raw token
|
parse bounded structure
|
enforce algorithm policy
|
select approved key
|
verify signature
|
validate issuer, audience, time constraints
|
produce trusted claims objectAvoid APIs that return the same claims type for both decoded-but-unverified data and verified data. Distinct types or narrowly scoped functions can make accidental trust harder.
Validate issuer and audience as part of verification
A valid signature answers a limited question: the token was signed by a holder of the corresponding signing key and the signed bytes have not been altered.
That is not enough to establish that the token belongs at the current endpoint.
An identity provider may issue tokens for several services. If Service B accepts a token intended for Service A merely because both trust the same signing infrastructure, the signature check has succeeded while the authorization boundary has failed.
Configure and validate at least the claims required by your protocol, commonly including:
issagainst the expected issuer;audagainst the intended service or resource;expso expired tokens are rejected;nbfwhen the token must not be accepted before a stated time.
Apply clock-skew tolerance deliberately and keep it small enough for the deployment environment.
Claims such as roles, scopes, tenant identifiers, and subject identifiers need application-specific authorization checks after cryptographic verification.
Separate token profiles when requirements differ
One verifier that accepts every token type used by an organization becomes difficult to reason about.
Suppose an environment has:
- access tokens for an API;
- ID tokens for a browser login flow;
- internal service tokens;
- tokens from a migration-era issuer.
Even if all are JWTs, they may have different issuers, audiences, algorithms, keys, required claims, and semantic meaning.
Create separate verification profiles:
API access token
-> issuer A
-> audience payments-api
-> RS256
-> access-token claim rules
internal service token
-> issuer B
-> audience ledger-worker
-> ES256
-> service-token claim rulesThis reduces the chance that a token valid in one context is accepted in another.
Treat algorithm changes as protocol migrations
Changing a signing algorithm is not just a library setting. It changes the contract between issuer and verifier.
Plan a migration explicitly:
- inventory every verifier for the token profile;
- add support only where the new algorithm is required;
- provision compatible keys through trusted configuration;
- test positive and negative cases;
- switch issuance according to the rollout plan;
- remove the old algorithm after the compatibility window closes.
If two algorithms must coexist temporarily, that temporary set should still be explicit. Avoid a generic setting equivalent to “accept any supported algorithm.”
Test rejection paths, not only valid tokens
A verification test suite should demonstrate that policy cannot be widened by token input.
Include cases such as:
expected algorithm -> accepted with valid signature
different algorithm -> rejected
unsigned form -> rejected
unknown kid -> rejected
key from another issuer -> rejected
wrong audience -> rejected
wrong issuer -> rejected
expired token -> rejected
not-yet-valid token -> rejected
modified payload -> rejected
modified signature -> rejectedAlso test malformed encodings and unusually large token components according to the limits of the surrounding HTTP stack.
Tests should call the same verification entry point used by production request handling. A low-level unit test of a signature primitive does not prove that the application has configured the full token policy correctly.
Review wrapper code around the JWT library
Security defects often appear in application glue rather than in the cryptographic primitive.
During review, trace the complete path from raw token to authorization decision:
Where is the token obtained?
Which verifier profile is selected?
Which algorithms are configured?
How is the candidate key selected?
Which issuer owns that key set?
Which claims are mandatory?
Which audience is expected?
What object is returned after verification?
Can any caller bypass this path?Pay special attention to convenience helpers that decode claims without verification, generic middleware shared across unrelated token profiles, and fallback behavior after verification errors.
A failure should result in rejection. Avoid retrying with progressively weaker settings.
Operational controls support the verifier
Application checks are strongest when deployment configuration preserves the same boundaries.
Keep signing private keys out of verifier services when asymmetric signatures are used. Give verifiers only the public material they require. Restrict configuration changes that can alter accepted issuers, algorithms, audiences, or key sources.
Log verification failures in a form useful for operations without recording raw bearer tokens. Aggregate categories such as unknown key identifier, invalid signature, expired token, or issuer mismatch. Rate controls may also be appropriate where attackers can force expensive verification work at high volume.
Key rotation should have a defined overlap period and cache behavior. A verifier should be able to obtain a newly approved public key without accepting keys from arbitrary locations.
A compact review checklist
For each JWT verification boundary, confirm that:
- accepted algorithms are explicitly configured;
- token input cannot add another algorithm;
- unsigned tokens are rejected;
- candidate keys come only from an approved key set;
- key type and intended use match the configured algorithm;
- issuer and audience are validated;
- time-based claims follow the protocol’s requirements;
- decoded data is not treated as verified data;
- separate token purposes use separate verification profiles where appropriate;
- verification errors fail closed;
- tests cover algorithm, key, claim, and signature rejection paths.
JWT security is not achieved by calling a signature function in isolation. The robust design is a fixed verification policy: the application defines acceptable cryptography and token context first, then untrusted token data is checked against those constraints.