Answers · agent safety
How do I bind a one-time human confirmation to an OAuth client/connector and exact action, then consume it?
Last updated
TL;DR: A standing OAuth grant for a connector is not a yes for every tools/call that connector can make. For high-risk actions, mint a one-time confirmation that names the OAuth client_id (or connector id), the exact action (tool name + argument hash), and optionally the step-up strength you required (RFC 9470 acr_values / max_age). Store it outside the agent session, show the human the real client and arguments, then at execution verify client_id + action hash, mark the confirmation consumed, and refuse reuse. Pair OAuth step-up when the access token's authentication event is too weak or too old with an application-level single-use approval so a refreshed token cannot silently replay the same dangerous call.
What should I check, step by step?
1. Identify the OAuth client or connector that will perform the side effect (MCP client_id, IDE connector id, or registered app id). Do not accept a confirmation that omits client_id.
- Freeze the action: tool or API name, canonical argument hash, resource/audience, and policy_version. Hash those fields the same way at approve time and at execute time.
- If the access token's auth event is too weak or stale for this action, challenge with RFC 9470 insufficient_user_authentication (acr_values / max_age) so the human re-authenticates before you even open the confirmation prompt.
- Call request_human_approval with summary plus action { client_id, connector, tool, arguments_sha256, audience, policy_version }. Expire in minutes, not days.
- Keep the approval id and decision on the server (or ledger). Give the agent only the approval id — never confirmed=true in chat memory.
- Immediately before the side effect: verify_human_approval against the live action hash and client_id; reject on mismatch, expiry, or prior consumption; then mark the confirmation single-use consumed in the same transaction as the start of the side effect.
- Refuse a second tools/call that presents the same approval id even with identical arguments unless you deliberately issued a multi-use grant with an explicit remaining_uses counter.
- Log client_id, approval id, consume time, and allow/deny outside the agent session (record_ai_run or your SIEM). Rotate or revoke the OAuth client if confirmations are being stolen or replayed.
Why the OAuth grant alone is not enough
OAuth access tokens authorize a client at a resource; they do not bind a human's yes to one tool invocation. RFC 9700 (OAuth 2.0 Security BCP) pushes sender-constrained tokens, exact redirect URI matching, and audience restriction — necessary baselines that still leave room for a compromised agent to call every scope the token carries.
- RFC 9470 lets a resource server demand a stronger or fresher authentication event (insufficient_user_authentication with acr_values and max_age). Use that when the connector's token was minted hours ago with a weak ACR, then still bind a separate one-time confirmation to the exact action so the new token cannot be reused for a different tool call.
- MCP's authorization profile requires audience-bound tokens and forbids token passthrough; clients must send the resource parameter. That stops confused-deputy token reuse across servers — it does not consume a human yes after one delete or one payment.
- MCP Tools guidance: applications SHOULD present confirmation prompts for operations so a human stays in the loop. Product elicitation (MCP elicitation / MRTR) and an external hashed approval are complementary: elicitation is the UX; the consumed confirmation record is the enforcement.
Approve once, verify client + action, consume
Put client_id (and connector label humans recognize) in the approval action object alongside tool and arguments_sha256. verify_human_approval must fail if the agent swaps to another OAuth client that shares a user grant.
- OpenAI Agents SDK human-in-the-loop patterns keep approval state on the server for browser UIs; do not treat a client-uploaded "already confirmed" flag as authorization.
- Consume in the same durable transaction that begins the side effect (row lock on approval id → check → mark consumed → run). If the tool fails after consume, open a new confirmation — do not resurrect the old id.
- For money moves, layer set_spend_policy and authorize_spend so the one-time human yes and the spend cap are both checked at execution. For connector installs, precall_check the MCP server fingerprint before you ever request confirmation.
What can go wrong?
Confirmation UI shows "GitHub connector" while the token presented belongs to a different client_id the user also authorized.
- Approval is bound to prose ("delete the stale rows") but not to arguments_sha256, so the agent changes the id list after the yes.
- Step-up returns a new access token and the agent replays an old, unconsumed confirmation from chat memory.
- Consume happens only after a slow upstream succeeds, so two parallel tools/call requests both pass verify and both run.
How do I ask Rightshold about this (free, no key)?
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “Before this OAuth connector runs a risky tool call, ask me to approve the exact client id, tool and argument hash, expire the yes quickly, and wait for my decision so it can be consumed once.”
{
"tool": "request_human_approval",
"arguments": {
"summary": "Allow cursor-mcp-github (client_id md.cursor.github) to call issues.create on repo acme/payments (args sha256:7a1c…); consume after one use",
"action": {
"type": "oauth_connector_action",
"client_id": "md.cursor.github",
"connector": "GitHub",
"tool": "issues.create",
"arguments_sha256": "7a1c…",
"audience": "https://api.github.com",
"policy_version": 9
},
"expires_in_minutes": 10
}
}- Needs your API key. The action is hashed; decide once with decide_human_approval. verify_human_approval fails if client_id or arguments changed. Treat the token as single-use in your runner.
- If the access token's ACR/auth_time is too weak, challenge with RFC 9470 step-up before opening this approval.
- record_ai_run stores a signed receipt of the confirm→consume decision outside the agent session.
- How to ask the user to confirm an action from an MCP server with elicitation and How to re-check agent authority at execution time cover the UX and the execute-time grant check.
Rightshold records human approval requests and signed decisions for agents that call it. It does not run your authorization server, implement RFC 9470 challenges, or stop an OAuth connector that never verifies or consumes the approval. Enforce client_id + action hash checks and single-use consume in your connector and tool runner. Not legal advice.
What are the sources?
- RFC 9470: OAuth 2.0 Step Up Authentication Challenge Protocol
- RFC 9700: Best Current Practice for OAuth 2.0 Security
- Model Context Protocol specification: Authorization (audience binding; resource parameter)
- Model Context Protocol specification: Tools (human in the loop; confirmation prompts)
- OpenAI Agents SDK (JS): Human-in-the-loop (keep approval state on the server)
- Human approvals on Rightshold
Related
- How to ask the user to confirm an action from an MCP server with elicitation
- How to re-check agent authority at execution time
- How to scope OAuth tokens per task for AI agents
- How to audit which human authorized an AI agent action
Not legal advice. JSON · Markdown · All answers
Get a free API key in one call
No card and no email needed. 100 free credits; the license and policy checks stay free without a key.
curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_... # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
-H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'
