A service receives a JSON Web Token, verifies its signature successfully, reads the user identifier, and accepts the request. That sounds reasonable, but one question is still unanswered: was this token issued for this service and this purpose?
A valid signature proves something narrow. Under the expected cryptographic scheme and key, it shows that the protected token content has not been changed since it was signed by whoever controls that signing key. It does not by itself prove that your API is an intended recipient, that the token is still within its accepted lifetime, or that a token created for one workflow should be accepted by another.
That gap can turn a genuinely signed token into an authorization or authentication mistake. This article builds a practical mental model for JWT validation: verify both the token’s cryptographic evidence and the context in which your application is willing to trust its claims.
A JWT is evidence with a scope
A useful way to think about a signed JWT is as a set of claims carried with cryptographic evidence. The claims might identify an issuer, a subject, intended recipients, and time limits. The signature protects the signed bytes, but the receiving application still decides what those claims mean.
Consider two APIs that trust tokens from the same identity system:
identity service
|
+---- token for billing API
|
+---- token for reporting APIBoth tokens may have perfectly valid signatures. If the reporting API accepts any token signed by the identity service, a token intended only for billing may cross a boundary that the issuer meant to preserve.
The defensive rule is therefore not:
valid signature -> trusted tokenIt is closer to:
valid cryptographic protection
+
expected issuer and key
+
expected audience and token purpose
+
acceptable time window
+
application-specific claim rules
|
v
acceptable token for this operationThe exact rules depend on the protocol and token profile your application uses. JWT is a token format, not a complete authentication or authorization protocol.
Start with an explicit validation policy
Token validation is easier to reason about when the application knows what it expects before it reads an untrusted token.
Suppose an API accepts access tokens from one issuer. Its policy might conceptually say:
expected issuer: https://identity.example
expected audience: payments-api
allowed algorithm: one algorithm chosen by deployment policy
required claims: issuer, subject, audience, expiration
accepted token kind: access token for this APIThe values are illustrative. A real deployment should derive them from the protocol it implements and the issuer’s documented configuration.
The important direction of trust is from configuration to token, not from token to configuration. The token can state an algorithm, issuer, key identifier, audience, and other metadata, but those values are inputs to validation. They do not get to define what the receiver considers trustworthy.
This threat model assumes an attacker can present arbitrary tokens to the application, including tokens that are malformed, expired, issued for another recipient, or legitimately signed for a different use. Contextual validation reduces the risk that such a token is accepted outside its intended role. It does not protect a signing key that has been stolen, repair a compromised issuer, stop theft of a valid bearer token, or replace authorization checks on the requested resource.
Do not let the token choose an acceptable algorithm
Signed JWTs carry an alg header that identifies the algorithm used for the signature operation. The receiver needs that information to process the token, but it should not interpret the field as permission to use any algorithm the token requests.
The application or its trusted protocol configuration should define the algorithms it accepts. Verification should reject tokens outside that set.
Conceptually:
configured algorithms = {expected algorithm}
received alg = token header value
if received alg is not configured:
reject
verify using the key and algorithm allowed by policyThis matters because cryptographic verification only has meaning when the verifier uses the construction it intended to trust. A library that supports several algorithms for generality does not imply that every application should accept all of them.
Use a maintained JWT or JOSE library and its high-level verification interface rather than implementing signature processing yourself. Configure the acceptable algorithm or algorithms explicitly when the library and protocol require that choice. Also keep key selection tied to the trusted issuer and intended algorithm. A key identifier can help select among already trusted keys; it should not turn an arbitrary location or key supplied by the token into a new trust anchor.
Bind the signing key to the expected issuer
The iss claim identifies the issuer of a JWT. Checking that its text matches an expected value is useful, but issuer validation and key validation belong together.
Imagine that an application trusts two independent issuers:
issuer A -> keys for issuer A
issuer B -> keys for issuer BIf the application accepts a token claiming iss = issuer A while verifying it with a key trusted only for issuer B, the issuer claim has not actually been authenticated under the intended trust relationship.
A sound validation flow determines which issuers are trusted and how their verification keys are obtained through trusted configuration or the protocol’s defined discovery mechanism. It then verifies that the token’s cryptographic evidence and issuer claim agree with that relationship.
Do not fetch a verification key merely because an untrusted token contains a URL that points to one. Some JOSE headers and surrounding protocols support key-discovery mechanisms, but the receiver needs an explicit policy for which locations, issuers, and keys are trusted. Otherwise key discovery can quietly become trust discovery.
Audience answers who the token is for
The aud claim identifies intended recipients. For services that can receive tokens from an issuer serving multiple applications, audience validation is a major part of preventing token substitution between those applications.
Suppose the identity service issues this token for a reporting service:
iss = https://identity.example
aud = reporting-api
sub = user-123The signature can be valid and the subject can be real. A payments API expecting payments-api should still reject it because it is not an intended audience.
This is a useful distinction:
issuer check -> who made the token?
audience check -> was this receiver meant to accept it?Audience values can have protocol-specific rules, and JWT permits an audience to be represented as one value or multiple values. Do not write ad hoc string handling when your library or protocol implementation already provides audience validation. Configure the expected audience and test both matching and non-matching cases.
Time claims define a validity window, not token purpose
JWT defines registered claims including exp for expiration and nbf for a time before which the token must not be accepted. A profile may require particular claims and define how they are processed.
When expiration is part of the token profile, validation should happen before the application acts on protected claims. Small clock-skew allowances may be appropriate in distributed systems, but they should be intentional and bounded. A large allowance quietly extends the effective acceptance period.
Time checks answer whether the token is acceptable now. They do not answer whether it is the right kind of token.
For example, these two tokens could both be unexpired and signed by the same issuer:
Token A: intended for API access
Token B: intended for a different application workflowIf their validation rules overlap completely, the receiver may accept one where only the other belongs. Time validity cannot distinguish them.
Separate token types with mutually exclusive rules
Systems often use more than one JWT-shaped object: access tokens, identity assertions, logout tokens, internal capability tokens, or application-specific signed messages. Reusing one generic validator for all of them can create a cross-token confusion problem.
The safer design gives different token kinds validation rules that cannot accidentally accept one another. Depending on the protocol, separation can come from distinct audiences, issuers, keys, required claims, header values, or explicit token typing.
For a simplified internal design:
API access token:
audience = orders-api
type = access+jwt
email-action token:
audience = account-service
type = email-action+jwtThose type strings are illustrative rather than standard values for every JWT use. The point is that each token kind should have a profile, and the profiles should differ in ways the validator actually enforces.
If two workflows use the same issuer, key, audience, claims, and validation function, then the receiver has little evidence that a token from one workflow does not belong in the other. Adding a new token type is therefore a validation-design change, not merely a new place to call the existing decoder.
Parsing claims is not validation
Many JWT libraries expose operations that decode or parse a token without establishing that it is acceptable. That can be useful for diagnostics or for examining metadata needed during a carefully designed verification flow, but decoded data is still untrusted until validation succeeds.
A dangerous application flow looks like this:
decode token
read sub and role
make authorization decision
verify token laterThe decision has already crossed the trust boundary before the evidence was checked.
Prefer an interface where application code receives trusted claims only after the complete validation policy succeeds:
validated = validate(token, policy)
if validation failed:
reject request
use validated claimsThis also makes review easier. Developers can search for the small number of places where raw tokens enter validation rather than proving that every downstream claim read happens after the correct checks.
Signature validation does not replace authorization
After a JWT passes validation, its claims are acceptable under the token policy. The application still has to decide whether the subject may perform the requested action on the requested resource.
A valid access token for orders-api might identify a normal customer. That does not mean the customer may read every order. Object ownership, tenant boundaries, roles, scopes, resource state, and other authorization inputs still apply according to the application’s policy.
Keep the two decisions conceptually separate:
token validation:
may this service trust these claims in this context?
authorization:
do these trusted claims permit this action on this resource?This separation prevents a common reasoning error where “the JWT is valid” becomes shorthand for “the request is allowed.”
Test rejection paths deliberately
A validator should be tested with tokens that are cryptographically valid but contextually wrong. Those cases exercise the controls most likely to disappear when code is simplified or a library default changes.
For the token profile your application actually uses, useful tests include a token with the expected issuer and audience, a token from an untrusted issuer, a valid token for another audience, an expired token, a token that is not yet valid when nbf is part of the profile, and a token whose algorithm is outside the configured set. If the system has multiple JWT types, verify that each validator rejects the other types.
Also test key rotation according to the issuer’s supported mechanism. Rotation should allow newly trusted keys to become usable without turning an unknown key identifier into permission to trust arbitrary key material.
Observe validation failures without logging raw bearer tokens. Logs can record a bounded reason such as issuer mismatch, audience mismatch, expiration, or signature failure when that is operationally appropriate. A bearer token copied into logs can become a credential exposure problem of its own.
Keep the policy narrower than the parser
A JWT library often needs to support many algorithms, claims, and token shapes because it serves many applications. Your service usually needs only a small subset.
That difference is useful. Keep the library general, but make the application’s acceptance policy narrow and explicit: known issuer, trusted key relationship, intended audience, allowed algorithm, required time rules, and a token profile that matches the operation. Reject tokens that fall outside that policy even when they are syntactically valid and cryptographically well formed.
The practical next step is to find each JWT validation entry point in your application and write down the expected issuer, audience, algorithm policy, time requirements, and token kind. If any answer is “whatever the token says” or “whatever the library accepts by default,” that boundary needs a deliberate policy before the claims should influence a security decision.