{
  "version": 1,
  "generatedAt": "2026-10-16T21:39:44.748Z",
  "license": "CC BY 4.0",
  "source": "https://letsauth.eu/explorer/",
  "scenarios": [
    {
      "id": "mcp-authorization",
      "title": "An MCP client obtains a token for an MCP server",
      "summary": "The baseline chain of MCP Authorization 2026-07-28: an agent host discovers where a tasks server's authorization server is, identifies itself with a client ID metadata document, runs the OAuth 2.1 code flow with PKCE and a resource indicator, and presents the token the server was issued for. The server validates it and uses a token of its own for the API behind it.\n",
      "actors": [
        {
          "id": "user",
          "kind": "user",
          "name": "Alex"
        },
        {
          "id": "client",
          "kind": "mcp-client",
          "name": "Agent host"
        },
        {
          "id": "server",
          "kind": "mcp-server",
          "name": "Tasks MCP server"
        },
        {
          "id": "as",
          "kind": "authorization-server",
          "name": "auth.tasks.example"
        },
        {
          "id": "api",
          "kind": "resource",
          "name": "Tasks API"
        }
      ],
      "features": [
        "mcp.prm.www-authenticate",
        "mcp.prm.well-known",
        "mcp.prm.authorization-servers",
        "mcp.as.metadata",
        "mcp.as.cimd",
        "mcp.client.pkce",
        "mcp.as.pkce-s256",
        "mcp.client.resource-param",
        "mcp.as.resource-indicator",
        "mcp.as.iss-param",
        "mcp.client.iss-compare-no-normalization",
        "mcp.rs.bearer-header",
        "mcp.rs.audience-validation",
        "mcp.rs.no-passthrough"
      ],
      "steps": [
        {
          "id": "unauthenticated-request",
          "from": "client",
          "to": "server",
          "message": "POST /mcp (initialize) with no Authorization header",
          "note": "The host has no token for this server yet, so it starts with the request any client makes first. Everything it learns about where to get a token comes from the answer.\n",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Authorization Server Discovery › Protected Resource Metadata Discovery Requirements"
          }
        },
        {
          "id": "challenge",
          "from": "server",
          "to": "client",
          "message": "401 with WWW-Authenticate: Bearer resource_metadata=\"…/.well-known/oauth-protected-resource/mcp\"",
          "note": "The server must point at its protected resource metadata, either in this challenge or at the well-known location; this server does both.\n",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Authorization Server Discovery › Protected Resource Metadata Discovery Requirements"
          }
        },
        {
          "id": "fetch-prm",
          "from": "client",
          "to": "server",
          "message": "GET /.well-known/oauth-protected-resource/mcp",
          "cites": {
            "spec": "rfc9728",
            "revision": "rfc",
            "section": "3. Protected Resource Metadata"
          }
        },
        {
          "id": "prm",
          "from": "server",
          "to": "client",
          "message": "resource: https://mcp.tasks.example/mcp, authorization_servers: [https://auth.tasks.example]",
          "note": "The resource identifier is what the client will ask the token to be bound to; the authorization server list is where it will ask.\n",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Authorization Server Discovery › Authorization Server Location"
          }
        },
        {
          "id": "fetch-as-metadata",
          "from": "client",
          "to": "as",
          "message": "GET /.well-known/oauth-authorization-server",
          "cites": {
            "spec": "rfc8414",
            "revision": "rfc",
            "section": "3. Obtaining Authorization Server Metadata"
          }
        },
        {
          "id": "as-metadata",
          "from": "as",
          "to": "client",
          "message": "issuer, endpoints, code_challenge_methods_supported: [S256], client_id_metadata_document_supported: true",
          "note": "The issuer must match the URL the client asked; the client keeps the metadata keyed by that issuer.\n",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Authorization Server Discovery › Authorization Server Metadata Discovery"
          }
        },
        {
          "id": "authorization-request",
          "from": "client",
          "to": "as",
          "message": "Authorization request via Alex's browser: client_id=https://agent.example/client.json, code_challenge (S256), resource=https://mcp.tasks.example/mcp, state",
          "note": "The client identifies itself with the URL of its metadata document instead of registering first. The resource parameter names the server the token is for.\n",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Client Registration › Client ID Metadata Documents"
          }
        },
        {
          "id": "consent",
          "from": "as",
          "to": "user",
          "message": "Consent screen built from the fetched client metadata: name, redirect URI, requested scope",
          "note": "The authorization server fetches the document at the client_id URL, checks that the redirect URI is one it lists, and shows Alex who is asking.\n",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Client Registration › Client ID Metadata Documents"
          }
        },
        {
          "id": "authorization-response",
          "from": "as",
          "to": "client",
          "message": "Redirect with code, state and iss=https://auth.tasks.example",
          "note": "The client compares iss byte for byte with the issuer it discovered before it uses the code; a mismatch means the response came from somewhere else.\n",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Authorization › Authorization Response Validation"
          }
        },
        {
          "id": "token-request",
          "from": "client",
          "to": "as",
          "message": "POST /token: grant_type=authorization_code, code, code_verifier, resource=https://mcp.tasks.example/mcp",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Security Considerations › Authorization Code Protection"
          }
        },
        {
          "id": "token-response",
          "from": "as",
          "to": "client",
          "message": "Access token bound to the tasks server",
          "note": "The authorization server verified the code verifier against the challenge and issued a token whose audience is the resource the client asked for.\n",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Security Considerations › Token Audience Binding and Validation"
          },
          "token": {
            "name": "Access token for the Tasks MCP server",
            "header": {
              "typ": "at+jwt",
              "alg": "ES256",
              "kid": "auth-2026-1"
            },
            "claims": {
              "iss": "https://auth.tasks.example",
              "sub": "alex",
              "aud": "https://mcp.tasks.example/mcp",
              "client_id": "https://agent.example/client.json",
              "scope": "tasks:read tasks:write",
              "exp": 1790000000,
              "iat": 1789996400,
              "jti": "7f2c"
            },
            "note": "Shown in the RFC 9068 JWT profile; an opaque token would carry the same binding on the server side."
          }
        },
        {
          "id": "bearer-call",
          "from": "client",
          "to": "server",
          "message": "POST /mcp (tools/call) with Authorization: Bearer <access token>",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Authorization › Access Token Usage"
          },
          "token": {
            "name": "Access token as presented",
            "header": {
              "typ": "at+jwt",
              "alg": "ES256",
              "kid": "auth-2026-1"
            },
            "claims": {
              "iss": "https://auth.tasks.example",
              "sub": "alex",
              "aud": "https://mcp.tasks.example/mcp",
              "client_id": "https://agent.example/client.json",
              "scope": "tasks:read tasks:write",
              "exp": 1790000000,
              "iat": 1789996400,
              "jti": "7f2c"
            }
          }
        },
        {
          "id": "validate",
          "from": "server",
          "to": "server",
          "message": "Validate signature, iss, exp and that aud is this server; check scope",
          "note": "A token issued for another resource is rejected here, whoever presents it. This is the check the resource parameter exists for.\n",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Security Considerations › Token Audience Binding and Validation"
          }
        },
        {
          "id": "upstream-call",
          "from": "server",
          "to": "api",
          "message": "GET /tasks with a token the server obtained for the API (here by token exchange, acting for Alex)",
          "note": "The server never forwards the token it received. It holds credentials of its own for the API and obtains a token whose audience is the API, so the API can tell who is calling on whose behalf.\n",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Security Considerations › Access Token Privilege Restriction"
          },
          "token": {
            "name": "The server's own token for the Tasks API",
            "header": {
              "typ": "at+jwt",
              "alg": "ES256",
              "kid": "auth-2026-1"
            },
            "claims": {
              "iss": "https://auth.tasks.example",
              "sub": "alex",
              "aud": "https://api.tasks.example",
              "client_id": "tasks-mcp-server",
              "scope": "tasks:read",
              "act": {
                "sub": "tasks-mcp-server"
              },
              "exp": 1789999000,
              "iat": 1789996500,
              "jti": "9b10"
            }
          }
        },
        {
          "id": "result",
          "from": "server",
          "to": "client",
          "message": "200 with the tool result",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Authorization › Access Token Usage"
          }
        }
      ],
      "toggles": [
        {
          "id": "token-passthrough",
          "name": "Token passthrough",
          "kind": "misconfiguration",
          "summary": "The MCP server forwards the token it received to the Tasks API instead of obtaining one of its own. The API now sees a token whose audience is the MCP server, issued to the agent host; if it accepts it, the agent host's token works against an API it was never authorized for, and the API cannot tell the server's calls from the client's.\n",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Security Considerations › Access Token Privilege Restriction"
          },
          "replaces": {
            "upstream-call": [
              {
                "id": "forwarded-call",
                "from": "server",
                "to": "api",
                "message": "GET /tasks with Authorization: Bearer <the client's access token>, unchanged",
                "note": "The token's audience is still the MCP server. An API that checks aud rejects it; one that does not now trusts a token it never issued for itself.\n",
                "cites": {
                  "spec": "mcp-authorization",
                  "revision": "2026-07-28",
                  "section": "Security Considerations › Access Token Privilege Restriction"
                },
                "token": {
                  "name": "The client's token, forwarded",
                  "header": {
                    "typ": "at+jwt",
                    "alg": "ES256",
                    "kid": "auth-2026-1"
                  },
                  "claims": {
                    "iss": "https://auth.tasks.example",
                    "sub": "alex",
                    "aud": "https://mcp.tasks.example/mcp",
                    "client_id": "https://agent.example/client.json",
                    "scope": "tasks:read tasks:write",
                    "exp": 1790000000,
                    "iat": 1789996400,
                    "jti": "7f2c"
                  }
                }
              }
            ]
          },
          "violates": [
            "mcp.rs.no-passthrough",
            "mcp.rs.audience-validation"
          ]
        },
        {
          "id": "audience-unchecked",
          "name": "Missing audience check",
          "kind": "attack",
          "summary": "The MCP server validates the signature and expiry but not the audience. An attacker who obtained a token issued for a different resource behind the same authorization server presents it here, and the server accepts a token that was never meant for it.\n",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Security Considerations › Token Audience Binding and Validation"
          },
          "replaces": {
            "bearer-call": [
              {
                "id": "replayed-call",
                "from": "client",
                "to": "server",
                "message": "POST /mcp with Authorization: Bearer <a token issued for https://notes.example>",
                "note": "The token is genuine and unexpired, signed by the same authorization server, but its audience is another resource.\n",
                "cites": {
                  "spec": "mcp-authorization",
                  "revision": "2026-07-28",
                  "section": "Security Considerations › Token Audience Binding and Validation"
                },
                "token": {
                  "name": "A token for another resource, replayed",
                  "header": {
                    "typ": "at+jwt",
                    "alg": "ES256",
                    "kid": "auth-2026-1"
                  },
                  "claims": {
                    "iss": "https://auth.tasks.example",
                    "sub": "alex",
                    "aud": "https://notes.example/mcp",
                    "client_id": "https://other-agent.example/client.json",
                    "scope": "notes:read",
                    "exp": 1790000000,
                    "iat": 1789996400,
                    "jti": "c4d1"
                  }
                }
              }
            ],
            "validate": [
              {
                "id": "validate-without-aud",
                "from": "server",
                "to": "server",
                "message": "Validate signature, iss and exp only; aud is not compared",
                "note": "Without the audience check every token the authorization server ever issued for any of its resources opens this server.\n",
                "cites": {
                  "spec": "mcp-authorization",
                  "revision": "2026-07-28",
                  "section": "Security Considerations › Token Audience Binding and Validation"
                }
              }
            ]
          },
          "violates": [
            "mcp.rs.audience-validation"
          ]
        },
        {
          "id": "issuer-mix-up",
          "name": "Issuer mix-up",
          "kind": "attack",
          "summary": "The authorization response arrives without an iss parameter the client checks, so a response crafted by an attacker-controlled authorization server is taken for the real one and the client sends the authorization code, and its code verifier, to the attacker's token endpoint.\n",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Authorization › Authorization Response Validation"
          },
          "replaces": {
            "authorization-response": [
              {
                "id": "spoofed-response",
                "from": "as",
                "to": "client",
                "message": "Redirect with code and state, no iss; the client does not check which server answered",
                "note": "The client started flows with two authorization servers, one of them the attacker's. Without iss it cannot tell whose response this is and pairs the code with the wrong one.\n",
                "cites": {
                  "spec": "rfc9207",
                  "revision": "rfc",
                  "section": "2. Response Parameter iss"
                }
              }
            ],
            "token-request": [
              {
                "id": "misrouted-token-request",
                "from": "client",
                "to": "as",
                "message": "POST /token at the attacker's server with the real code and code_verifier",
                "note": "The attacker redeems the code at the honest server and holds Alex's access token.\n",
                "cites": {
                  "spec": "rfc9700",
                  "revision": "rfc",
                  "section": "4.4. Mix-Up Attacks"
                }
              }
            ]
          },
          "violates": [
            "mcp.as.iss-param",
            "mcp.client.iss-compare-no-normalization"
          ]
        }
      ]
    },
    {
      "id": "mcp-enterprise-idp",
      "title": "An enterprise identity provider brokers the token",
      "summary": "The same agent host and tasks server, with the company's identity provider deciding who may use which MCP server. The host signs Alex in at the identity provider, exchanges the ID token for an Identity Assertion JWT Authorization Grant (ID-JAG) addressed to the server's authorization server, and presents that grant for an access token. No consent screen at the tasks server: the policy lives at the identity provider.\n",
      "actors": [
        {
          "id": "user",
          "kind": "user",
          "name": "Alex"
        },
        {
          "id": "client",
          "kind": "mcp-client",
          "name": "Agent host"
        },
        {
          "id": "idp",
          "kind": "idp",
          "name": "Enterprise IdP (idp.corp.example)"
        },
        {
          "id": "as",
          "kind": "authorization-server",
          "name": "auth.tasks.example"
        },
        {
          "id": "server",
          "kind": "mcp-server",
          "name": "Tasks MCP server"
        }
      ],
      "features": [
        "mcp.prm.well-known",
        "mcp.prm.authorization-servers",
        "mcp.as.metadata",
        "mcp.rs.bearer-header",
        "mcp.rs.audience-validation"
      ],
      "steps": [
        {
          "id": "sign-in",
          "from": "user",
          "to": "idp",
          "message": "Alex signs in to the agent host with the company's single sign-on",
          "note": "The host is an application the enterprise manages. The identity provider authenticates Alex and issues the host an ID token it keeps for later.\n",
          "cites": {
            "spec": "id-jag",
            "revision": "draft-04",
            "section": "4.2. User Authentication"
          }
        },
        {
          "id": "id-token",
          "from": "idp",
          "to": "client",
          "message": "ID token for the agent host",
          "cites": {
            "spec": "id-jag",
            "revision": "draft-04",
            "section": "4.2. User Authentication"
          },
          "token": {
            "name": "ID token",
            "header": {
              "typ": "JWT",
              "alg": "RS256",
              "kid": "idp-2026"
            },
            "claims": {
              "iss": "https://idp.corp.example",
              "sub": "u-48213",
              "aud": "agent-host",
              "email": "alex@corp.example",
              "exp": 1790000000,
              "iat": 1789996400
            }
          }
        },
        {
          "id": "discovery",
          "from": "client",
          "to": "server",
          "message": "Unauthenticated request, challenge, protected resource metadata and authorization server metadata, as in the baseline chain",
          "note": "Discovery is unchanged: the host learns the server's resource identifier and that auth.tasks.example is its authorization server, and the server declares the enterprise-managed authorization extension.\n",
          "cites": {
            "spec": "mcp-enterprise-managed-authorization",
            "revision": "stable",
            "section": "Implementation guide › For MCP servers"
          }
        },
        {
          "id": "jag-request",
          "from": "client",
          "to": "idp",
          "message": "POST /token: grant_type=token-exchange, subject_token=<ID token>, subject_token_type=id_token, requested_token_type=id-jag, audience=https://auth.tasks.example, resource=https://mcp.tasks.example/mcp, scope=tasks:read",
          "note": "Instead of sending Alex to the tasks server's authorization endpoint, the host asks the identity provider for a grant it can present there. The audience is the resource authorization server, not the MCP server.\n",
          "cites": {
            "spec": "id-jag",
            "revision": "draft-04",
            "section": "4.3. Token Exchange"
          }
        },
        {
          "id": "policy",
          "from": "idp",
          "to": "idp",
          "message": "Evaluate policy: may this user, through this app, reach this server with this scope?",
          "note": "Group membership, the approved-app list and conditional access are checked here, once, centrally. A user without access gets an error and the host never receives a grant.\n",
          "cites": {
            "spec": "id-jag",
            "revision": "draft-04",
            "section": "4.3.3. Processing Rules"
          }
        },
        {
          "id": "jag-response",
          "from": "idp",
          "to": "client",
          "message": "issued_token_type=id-jag with the signed grant",
          "cites": {
            "spec": "id-jag",
            "revision": "draft-04",
            "section": "4.3.4. Response"
          },
          "token": {
            "name": "Identity Assertion JWT Authorization Grant",
            "header": {
              "typ": "oauth-id-jag+jwt",
              "alg": "RS256",
              "kid": "idp-2026"
            },
            "claims": {
              "iss": "https://idp.corp.example",
              "sub": "u-48213",
              "aud": "https://auth.tasks.example",
              "client_id": "https://agent.example/client.json",
              "scope": "tasks:read",
              "resource": "https://mcp.tasks.example/mcp",
              "jti": "3a9e",
              "exp": 1789996700,
              "iat": 1789996400
            },
            "note": "The required claims are iss, sub, aud, client_id, jti, exp and iat; scope and resource are optional and carry what the policy allowed."
          }
        },
        {
          "id": "access-token-request",
          "from": "client",
          "to": "as",
          "message": "POST /token: grant_type=jwt-bearer, assertion=<ID-JAG>, client authentication",
          "note": "The grant is presented as an assertion; the host authenticates with the credentials it holds at this authorization server.\n",
          "cites": {
            "spec": "id-jag",
            "revision": "draft-04",
            "section": "4.4. Access Token Request"
          }
        },
        {
          "id": "validate-jag",
          "from": "as",
          "to": "as",
          "message": "Validate the grant: signature against the IdP's keys, iss is a trusted IdP, aud is this server, exp, jti unused; map sub to an account",
          "note": "The authorization server trusts the identity provider's decision and issues no consent screen of its own.\n",
          "cites": {
            "spec": "id-jag",
            "revision": "draft-04",
            "section": "4.4.1. Processing Rules"
          }
        },
        {
          "id": "access-token",
          "from": "as",
          "to": "client",
          "message": "Access token for the tasks server, scope as granted",
          "cites": {
            "spec": "id-jag",
            "revision": "draft-04",
            "section": "4.4.2. Response"
          },
          "token": {
            "name": "Access token for the Tasks MCP server",
            "header": {
              "typ": "at+jwt",
              "alg": "ES256",
              "kid": "auth-2026-1"
            },
            "claims": {
              "iss": "https://auth.tasks.example",
              "sub": "alex",
              "aud": "https://mcp.tasks.example/mcp",
              "client_id": "https://agent.example/client.json",
              "scope": "tasks:read",
              "exp": 1790000000,
              "iat": 1789996800,
              "jti": "d02f"
            }
          }
        },
        {
          "id": "bearer-call",
          "from": "client",
          "to": "server",
          "message": "POST /mcp (tools/call) with Authorization: Bearer <access token>",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Authorization › Access Token Usage"
          },
          "token": {
            "name": "Access token as presented",
            "header": {
              "typ": "at+jwt",
              "alg": "ES256",
              "kid": "auth-2026-1"
            },
            "claims": {
              "iss": "https://auth.tasks.example",
              "sub": "alex",
              "aud": "https://mcp.tasks.example/mcp",
              "client_id": "https://agent.example/client.json",
              "scope": "tasks:read",
              "exp": 1790000000,
              "iat": 1789996800,
              "jti": "d02f"
            }
          }
        },
        {
          "id": "validate",
          "from": "server",
          "to": "server",
          "message": "Validate signature, iss, exp and that aud is this server; check scope",
          "cites": {
            "spec": "mcp-authorization",
            "revision": "2026-07-28",
            "section": "Security Considerations › Token Audience Binding and Validation"
          }
        }
      ],
      "toggles": [
        {
          "id": "scope-accumulation",
          "name": "Scope accumulation",
          "kind": "misconfiguration",
          "summary": "The resource authorization server ignores the scope in the grant and issues the access token with everything the host's client registration allows. The identity provider's policy said tasks:read; the token says tasks:read tasks:write tasks:admin. Each hop that widens scope defeats the one before it.\n",
          "cites": {
            "spec": "id-jag",
            "revision": "draft-04",
            "section": "4.4.1. Processing Rules"
          },
          "replaces": {
            "access-token": [
              {
                "id": "widened-token",
                "from": "as",
                "to": "client",
                "message": "Access token with the client's full registered scope, not the grant's",
                "note": "The grant's scope is the ceiling the identity provider set. A token wider than the grant means the central policy no longer decides what the host may do.\n",
                "cites": {
                  "spec": "id-jag",
                  "revision": "draft-04",
                  "section": "4.4.1. Processing Rules"
                },
                "token": {
                  "name": "Access token, wider than the grant",
                  "header": {
                    "typ": "at+jwt",
                    "alg": "ES256",
                    "kid": "auth-2026-1"
                  },
                  "claims": {
                    "iss": "https://auth.tasks.example",
                    "sub": "alex",
                    "aud": "https://mcp.tasks.example/mcp",
                    "client_id": "https://agent.example/client.json",
                    "scope": "tasks:read tasks:write tasks:admin",
                    "exp": 1790000000,
                    "iat": 1789996800,
                    "jti": "d02f"
                  }
                }
              }
            ],
            "bearer-call": [
              {
                "id": "widened-call",
                "from": "client",
                "to": "server",
                "message": "POST /mcp (tools/call) with the widened token",
                "cites": {
                  "spec": "mcp-authorization",
                  "revision": "2026-07-28",
                  "section": "Authorization › Access Token Usage"
                },
                "token": {
                  "name": "Access token as presented",
                  "header": {
                    "typ": "at+jwt",
                    "alg": "ES256",
                    "kid": "auth-2026-1"
                  },
                  "claims": {
                    "iss": "https://auth.tasks.example",
                    "sub": "alex",
                    "aud": "https://mcp.tasks.example/mcp",
                    "client_id": "https://agent.example/client.json",
                    "scope": "tasks:read tasks:write tasks:admin",
                    "exp": 1790000000,
                    "iat": 1789996800,
                    "jti": "d02f"
                  }
                }
              }
            ]
          }
        },
        {
          "id": "jag-audience-unchecked",
          "name": "Grant audience not checked",
          "kind": "attack",
          "summary": "The resource authorization server accepts any grant the identity provider signed, without checking that the grant's audience is itself. A grant the host obtained for a different application's authorization server is presented here and redeemed for a tasks-server token the identity provider never approved.\n",
          "cites": {
            "spec": "id-jag",
            "revision": "draft-04",
            "section": "4.4.1. Processing Rules"
          },
          "replaces": {
            "access-token-request": [
              {
                "id": "misdirected-assertion",
                "from": "client",
                "to": "as",
                "message": "POST /token: grant_type=jwt-bearer, assertion=<an ID-JAG issued for https://auth.notes.example>",
                "note": "The grant is genuine, signed by the trusted identity provider, unexpired, for the right user; only its audience is another server.\n",
                "cites": {
                  "spec": "id-jag",
                  "revision": "draft-04",
                  "section": "3.1. ID-JAG Claims"
                },
                "token": {
                  "name": "An ID-JAG for another authorization server",
                  "header": {
                    "typ": "oauth-id-jag+jwt",
                    "alg": "RS256",
                    "kid": "idp-2026"
                  },
                  "claims": {
                    "iss": "https://idp.corp.example",
                    "sub": "u-48213",
                    "aud": "https://auth.notes.example",
                    "client_id": "https://agent.example/client.json",
                    "scope": "notes:read",
                    "jti": "51c0",
                    "exp": 1789996700,
                    "iat": 1789996400
                  }
                }
              }
            ],
            "validate-jag": [
              {
                "id": "validate-jag-without-aud",
                "from": "as",
                "to": "as",
                "message": "Validate signature, iss, exp and jti; aud is not compared with this server's issuer",
                "note": "With the audience unchecked, one grant from the identity provider opens every authorization server that trusts it.\n",
                "cites": {
                  "spec": "id-jag",
                  "revision": "draft-04",
                  "section": "4.4.1. Processing Rules"
                }
              }
            ]
          }
        }
      ]
    },
    {
      "id": "token-exchange-chain",
      "title": "An orchestrator delegates to a sub-agent with token exchange",
      "summary": "Alex asks an orchestrator agent for a report. The orchestrator holds a token for the documents API in Alex's name and hands part of the work to a research sub-agent. Each hop exchanges the token it holds for a narrower one with an act claim, so the API can see the whole chain of who acted for whom, and no hop can do more than the one before it.\n",
      "actors": [
        {
          "id": "user",
          "kind": "user",
          "name": "Alex"
        },
        {
          "id": "orchestrator",
          "kind": "agent",
          "name": "Orchestrator agent"
        },
        {
          "id": "as",
          "kind": "authorization-server",
          "name": "auth.docs.example"
        },
        {
          "id": "subagent",
          "kind": "agent",
          "name": "Research sub-agent"
        },
        {
          "id": "api",
          "kind": "resource",
          "name": "Documents API"
        }
      ],
      "features": [
        "mcp.rs.no-passthrough",
        "mcp.rs.audience-validation"
      ],
      "steps": [
        {
          "id": "request",
          "from": "user",
          "to": "orchestrator",
          "message": "Write me a report from this quarter's documents",
          "note": "The orchestrator already holds an access token for the documents API that Alex obtained through the usual authorization code flow.\n",
          "cites": {
            "spec": "rfc8693",
            "revision": "rfc",
            "section": "1.1. Delegation vs. Impersonation Semantics"
          },
          "token": {
            "name": "Alex's access token held by the orchestrator",
            "header": {
              "typ": "at+jwt",
              "alg": "ES256",
              "kid": "docs-2026"
            },
            "claims": {
              "iss": "https://auth.docs.example",
              "sub": "alex",
              "aud": "https://api.docs.example",
              "client_id": "orchestrator",
              "scope": "docs:read docs:write",
              "exp": 1790000000,
              "iat": 1789996400,
              "jti": "a1"
            }
          }
        },
        {
          "id": "first-exchange",
          "from": "orchestrator",
          "to": "as",
          "message": "POST /token: grant_type=token-exchange, subject_token=<Alex's token>, actor_token=<orchestrator credential>, audience=https://api.docs.example, scope=docs:read",
          "note": "Delegation, not impersonation: the orchestrator identifies itself with an actor token and asks for less than it holds.\n",
          "cites": {
            "spec": "rfc8693",
            "revision": "rfc",
            "section": "2.1. Request"
          }
        },
        {
          "id": "first-delegated",
          "from": "as",
          "to": "orchestrator",
          "message": "Delegated token: sub=alex, act.sub=orchestrator, scope=docs:read",
          "cites": {
            "spec": "rfc8693",
            "revision": "rfc",
            "section": "4.1. act (Actor) Claim"
          },
          "token": {
            "name": "Delegated token for the orchestrator",
            "header": {
              "typ": "at+jwt",
              "alg": "ES256",
              "kid": "docs-2026"
            },
            "claims": {
              "iss": "https://auth.docs.example",
              "sub": "alex",
              "aud": "https://api.docs.example",
              "client_id": "orchestrator",
              "scope": "docs:read",
              "act": {
                "sub": "orchestrator"
              },
              "may_act": {
                "sub": "research-subagent"
              },
              "exp": 1789998200,
              "iat": 1789996500,
              "jti": "a2"
            },
            "note": "may_act says which party the subject allows to act on this token next; the authorization server checks it on the following exchange."
          }
        },
        {
          "id": "hand-over",
          "from": "orchestrator",
          "to": "subagent",
          "message": "Task: summarise the quarter's documents; here is the delegated token to exchange",
          "note": "The sub-agent does not present this token to the API. It is the subject token for the sub-agent's own exchange.\n",
          "cites": {
            "spec": "rfc8693",
            "revision": "rfc",
            "section": "1.1. Delegation vs. Impersonation Semantics"
          },
          "token": {
            "name": "Delegated token, handed to the sub-agent",
            "header": {
              "typ": "at+jwt",
              "alg": "ES256",
              "kid": "docs-2026"
            },
            "claims": {
              "iss": "https://auth.docs.example",
              "sub": "alex",
              "aud": "https://api.docs.example",
              "client_id": "orchestrator",
              "scope": "docs:read",
              "act": {
                "sub": "orchestrator"
              },
              "may_act": {
                "sub": "research-subagent"
              },
              "exp": 1789998200,
              "iat": 1789996500,
              "jti": "a2"
            }
          }
        },
        {
          "id": "second-exchange",
          "from": "subagent",
          "to": "as",
          "message": "POST /token: grant_type=token-exchange, subject_token=<delegated token>, actor_token=<sub-agent credential>, audience=https://api.docs.example, scope=docs:read",
          "cites": {
            "spec": "rfc8693",
            "revision": "rfc",
            "section": "2.1. Request"
          }
        },
        {
          "id": "may-act-check",
          "from": "as",
          "to": "as",
          "message": "Check may_act on the subject token names the actor; keep scope within the subject token's",
          "note": "The chain can only grow by parties the previous token authorised, and never wider.\n",
          "cites": {
            "spec": "rfc8693",
            "revision": "rfc",
            "section": "4.4. may_act (Authorized Actor) Claim"
          }
        },
        {
          "id": "second-delegated",
          "from": "as",
          "to": "subagent",
          "message": "Delegated token with a nested act chain",
          "cites": {
            "spec": "rfc8693",
            "revision": "rfc",
            "section": "4.1. act (Actor) Claim"
          },
          "token": {
            "name": "Delegated token for the sub-agent",
            "header": {
              "typ": "at+jwt",
              "alg": "ES256",
              "kid": "docs-2026"
            },
            "claims": {
              "iss": "https://auth.docs.example",
              "sub": "alex",
              "aud": "https://api.docs.example",
              "client_id": "research-subagent",
              "scope": "docs:read",
              "act": {
                "sub": "research-subagent",
                "act": {
                  "sub": "orchestrator"
                }
              },
              "exp": 1789997900,
              "iat": 1789996600,
              "jti": "a3"
            },
            "note": "The outermost act is the current actor; each nested act is the party it received authority from."
          }
        },
        {
          "id": "api-call",
          "from": "subagent",
          "to": "api",
          "message": "GET /documents?quarter=2026-Q3 with Authorization: Bearer <delegated token>",
          "cites": {
            "spec": "rfc9068",
            "revision": "rfc",
            "section": "4. Validating JWT Access Tokens"
          },
          "token": {
            "name": "Delegated token as presented",
            "header": {
              "typ": "at+jwt",
              "alg": "ES256",
              "kid": "docs-2026"
            },
            "claims": {
              "iss": "https://auth.docs.example",
              "sub": "alex",
              "aud": "https://api.docs.example",
              "client_id": "research-subagent",
              "scope": "docs:read",
              "act": {
                "sub": "research-subagent",
                "act": {
                  "sub": "orchestrator"
                }
              },
              "exp": 1789997900,
              "iat": 1789996600,
              "jti": "a3"
            }
          }
        },
        {
          "id": "api-validate",
          "from": "api",
          "to": "api",
          "message": "Validate signature, iss, aud and exp; enforce docs:read; log sub and the act chain",
          "note": "The audit record says the sub-agent read documents for Alex by way of the orchestrator, and the API can refuse actors it does not expect.\n",
          "cites": {
            "spec": "rfc8693",
            "revision": "rfc",
            "section": "4.1. act (Actor) Claim"
          }
        },
        {
          "id": "documents",
          "from": "api",
          "to": "subagent",
          "message": "200 with the documents",
          "cites": {
            "spec": "rfc9068",
            "revision": "rfc",
            "section": "4. Validating JWT Access Tokens"
          }
        }
      ],
      "toggles": [
        {
          "id": "token-passthrough",
          "name": "Token passthrough",
          "kind": "attack",
          "summary": "The orchestrator hands Alex's original token to the sub-agent and the sub-agent presents it as is. No exchange, no act claim, full docs:write scope: the API sees Alex, cannot tell which agent is calling, and a sub-agent that goes wrong can write and delete in Alex's name.\n",
          "cites": {
            "spec": "rfc8693",
            "revision": "rfc",
            "section": "1.1. Delegation vs. Impersonation Semantics"
          },
          "replaces": {
            "first-exchange": [],
            "first-delegated": [],
            "hand-over": [
              {
                "id": "hand-over-raw",
                "from": "orchestrator",
                "to": "subagent",
                "message": "Task plus Alex's token, unchanged",
                "cites": {
                  "spec": "rfc8693",
                  "revision": "rfc",
                  "section": "1.1. Delegation vs. Impersonation Semantics"
                },
                "token": {
                  "name": "Alex's token, forwarded",
                  "header": {
                    "typ": "at+jwt",
                    "alg": "ES256",
                    "kid": "docs-2026"
                  },
                  "claims": {
                    "iss": "https://auth.docs.example",
                    "sub": "alex",
                    "aud": "https://api.docs.example",
                    "client_id": "orchestrator",
                    "scope": "docs:read docs:write",
                    "exp": 1790000000,
                    "iat": 1789996400,
                    "jti": "a1"
                  }
                }
              }
            ],
            "second-exchange": [],
            "may-act-check": [],
            "second-delegated": [],
            "api-call": [
              {
                "id": "api-call-raw",
                "from": "subagent",
                "to": "api",
                "message": "GET /documents with Authorization: Bearer <Alex's token>",
                "note": "Nothing in the token says a sub-agent is calling. The API's audit trail names Alex and the orchestrator's client id, and the scope allows writes the task never needed.\n",
                "cites": {
                  "spec": "rfc8693",
                  "revision": "rfc",
                  "section": "1.1. Delegation vs. Impersonation Semantics"
                },
                "token": {
                  "name": "Alex's token as presented by the sub-agent",
                  "header": {
                    "typ": "at+jwt",
                    "alg": "ES256",
                    "kid": "docs-2026"
                  },
                  "claims": {
                    "iss": "https://auth.docs.example",
                    "sub": "alex",
                    "aud": "https://api.docs.example",
                    "client_id": "orchestrator",
                    "scope": "docs:read docs:write",
                    "exp": 1790000000,
                    "iat": 1789996400,
                    "jti": "a1"
                  }
                }
              }
            ]
          },
          "violates": [
            "mcp.rs.no-passthrough"
          ]
        },
        {
          "id": "scope-accumulation",
          "name": "Scope accumulation",
          "kind": "misconfiguration",
          "summary": "The authorization server grants the sub-agent's exchange request more scope than the subject token carried: the sub-agent asked for docs:read docs:admin and got it, although the token it exchanged only had docs:read. Authority grew at a hop that should only ever narrow it.\n",
          "cites": {
            "spec": "rfc8693",
            "revision": "rfc",
            "section": "2.2.1. Successful Response"
          },
          "replaces": {
            "second-exchange": [
              {
                "id": "second-exchange-wider",
                "from": "subagent",
                "to": "as",
                "message": "POST /token: grant_type=token-exchange, subject_token=<delegated token>, actor_token=<sub-agent credential>, scope=docs:read docs:admin",
                "cites": {
                  "spec": "rfc8693",
                  "revision": "rfc",
                  "section": "2.1. Request"
                }
              }
            ],
            "may-act-check": [
              {
                "id": "may-act-check-no-scope-bound",
                "from": "as",
                "to": "as",
                "message": "Check may_act; grant the requested scope without comparing it to the subject token's",
                "note": "The specification leaves the issued token's scope to the authorization server's policy. A policy that lets a delegated token exceed its subject token breaks the chain's one guarantee.\n",
                "cites": {
                  "spec": "rfc8693",
                  "revision": "rfc",
                  "section": "2.2.1. Successful Response"
                }
              }
            ],
            "second-delegated": [
              {
                "id": "second-delegated-wider",
                "from": "as",
                "to": "subagent",
                "message": "Delegated token with docs:admin the subject token never had",
                "cites": {
                  "spec": "rfc8693",
                  "revision": "rfc",
                  "section": "4.2. scope (Scopes) Claim"
                },
                "token": {
                  "name": "Delegated token, wider than its subject token",
                  "header": {
                    "typ": "at+jwt",
                    "alg": "ES256",
                    "kid": "docs-2026"
                  },
                  "claims": {
                    "iss": "https://auth.docs.example",
                    "sub": "alex",
                    "aud": "https://api.docs.example",
                    "client_id": "research-subagent",
                    "scope": "docs:read docs:admin",
                    "act": {
                      "sub": "research-subagent",
                      "act": {
                        "sub": "orchestrator"
                      }
                    },
                    "exp": 1789997900,
                    "iat": 1789996600,
                    "jti": "a3"
                  }
                }
              }
            ]
          }
        },
        {
          "id": "may-act-unchecked",
          "name": "may_act not checked",
          "kind": "misconfiguration",
          "summary": "The authorization server exchanges the delegated token for any actor that presents it, without checking that the subject token's may_act names that actor. Whoever obtains the orchestrator's delegated token can extend the chain in Alex's name.\n",
          "cites": {
            "spec": "rfc8693",
            "revision": "rfc",
            "section": "4.4. may_act (Authorized Actor) Claim"
          },
          "replaces": {
            "second-exchange": [
              {
                "id": "second-exchange-by-stranger",
                "from": "subagent",
                "to": "as",
                "message": "POST /token: grant_type=token-exchange, subject_token=<delegated token>, actor_token=<credential of an agent the token never authorised>",
                "cites": {
                  "spec": "rfc8693",
                  "revision": "rfc",
                  "section": "2.1. Request"
                }
              }
            ],
            "may-act-check": [],
            "second-delegated": [
              {
                "id": "second-delegated-to-stranger",
                "from": "as",
                "to": "subagent",
                "message": "Delegated token with an actor the subject never allowed",
                "cites": {
                  "spec": "rfc8693",
                  "revision": "rfc",
                  "section": "4.1. act (Actor) Claim"
                },
                "token": {
                  "name": "Delegated token for an unauthorised actor",
                  "header": {
                    "typ": "at+jwt",
                    "alg": "ES256",
                    "kid": "docs-2026"
                  },
                  "claims": {
                    "iss": "https://auth.docs.example",
                    "sub": "alex",
                    "aud": "https://api.docs.example",
                    "client_id": "stranger-agent",
                    "scope": "docs:read",
                    "act": {
                      "sub": "stranger-agent",
                      "act": {
                        "sub": "orchestrator"
                      }
                    },
                    "exp": 1789997900,
                    "iat": 1789996600,
                    "jti": "a4"
                  }
                }
              }
            ]
          }
        }
      ]
    }
  ]
}