Token refusals#
| Symptom | Cause | Fix |
|---|
401 malformed | Token does not decode | Send a valid JWT in Authorization |
|---|
401 iss not allowed | Issuer not in allowed_issuers | Trust only authorization_servers issuers |
|---|
401 unknown kid | Key missing after one refetch | Fix the JWKS refresh job |
|---|
401 bad signature | Signature does not verify | Reject. There is no fallback |
|---|
401 audience mismatch | Token is for another resource | Request it with this resource |
|---|
401 expired | exp is in the past | Get a new token |
|---|
403 insufficient_scope | Tool needs a larger scope | Step up with combined scopes, new id |
|---|
| Valid tokens fail after a key change | Stale JWKS cache | Scheduled refresh plus one refetch on a miss |
|---|
Gateway and content findings#
| Symptom | Cause | Fix |
|---|
metadata_poisoning finding | Scanner hit in a description | Block and review the full descriptor |
|---|
rug_pull finding | Approved descriptor changed | Quarantine until re-approval |
|---|
unapproved finding | No approved digest for the key | Quarantine until review |
|---|
shadowing finding | Two servers expose one name | Namespace it, for example notes.search |
|---|
-32020 | Mcp-Method or Mcp-Name differs from the body | Reject before routing, RBAC and rate limits |
|---|
-32021 on a form request | Client declared URL-only elicitation | Declare elicitation form support |
|---|
| Retry refused after confirm | requestState bound to other arguments | Ask for a new confirmation |
|---|
| Agent obeys a web page | No source tag on retrieved content | Tag content and validate each tool call |
|---|
Validation order on the resource server#
Authorization code with PKCE#
Tasks in code#
| Task | Code |
|---|
| Pin a whole descriptor | descriptor_digest(tool) |
|---|
| Scan a catalog against pins | scan_catalog(catalog, approved) |
|---|
| Scan one description | scan_description(description) |
|---|
| Make a PKCE pair | verifier, challenge = pkce_pair() |
|---|
| Step up a scope on 403 | client.call_with_step_up(tool, server, auth) |
|---|
| Validate a bearer token | server.validate(bearer_token, required_scope=<s>) |
|---|
| Re-fetch the key set | server.refresh_jwks() |
|---|
| Run the lesson tests | python3 -m unittest discover code/tests -v |
|---|
Seven attack surfaces#
- Metadata poisoning
- a description holds instructions unrelated to the tool.
- Descriptor rug pull
- an approved name, description, schema or annotation changes.
- Cross-server shadowing
- two backends expose one unqualified name.
- Header and body confusion
Mcp-Method or Mcp-Name disagrees with the body.- Capability escalation
- a declared feature is read as authorization.
- MRTR state tampering
- a changed
requestState or reused confirmation. - Identity confusion
- a familiar display name is read as publisher proof.