Rightshold™ Sign up

Answers · agent safety

How do I scope OAuth tokens per task for an AI agent instead of giving it one broad token?

Short answer: Give the agent a fresh, short-lived token for each task, limited to the scopes and the one resource that task needs, and record who delegated it. Keep the user's broad grant (or refresh token) on your server, never in the agent's context. When a task starts, mint a token for it: request only the task's scopes, name the target API with the resource parameter (RFC 8707) so the token works nowhere else, and keep its lifetime to minutes. Where your identity provider supports OAuth 2.0 Token Exchange (RFC 8693), exchange the user's token for a narrower one in which the act claim names the agent acting on the user's behalf, so every downstream log shows both the person and the agent. When the agent hits something its token can't do, it should get a 403 insufficient_scope and ask for a step-up with the user's approval, not fall back to a bigger token. Write a delegation log entry for every token: user, agent, task, scopes, audience, expiry and token ID.

Checklist

  1. Split the grant: the user's long-lived consent and refresh token stay in your backend or token vault. The agent only ever holds the short-lived access token for the task in hand.
  2. One task, one token: request only the scopes the task needs (for example calendar.events.read for a scheduling lookup, not full calendar access) and name a single audience with the resource parameter (RFC 8707). MCP clients must already send resource for MCP servers.
  3. Keep lifetimes short: minutes for an access token, ending when the task does, and no refresh token in the agent's hands. A leaked or misused token then expires on its own.
  4. Use token exchange for delegation: RFC 8693 lets a service swap the user's token (subject_token) plus the agent's own credential (actor_token) for a narrower token whose act claim records the agent. In Microsoft Entra ID the equivalent is the on-behalf-of flow.
  5. Narrow further where the server supports it: Rich Authorization Requests (RFC 9396) express a grant as structured authorization_details, such as one payment amount or one folder, instead of a broad scope string.
  6. Bind the token to the agent where you can: DPoP (RFC 9449) sender-constrains a token to a key the agent holds, so a copied token is useless elsewhere.
  7. Handle missing permissions with a step-up, not a wider default: the MCP authorization spec has servers answer 403 with error="insufficient_scope" and the scopes needed; the client asks the user to approve those, with a retry limit.
  8. Log every delegation and every call: user, agent, task ID, scopes, audience, issued and expiry times, token ID (jti), and the tool calls made with it, with token values kept out of the logs. Revoke the task's tokens when the task ends or is cancelled.

Why one broad token is the wrong unit

What a delegated token should say

Ask Rightshold (free, no key)

Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “An agent presented this RS-Intent token. Is it still in force, signed by that agent, and does it cover the operation and domain I'm about to allow?”

One-click setup

{
  "tool": "verify_intent",
  "arguments": {
    "token": "<the RS-Intent value the agent presented>",
    "operation": "<the operation, for example read>",
    "domain": "<your-domain.com>"
  }
}

Rightshold does not issue OAuth tokens for your APIs or run your authorization server. Per-task OAuth scoping is configured in your identity provider, token service and resource servers; Rightshold's intents and intent keys scope agents' use of Rightshold itself and give sites a token they can verify.

Sources

Related

Not legal advice. Updated 2026-10-29. 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}}'