Skip to content

Scopes, claims, and discovery

Four scopes are advertised in discovery as scopes_supported:

ScopeEffect
openidIssues an ID token alongside the access token.
profileAdds name and email to the ID token.
work_credentialPermits issuing a verifiable Work Credential for the subject.
offline_accessIssues a refresh token.

There is no separate email scope. Requesting one has no effect — the email claim travels with profile.

A scope is only granted if it is also in the client’s own allowed_scopes. Requesting more than the client is registered for narrows silently to the intersection rather than failing, so check the scope value in the token response rather than assuming you got what you asked for.

Agent and MCP scopes are namespaced and matched by pattern rather than enumerated:

mcp:<name> tool:<name> a2a:<name>

Names may contain letters, digits, _, ., -, and * — for example mcp:tools.invoke, tool:search, tool:email.*.

A client may request a tool scope only if it appears in its own allowed_scopes, which is set when the agent is registered through the management API. Dynamically registered and URL clients cannot obtain tool scopes at all — that path is deliberately closed, because those clients are anonymous.

{
"iss": "https://acme.oauth.work",
"sub": "usr_01J…",
"aud": "https://tools.acme.com/mcp",
"exp": 1750003600,
"iat": 1750000000,
"jti": "",
"client_id": "client_7f3a…",
"scope": "openid profile",
"org_id": "org_01J…",
"roles": ["admin"],
"actor_type": "user",
"cnf": { "jkt": "" },
"act": { "sub": "did:key:z6Mk…" },
"authorization_details": [ ]
}
ClaimPresent when
audA resource indicator was bound to the grant
org_idThe subject or client belongs to an organization
rolesThe subject has a role in that organization
actor_typeAlways for machine tokens: user, agent, or service
cnf.jktThe token is DPoP-bound
actThe token came from token exchange — the delegation chain
authorization_detailsRich authorization was requested
{
"iss": "https://acme.oauth.work",
"sub": "usr_01J…",
"aud": "client_7f3a…",
"exp": 1750003600,
"nonce": "",
"name": "Ada Lovelace",
"email": "ada@acme.com",
"org_id": "org_01J…",
"roles": ["admin"],
"auth_time": 1750000000,
"amr": ["pwd", "totp"]
}

name and email require the profile scope. amr reports how the user actually authenticated — values include otp, link, pwd, sso, and totp — and multiple entries mean multiple factors, so ["pwd","totp"] is a password plus a second factor. Use amr and auth_time together to decide whether a session is fresh and strong enough for a sensitive operation, rather than asking for re-authentication unconditionally.

RFC 8707. Pass resource at /authorize, /par, or /token to bind the token’s audience to a specific service:

&resource=https://tools.acme.com/mcp

The value becomes aud — a single string for one resource, an array for several. resource may repeat, and a value passed at /token must be a subset of what was bound at /authorize; otherwise the request fails with invalid_target.

A resource server must check that aud names it. Without that check, a token minted for one service is replayable at every other service that trusts the same issuer, and the indicator has bought you nothing.

DocumentContents
/.well-known/openid-configurationOpenID Provider metadata
/.well-known/oauth-authorization-serverAuthorization server metadata (RFC 8414)
/.well-known/oauth-protected-resourceProtected resource metadata (RFC 9728)
/.well-known/jwks.jsonPublic signing keys
/.well-known/did.jsonThe tenant’s did:web document
/.well-known/openid-credential-issuerCredential issuer metadata (OID4VCI)
/.well-known/agent-card.jsonA2A AgentCard (also at /.well-known/agent.json)
/auth.mdThe agent skill document, in Markdown

All of these are served per host, so a tenant’s documents describe that tenant. The four OAuth-related ones send CORS headers, because browser relying parties and MCP inspectors fetch them cross-origin — as does /auth.md, which agents reach by following the agent_auth.skill URL out of the authorization server metadata.

{
"issuer": "https://acme.oauth.work",
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token",
"urn:ietf:params:oauth:grant-type:token-exchange"
],
"response_types_supported": ["code"],
"code_challenge_methods_supported": ["S256"],
"id_token_signing_alg_values_supported": ["EdDSA"],
"token_endpoint_auth_methods_supported": [
"none", "client_secret_basic", "client_secret_post", "private_key_jwt"
],
"token_endpoint_auth_signing_alg_values_supported": ["ES256", "EdDSA", "RS256"],
"resource_indicators_supported": true,
"authorization_response_iss_parameter_supported": true,
"authorization_details_types_supported": ["mcp_tool"],
"client_id_metadata_document_supported": true,
"agent_auth": { "skill": "https://acme.oauth.work/auth.md", "identity_types_supported": [] },
"require_pushed_authorization_requests": false,
"request_parameter_supported": false,
"request_uri_parameter_supported": false
}

Two absences are deliberate. response_types_supported is ["code"] only — the implicit and hybrid flows are not offered. And request objects are explicitly unsupported at both request_parameter_supported and request_uri_parameter_supported; use PAR instead, which achieves the same integrity without the client having to sign anything.

The authorization response also carries iss (RFC 9207). Check it: it is what stops a response from one authorization server being replayed at another in a multi-issuer client.