Step-Up Authentication Challenge
Lets an API pull a human back in for sensitive operations instead of trusting a long-lived grant.
Flows / Human in the loop
Start with a weak token. When the agent reaches something sensitive, the API says so and the user is asked for exactly that.
Asking for every permission up front is how agents end up holding tokens that can do far more than the task needs. The alternative is to begin with the minimum and let the resource server say when more is required.
Two standards make this workable. The step-up challenge lets an API answer “this token is fine, but not for this: come back with a stronger authentication or a broader scope.” Rich Authorization Requests let the follow-up request describe one specific action in structured detail instead of a coarse scope string. Together they give a pattern where routine work flows without interruption and a human is pulled in only for the moments that matter, to approve something concrete.
12 steps between 4 parties. Each step shows the message and who holds which credential afterward. Use ←→ or select any arrow.
Drag to see every party and step. Tap a step to jump to its explanation.
The agent has a token with accounts.read, obtained when the user first connected the bank. It now attempts a transfer.
Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
The token is valid. The policy for this endpoint asks for more: a scope the token lacks, a recent authentication, a phishing-resistant factor, or explicit approval of the transaction. The requirement lives at the API, which is the only place it cannot be bypassed.
Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
The WWW-Authenticate header is machine-readable. insufficient_scope names the scope needed. insufficient_user_authentication names the authentication strength (acr_values) or freshness (max_age) required. The agent does not have to guess.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="insufficient_user_authentication",
error_description="A recent phishing-resistant login is required",
acr_values="urn:example:acr:phishing-resistant",
max_age=300Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
A well-behaved agent surfaces the reason in its own interface before sending the user anywhere. This is courtesy, not security. The authorization server will show the authoritative version.
Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
The agent echoes the API’s requirements and attaches authorization_details: a typed JSON object describing the transfer. Sending this through a pushed authorization request (PAR) keeps the details out of the browser’s address bar and stops anyone tampering with them on the way.
{
"acr_values": "urn:example:acr:phishing-resistant",
"max_age": 300,
"authorization_details": [
{
"type": "transfer",
"locations": ["https://bank.example/api"],
"from_account": "…4411",
"to": { "name": "Riverside Landscaping", "iban": "DE89 3704 0044 0532 0130 00" },
"amount": { "currency": "EUR", "value": "480.00" }
}
]
}Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
The user is asked for the stronger factor and shown the transfer as the server received it: payee, account, amount. If injected instructions changed the payee, this is where a person can catch it.
Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
Not “allow the agent to make transfers.” This transfer.
Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
The usual authorization code response, protected by PKCE.
Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
Identical to the baseline flow.
Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
The new token records how and when the user authenticated (acr, auth_time) and includes the approved authorization_details.
{
"sub": "user:alice",
"aud": "https://bank.example/api",
"acr": "urn:example:acr:phishing-resistant",
"auth_time": 1794268790,
"exp": 1794269090,
"authorization_details": [
{ "type": "transfer", "to": { "iban": "DE89 3704 0044 0532 0130 00" },
"amount": { "currency": "EUR", "value": "480.00" } }
]
}Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
The API compares the request body with the authorization details in the token. They must match.
Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
The stronger token expires in minutes. The agent drops back to its everyday token for everything else.
Who holds what after this step
Parties not listed hold none of this flow’s credentials at this point.
See the full tracker for status and links.
Lets an API pull a human back in for sensitive operations instead of trusting a long-lived grant.
Consent for one payment or one file, not “payments” or “files.” The base of several agent proposals.
Keeps detailed, transaction-specific requests out of URLs and safe from tampering.
The default way agents present tokens, and the thing proof-of-possession schemes are trying to replace.
The most widely implemented authorization profile written specifically for agents and tools.
The agent asks the authorization server to go and find the user. Approval arrives as a push notification, not a redirect.
The baseline. A person signs in, approves specific scopes, and the agent receives a token that says so.
How an MCP client that has never seen a server before discovers where to sign in, identifies itself, and gets a token good for that server alone.
A tool needs the user to sign in somewhere else. It hands back a link, the user completes the flow in a browser, and the secret never touches the agent.