Skip to content

PAR and authorization details

Two extensions that matter most for agent authorization: PAR moves the authorization request off the browser URL, and Rich Authorization Requests replace a flat scope string with structured permissions.

RFC 9126. Instead of putting every parameter in a front-channel redirect — where it is visible in browser history, Referer headers, and server logs, and where anything not signed can be tampered with — the client POSTs the request over the back channel and receives a handle.

POST /par
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/x-www-form-urlencoded
response_type=code
&client_id=client_7f3a…
&redirect_uri=https://app.acme.com/callback
&scope=openid%20profile
&code_challenge=<S256>
&code_challenge_method=S256
&state=
{
"request_uri": "urn:ietf:params:oauth:request_uri:8f3b2c…",
"expires_in": 90
}

The front-channel redirect then carries only two parameters:

GET /authorize?client_id=client_7f3a…&request_uri=urn:ietf:params:oauth:request_uri:8f3b2c…

The request_uri is single-use and valid for 90 seconds. Redeeming it consumes it, so a replayed authorization request fails.

/par accepts response_type, redirect_uri, scope, state, nonce, code_challenge, code_challenge_method, organization, resource (repeatable), and authorization_details.

Because it runs on the back channel, /par can fail fast with a real error instead of a redirect:

  • Confidential clients must authenticate, exactly as at /token.
  • redirect_uri must already be registered for the client.
  • response_type must be code.
  • code_challenge is required, and only S256 is accepted.
  • A pushed request may not itself carry a request_uri.

That is the practical benefit during integration: a misconfigured redirect URI surfaces as a JSON error on your own server rather than as a redirect loop in someone’s browser.

PAR is supported but not required — discovery reports require_pushed_authorization_requests: false.

RFC 9396. A scope string says what kind of access; authorization_details says which resources, which actions, and how much. For agents this is the difference between “may call tools” and “may call these two tools, at this server, at most fifty times”.

[
{
"type": "mcp_tool",
"locations": ["https://tools.acme.com/mcp"],
"tools": ["search", "weather"],
"max_calls": 50
}
]

Send it URL-encoded on /authorize, or as a parameter on /par:

GET /authorize
?response_type=code
&client_id=agent-research-bot
&code_challenge=<S256>
&authorization_details=%5B%7B%22type%22%3A%22mcp_tool%22%2C…%5D

mcp_tool is the only supported type today; discovery lists it under authorization_details_types_supported. An unknown type, or malformed JSON, is rejected with invalid_authorization_details rather than being silently dropped — a permission request that is not understood must not be treated as no request at all.

FieldMeaning
typeThe detail type. Must be mcp_tool.
locationsThe resource(s) this detail applies to.
actionsActions being requested.
toolsThe tool names this grant authorizes.
max_callsAn advisory per-grant call budget.

Granted details are echoed in the token response and carried as a claim in the access token:

{
"sub": "usr_01J…",
"client_id": "agent-research-bot",
"scope": "mcp:tools.invoke",
"authorization_details": [
{ "type": "mcp_tool", "locations": ["https://tools.acme.com/mcp"], "tools": ["search"] }
]
}

A resource server should enforce them alongside scope and audience. max_calls is advisory — the authorization server records what was granted; counting invocations is the resource server’s job.

For an agent connecting to an MCP server, the strong combination is: push the request with PAR so the requested tools cannot be tampered with in the browser, describe the grant with authorization_details so consent shows the user exactly which tools are being requested, bind the audience with a resource indicator, and take the token under DPoP so it cannot be used anywhere else.

See MCP authorization for that flow end to end.