Why signature verification alone cannot prevent cross-API token replay.
For API developers and security architects building resource servers behind a shared identity provider.
An access token can be correctly signed, issued by a trusted authorization server, and still unexpired, yet be the wrong credential for the API receiving it. When several APIs trust the same issuer, a resource server that verifies the signature but skips the audience check may accept a token that was minted for a completely different service. Nothing looks broken: authentication succeeds, standard authentication logs may show no obvious failure, and the trust boundary between APIs has already failed.
This is not a weakness in OAuth itself. It is a failure to enforce the boundary that OAuth token validation is designed to provide. The scenario below is hypothetical, uses fictional hostnames, and is built from publicly documented specifications. It illustrates a configuration weakness, not a confirmed vulnerability in any specific product.
1. What the Audience Claim Is For
Many OAuth 2.0 deployments issue access tokens as JSON Web Tokens (JWTs), which lets resource servers validate them locally. A JWT has a header, a payload, and a signature. The payload carries claims such as these:
{ "iss": "https://identity.example.com", "sub": "user-1042", "aud": "https://inventory.example.com", "scope": "inventory.read", "iat": 1790000000, "exp": 1790003600 } The timestamps in these examples are illustrative. Any real test should generate tokens with valid issuance and expiration times.
| Claim | What it answers |
|---|---|
|
| Who issued the token? |
|
| Who or what is the token about? |
|
| Which resource server is it meant for? |
|
| What permissions does it carry? |
|
| When does it stop being valid? |
The aud claim is how a resource server decides whether a token was meant for it. The token above is intended for the Inventory API. It should not authorize requests to a Finance API simply because both APIs trust the same identity provider.
A valid token is not necessarily a valid token for your API.
2. The Assumption That Creates the Gap
Centralized identity is operationally convenient. One authorization server may serve dozens or hundreds of APIs, and those APIs often share validation libraries, issuer configuration, and public signing keys.
The risk appears when teams treat a successful signature check as proof that a request is authorized.
Consider two hypothetical services that trust https://identity.example.com:
- Inventory API (
https://inventory.example.com) lets users view inventory data. - Finance API (
https://finance.example.com) exposes financial information to authorized users.
An employee legitimately obtains a token for the Inventory API.
If the Finance API validates signature, issuer, and expiry but never checks the audience, it may accept that Inventory token.
Whether this leads to unauthorized data access depends on the Finance API's remaining authorization checks, but the first boundary has already failed: the API accepted a credential that was never issued for it.
3. A Hypothetical Failure, Step by Step
Step 1: A legitimate token is issued
A client requests access to the Inventory API, and the authorization server returns a token signed with its private key:
{ "iss": "https://identity.example.com", "sub": "employee-1042", "aud": "https://inventory.example.com", "scope": "inventory.read", "exp": 1790003600 } Step 2: The token is presented to a different API
GET /reports/summary HTTP/1.1 Host: finance.example.com Authorization: Bearer <inventory-access-token> Step 3: The Finance API validates incompletely
It checks that the signature is valid, the issuer is trusted, and the token has not expired.
All three checks pass.
However, it never asks whether the token was issued for the Finance API.
If its authorization logic then treats the token's claims as sufficient for the requested operation, the request succeeds.
This is a cross-resource token replay weakness.
Step 4: The trust boundary is crossed
The authorization server did not issue this token for the Finance API.
The Finance API interpreted a token meant for another resource as acceptable authorization evidence on its own.
What it should have returned is a rejection along these lines, using the invalid_token error code defined in RFC 6750:
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer error="invalid_token", error_description="The access token audience is not valid for this resource" The error_description text is illustrative. RFC 6750 does not prescribe this wording, and the exact message varies by implementation.
4. Why Signatures Do Not Solve This
A digital signature answers one question:
Was this token signed by a trusted authority, and is its content unchanged?
Audience validation answers a different question:
Was this token intended for this resource server?
A token can pass the first check and fail the second.
Cryptographic validity and authorization validity must be evaluated separately.
The aud claim can also be an array.
When a token lists multiple audiences, it may be accepted by more than one resource server. This broadens the potential impact if the token is disclosed, although each API must still independently enforce scopes and authorization policies.
RFC 9700 recommends restricting a token to a single resource server where feasible, or otherwise to a small set.
5. What the Standards Say
Several OAuth and JWT specifications address this security boundary.
RFC 8707: Resource Indicators for OAuth 2.0
RFC 8707 allows a client to identify the protected resource it wants to access.
For example:
resource=https://inventory.example.com The authorization server can use this information to issue an audience-restricted token for that resource.
RFC 9068: JWT Profile for OAuth 2.0 Access Tokens
RFC 9068 requires resource servers using that profile to validate that the aud claim contains an identifier the resource server expects for itself.
A token that does not contain an acceptable audience must be rejected.
The specification also defines the at+jwt token type to help distinguish access tokens from other JWTs.
RFC 9700: OAuth 2.0 Security Best Current Practice
RFC 9700 recommends audience-restricting access tokens to reduce the risk of a stolen or leaked token being replayed against an unintended resource server.
The principle across these specifications is consistent:
A resource server must not accept an access token only because it trusts the issuer. It must also confirm that the token was intended for it.
6. The Implementation Mistake, in Code
Consider a simplified Python implementation using PyJWT.
The following function verifies the signature and issuer but deliberately disables audience validation:
import jwt def validate_token_insecure(token, public_key): return jwt.decode( token, public_key, algorithms=["RS256"], issuer="https://identity.example.com", options={"verify_aud": False}, ) This does not mean that applications using PyJWT are inherently vulnerable.
The weakness is the decision to disable audience validation without enforcing an equivalent resource-binding control.
A corrected implementation
The following version explicitly defines the expected audience and checks that the JWT is typed as an access token.
This is an illustration of audience enforcement, not a complete RFC 9068 validator. That profile also requires claims such as sub, iat, jti, and client_id, along with other profile-specific checks.
import jwt EXPECTED_ISSUER = "https://identity.example.com" EXPECTED_AUDIENCE = "https://finance.example.com" ACCEPTED_TYPES = {"at+jwt", "application/at+jwt"} def validate_token_secure(token, public_key): decoded = jwt.decode_complete( token, public_key, algorithms=["RS256"], issuer=EXPECTED_ISSUER, audience=EXPECTED_AUDIENCE, options={"require": ["iss", "aud", "exp"]}, ) if decoded["header"].get("typ", "").lower() not in ACCEPTED_TYPES: raise jwt.InvalidTokenError("Not an access token") return decoded["payload"] A token issued only for the Inventory API now fails when presented to the Finance API.
The typ check applies to JWT access tokens following RFC 9068. Other token formats require validation appropriate to their documented token profile.
Do not relax token-type checks merely to make an otherwise incompatible token pass validation.
In production, the verification key must also come from a trusted, securely configured source, and the API must enforce scopes and endpoint-level authorization.
Audience validation is essential, but it does not replace those checks.
7. Why This Is Hard to Detect
Normal workflows may continue working when audience validation is missing.
Legitimate users receive legitimate tokens, APIs respond, and authentication dashboards show expected activity.
Monitoring that focuses only on signature and issuer validation may not distinguish a token presented to its intended API from one presented to a different API and accepted by mistake.
Both requests involve authentic tokens from the same trusted issuer.
The absence of signature failures can create a false sense of security.
Monitoring should therefore ask not only whether a token is valid, but whether it is valid for the resource receiving it.
8. Testing Audience Enforcement
The following tests belong in authorized development or staging environments.
These are expected outcomes for a correctly configured API, not findings from a real system. Observed results should only be published after the tests have actually been executed.
| Test | Expected result |
|---|---|
| Correct audience | Accepted, subject to scopes and authorization policy |
| Token issued for another API | Rejected (401, |
| Missing | Rejected |
| Expired token | Rejected |
| Wrong | Rejected |
| Signed by an untrusted key | Rejected |
| ID token presented as an access token | Rejected |
| Correct audience, insufficient scope | Denied (403, |
The last row is equally important.
Audience validation and scope enforcement solve different problems.
An API that checks audience but ignores permissions can still expose sensitive operations.
9. A Layered Validation Checklist
Secure API authorization requires multiple controls working together.
1. Enforce audience restrictions
Each resource server should maintain an explicit list of acceptable audience identifiers.
Tokens intended for unrelated services should be rejected.
2. Validate the issuer
Trusted issuers must be explicitly configured.
Do not derive issuer trust from untrusted token content.
3. Verify signatures safely
Use trusted verification keys and an explicit allow-list of signing algorithms.
Never let a token select an arbitrary verification key or disable signature verification.
4. Enforce expiration and time constraints
Validate exp and other applicable time-based claims.
Short-lived access tokens reduce the potential exposure period when bearer tokens leak, although they do not correct missing audience checks.
5. Enforce scopes and object-level permissions
A token intended for an API should authorize only the operations that its subject or client is permitted to perform.
Resource servers must enforce permissions at the endpoint and object levels.
6. Separate ID tokens from access tokens
An OpenID Connect ID token is intended to convey authentication information to the client.
It should not be accepted as an API access token merely because its signature is valid.
Explicit access-token typing, such as at+jwt, helps distinguish token purposes.
7. Consider sender-constrained tokens
Audience restrictions limit where an access token can be used.
Sender-constrained mechanisms, such as mutual TLS (RFC 8705) or Demonstrating Proof of Possession (DPoP, RFC 9449), can additionally restrict who can successfully present it.
These controls address different aspects of token replay and provide stronger protection when combined.
8. Log rejection reasons, not tokens
Security telemetry should capture audience-validation failures and other token rejection reasons without logging raw bearer tokens.
Repeated wrong-audience requests may indicate integration mistakes, misconfiguration, or attempts to cross API trust boundaries.
10. The Zero Trust Angle
Zero Trust emphasizes continuous verification and limiting implicit trust.
An API that accepts any token from a trusted issuer reintroduces the kind of implicit trust that Zero Trust architectures are intended to reduce.
A centralized identity provider provides a common trust foundation, but every resource server still needs to enforce its own authorization boundary.
The same principle applies to service-to-service communication, microservices, cloud-native workloads, and machine identities.
As organizations expand their APIs and automated integrations, resource-specific token validation becomes increasingly important.
The question should not simply be whether a request carries a valid token.
It should be whether that token is valid for this resource, this operation, and this security context.
My position is straightforward:
Treat audience validation as a mandatory security control, not a library default that developers assume is enabled.
Make it part of API security reviews and test it explicitly, just as authentication itself is tested.
Conclusion
A valid digital signature proves that a token is authentic and has not been modified.
It does not prove that the token was issued for every API that trusts the same authorization server.
The solution is not to abandon centralized identity or JWT-based access tokens.
It is to validate tokens completely: issuer, signature, expiration, token type, audience, and the permissions required by the requested operation.
Never confuse trust in an issuer with trust in every token that issuer creates.
A secure API verifies who issued a token, which resource it was issued for, and what it permits.
Note: All hostnames, identifiers, and scenarios in this article are fictional illustrations based on publicly available specifications.