OAuth 2.0 for AI Agents: Delegated Access Without Leaking User Credentials
Your agent may need to read a customer’s calendar, send a message from their account, or file a ticket under their name. A shared service account with broad access is quick, but every action appears as “the integration,” attribution is lost, and one compromised credential can expose every account.
The safer design is delegated authorization: the user grants the agent a scoped, revocable token, the agent acts with that user’s permissions, and the audit trail identifies them. This is what OAuth 2.0 was designed for.
OAuth becomes awkward for agents because it assumes a browser and a person clicking Allow, while agents often run unattended at 3 AM. This guide shows how to choose an OAuth flow, scope and store tokens, handle refresh and revocation, and test the complete path without a live account.
If you are choosing between key-based and delegated authentication, start with API keys vs. OAuth.
Service account or delegated access?
Choose deliberately: the two models fail differently.
A service account is the agent’s own identity and permissions. Use it for work against your own resources, such as:
- Reading your database
- Calling internal services
- Running scheduled jobs
- Operating your infrastructure
Scope it tightly, following the principles in least-privilege API keys for agents, and rotate it regularly.
Delegated access means the agent acts as a specific user with that user’s permissions and no more. Use it when the data belongs to someone else. It provides three important guarantees:
- The user can see what they granted.
- The user can revoke access.
- Each action carries the user’s identity in the log.
Avoid using an organization-wide service account to act “as” individual users. One leaked credential then exposes everyone, per-user revocation is impossible, and the audit trail is misleading.
Which OAuth flow fits an agent?
OAuth 2.0 defines several grant types. These are the ones most relevant to agents.
Authorization code with PKCE
This is the standard flow for acting as a user:
- Redirect the user to the provider.
- Let them approve the requested scopes.
- Exchange the authorization code for tokens.
PKCE protects the exchange and is the default recommendation for client types under the OAuth 2.0 Security Best Current Practice. See the authorization code grant walkthrough for the request details.
For agents, separate the lifecycle into two phases:
- Connect time: A human completes the flow once.
- Run time: The agent uses the resulting refresh token.
The agent should not run the consent flow during normal execution.
Client credentials
This is machine-to-machine authentication with no user involved. It is correct for service accounts and incorrect when the agent must act as a user.
Device authorization grant
Use this for CLI agents and headless machines. The agent displays a code, and the user approves the request on another device.
Token exchange
RFC 8693 allows a service to exchange a broader token for a narrower one. A multi-agent system can use this to give a sub-agent a token limited to one scope and one task without exposing the original user grant.
This makes per-agent credentials practical and supports the boundaries described in multi-agent handoff.
Scope access narrowly and per agent
Scopes are where delegated access provides its main security benefit. They are also where implementations often become overly permissive.
Request only what the agent needs
A scheduling agent may need calendar read and write access, but not mail, contacts, or files. Users review consent screens, so excessive scopes create both a trust problem and a larger blast radius. See what OAuth 2 scopes are.
Request scopes incrementally
Ask for the minimum at connection time. Request additional scopes only when the user enables a feature that requires them. Consent tied to a concrete action is easier to understand and approve.
Give each agent its own token
If a research agent and a billing agent act for the same user, derive separate tokens with different scopes. A compromised research agent should not be able to issue refunds, and the audit log should identify which agent acted.
Prefer read-only scopes by default. Require explicit escalation for writes, and add an approval gate for destructive operations as described in AI agent guardrails.
Store, refresh, and revoke tokens safely
Tokens are credentials. Treat them like credentials.
Storage
- Encrypt refresh tokens at rest.
- Use encryption keys scoped per user where practical.
- Never write tokens to logs.
- Never place tokens in prompts or model-visible tool parameters.
- Redact authorization headers at the HTTP boundary.
Anything in model context can appear in traces, summaries, handoffs, errors, or user-facing explanations. Tracing agent tool calls covers boundary-level redaction.
Refresh
Access tokens are intentionally short-lived. Put refresh logic in a token manager in front of the HTTP client, not in the agent itself.
class TokenManager:
def __init__(self, store, provider):
self.store, self.provider = store, provider
def access_token(self, user_id, agent_scope):
rec = self.store.get(user_id, agent_scope)
if rec.expires_in() > 60:
return rec.access_token
fresh = self.provider.refresh(rec.refresh_token, scope=agent_scope)
self.store.save(user_id, agent_scope, fresh) # rotation: store the new refresh token
return fresh.access_token
Two implementation details matter:
- Persist rotated refresh tokens immediately. Many providers issue a new refresh token and invalidate the old one. Losing the new token can lock the user out.
- Serialize refreshes per user. Concurrent refreshes can race with rotating providers, causing one request to invalidate the other’s token.
Revocation
Users can revoke access, tokens can expire, and administrators can remove accounts. Treat 401 and 403 authentication failures as terminal, not retryable:
- Do not retry indefinitely.
- Stop the agent’s action.
- Explain which user and scope need attention.
- Ask the user to reconnect or request the missing scope.
Retrying authentication failures does not help and may trigger abuse protections. Follow the patterns in API error design for agents.
Solve consent by separating connect time from run time
OAuth consent requires a human. Background agents do not.
At connect time, a person authorizes the integration in a browser and you store the resulting refresh token. At run time, the agent uses that grant without requiring another interaction. This supports scheduled and unattended agents.
Plan for two limits:
- Grant expiry: Some grants expire after months of inactivity or according to provider policy. Detect this, stop the run, and notify the user instead of failing silently every night.
- Scope ceilings: If the agent needs a scope the user never granted, request consent. Never escalate permissions automatically.
For high-stakes actions, add an approval gate at execution time. The token answers “may this agent act?” The approval gate answers “should it act now?”
Test the complete flow before production
OAuth paths are often under-tested because manual testing requires navigating a provider’s consent screens.
Build at least these five test cases:
- Happy path: A valid access token produces a successful API call.
- **Expired access [REDACTED CREDENTIAL] The provider returns
401; the token manager refreshes, retries once, and succeeds. - **Revoked refresh [REDACTED CREDENTIAL] Refresh returns
invalid_grant; the agent stops and reports the problem instead of looping. -
Insufficient scope: The provider returns
403; the agent does not retry and identifies the missing scope. - Concurrent refresh: Two requests for the same user execute at once; exactly one refresh occurs.
Run these cases against mocks. With Apidog, define the token and protected endpoints, mock successful and error responses, and run the matrix without contacting a real provider.
See running agents against mocks instead of production and the OAuth 2 API testing guide for implementation details.
Three integrations and their authorization models
Calendar assistant
A calendar assistant reads availability and books meetings for one user.
Use:
- Delegated access
- Separate calendar read and write scopes
- Browser-based consent at connect time
- Refresh-token access during background runs
Test revocation carefully. If the user disconnects the integration, the nightly run must stop rather than retrying a dead grant for a week.
Support agent in a shared inbox
A support agent may act on tickets belonging to a team. The identity question is more nuanced here.
A shared team account can be defensible because the resource belongs to the team, but every reply then looks identical in the audit log. A better design is often:
- A bot identity with its own scopes
- A record of which human triggered the run
- Separate agent and human attribution
This preserves accountability without pretending the agent is a person.
Internal operations agent
An internal ops agent may restart services and read dashboards in your infrastructure. There is no user-owned data and no need for delegated consent.
Use a narrow service account and focus on rotation, permission boundaries, and blast-radius reduction.
The dividing line is ownership:
- If someone else owns the data and may reasonably revoke access, use delegated authorization.
- If your organization owns the data, use a service account and focus on least privilege.
Preserve human attribution
Delegated authorization answers “on whose behalf?” It does not answer “at whose request?”
Keep the requesting human’s identity alongside the work. In an assigned-work system, the work-management layer is the natural place: a Sharkly Task records the responsible person alongside the Agent or Crew executing it. The Sharkly documentation describes this separation.
Whatever system you use, record both identities. After an incident, the audit question is usually “who requested this?” A token alone cannot answer it.
Never let the model hold a credential
The model should never see an OAuth token.
Inject credentials at the executor’s HTTP layer, after the model selects a tool and supplies its arguments:
- Do not include a token parameter in the tool schema.
- Do not place credentials in prompts.
- Strip the
Authorizationheader before returning responses to the model. - Select the token from trusted run metadata, not from model-provided user IDs.
Anything in model context can be summarized, traced, echoed in an error, or returned to a user. These are normal system behaviors that become credential leaks when secrets enter the context.
The same rule applies to user identity. The executor should determine which user the run represents and select the corresponding token. Letting the model choose the user turns authorization into a model decision.
Implementation checklist
- Use delegated access when the data belongs to a user.
- Use service accounts only for resources your organization owns.
- Use authorization code with PKCE at connect time.
- Use the device grant for headless environments.
- Request minimal scopes per agent and escalate incrementally.
- Give sub-agents exchanged, narrowed tokens rather than copies of the user grant.
- Encrypt refresh tokens at rest.
- Keep tokens out of prompts, logs, and traces.
- Refresh through a token manager.
- Serialize refreshes per user and persist rotated refresh tokens.
- Treat
401and403authentication errors as terminal. - Surface expired grants to the user.
- Add approval gates for high-stakes operations.
- Test all five OAuth scenarios against mocks in CI.
Delegated authorization requires more work than a shared key, but it gives users control and gives operators an honest audit trail. Download Apidog to build and test the token flow, including expiry and revocation, before an agent runs unattended.
Frequently asked questions
Can the agent complete the OAuth consent flow itself?
No, and it should not try. Consent requires a person to decide what to grant. Have a human authorize once through a normal browser flow, then let the agent use the resulting grant.
Should each agent have its own OAuth client?
Usually, use separate clients per product integration and separate tokens per agent within that integration, often through token exchange. Distinct clients are useful when providers enforce per-client rate limits or when you need independent revocation.
What happens if a refresh token rotates and I miss the new one?
The user may be locked out and need to reconnect. Persist the new refresh token in the same transaction that consumes the old one, and serialize refreshes per user so workers cannot race.
Is it safe to let the model see an access token?
No. Tokens belong in the HTTP layer and should be injected by the executor. Anything the model sees can appear in a trace, summary, or response. See least-privilege API keys for agents.
How do I audit which agent did what?
Log the user ID, agent name, scope used, and token identifier for every call—never the token itself. See tracing agent tool calls.
What if the provider does not support token exchange?
Store separate grants per agent when the provider supports multiple grants. Otherwise, enforce scope narrowing in your own gateway so each agent’s calls are filtered to its allowed operations before leaving your network.
References
- API keys and OAuth
- Apidog
- Least-privilege API keys for agents
- OAuth 2.0 specification
- OAuth 2.0 Security Best Current Practice
- Authorization code grant
- RFC 8693
- Multi-agent handoff
- OAuth 2 scopes
- AI agent guardrails
- Tracing agent tool calls
- API error design for agents
- Running agents against mocks instead of production
- OAuth 2 API testing guide
- Sharkly
- Sharkly documentation
- Download Apidog


Top comments (0)