Stop at the first broken edge. A valid token endpoint cannot compensate for a missing challenge, and a successful login cannot compensate for a token minted for another resource.
Step 1: the unauthenticated challenge
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"
The first failure mutation is to remove resource_metadata. Expected result: the client has no advertised path to protected-resource metadata and may never open an authorization flow.
Step 2: protected-resource metadata
{
"resource": "https://api.example.com/mcp",
"authorization_servers": ["https://login.example.com"],
"scopes_supported": ["mcp:tools"]
}
The second mutation changes resource to an internal host such as http://mcp:3000. Expected result: the public endpoint and advertised resource identifier no longer agree.
Step 3: authorization-server metadata
{
"issuer": "https://login.example.com",
"authorization_endpoint": "https://login.example.com/authorize",
"token_endpoint": "https://login.example.com/token",
"code_challenge_methods_supported": ["S256"]
}
The third mutation changes the issuer to https://login.example.com/tenant-a while the resource document names the root issuer. Expected result: documents remain reachable but their identifiers describe different authorization servers.
Step 4: requested authorization
response_type=code resource=https%3A%2F%2Fapi.example.com%2Fmcp scope=mcp%3Atools code_challenge_method=S256
The fourth mutation requests mcp:admin, which the fixture does not advertise. Expected result: the authorization server rejects the scope or omits it from the grant.
Step 5: token acceptance
| Claim | Expected | Failure mutation |
|---|---|---|
iss | https://login.example.com | Different tenant |
aud | https://api.example.com/mcp | API homepage |
scope | mcp:tools | Missing scope |
exp | Future timestamp | Expired token |
Do not paste a live token into a public tool. Decode synthetic or redacted claims locally, then compare identifiers exactly. A cryptographically valid token with the wrong audience should still be rejected by the MCP resource.
Proxy-specific check
If the fixture works locally but the deployment fails, inspect the URLs generated behind the proxy. A missing forwarded protocol or host can turn every public HTTPS identifier into an internal HTTP address even though all application routes return 200.
Validate the chain
Use the fictional values above in the metadata validator, then apply one mutation at a time and confirm the first failing edge changes as expected.
Open MCP OAuth Lab →