Your agent needs to read a customer’s calendar, send a message from their account, or file a ticket under their name. The quick version is to hold one service account with broad access and act through it. Every action shows up as “the integration”, nobody can tell which user triggered what, and one compromised credential exposes every account you touch.
The correct version is delegated authorization: the user grants your agent a scoped, revocable token, the agent acts as that user, and the audit trail names them. This is what OAuth 2.0 was built for. What makes it awkward for agents is that OAuth assumes a browser and a person present to click “Allow”, and agents run in the background at 3 AM.
This guide covers which OAuth flow fits an agent, how to scope and store tokens, what to do about refresh and revocation, and how to test the whole path without a live account. If you are still choosing between key-based and delegated auth, our comparison of API keys and OAuth is the place to start.
Apidog helps with the part teams underestimate: exercising every branch of the flow, including expiry and revocation, before an agent meets them in production.
Service account or delegated access
Pick deliberately, because the two models fail differently.
A service account is your agent’s own identity, with its own permissions. It suits work the agent does on your behalf: reading your own database, calling your own internal services, running scheduled jobs against your infrastructure. Scope it tightly, as in our post on least-privilege API keys for agents, and rotate it.
Delegated access is the agent acting as a specific user, with that user’s permissions and no more. It is required whenever the data belongs to someone else. Three properties make it worth the extra work: the user can see what was granted, the user can revoke it, and every action carries their identity in the log.
The failure mode to avoid is a service account with organization-wide access used to act “as” users. It works, and it means a single leaked credential exposes everyone, with no per-user revocation and no honest audit trail.
Which flow fits an agent
OAuth 2.0 defines several grant types, and only a few make sense here. The OAuth 2.0 specification has the full set; these are the ones you will use.
Authorization code with PKCE. The standard flow for acting as a user. The user is redirected to the provider, approves the scopes, and your service exchanges the code for tokens. PKCE protects the exchange and is now the default recommendation for every client type, per the OAuth 2.0 Security Best Current Practice. Our walkthrough of the authorization code grant covers the mechanics step by step.
The agent-specific point: this flow runs once, with the human present, at connect time. The agent never runs it. It uses the refresh token that the flow produced. Separate those two moments in your design and most of the awkwardness disappears.
Client credentials. Machine-to-machine, no user involved. Correct for service accounts and wrong for acting as a user, because there is no user to consent.
Device authorization grant. For agents on machines with no browser. The user gets a code and approves on their phone. Useful for CLI agents and headless environments.
Token exchange. RFC 8693 lets a service swap a token for a narrower one. This is how you give a sub-agent a token limited to one scope for one task, derived from the user’s broader grant, without handing it the original. If you run multi-agent systems, this is the mechanism that makes per-agent credentials practical, and it fits the boundary rules in our post on multi-agent handoff.
Scope narrowly, and per agent
Scopes are where delegated access earns its keep, and where most implementations get lazy by requesting everything the app might ever need.
Request only what this agent does. A scheduling agent needs calendar write and nothing else. Not mail, not contacts, not files. Users read the consent screen, and a long list is both a trust problem and a blast-radius problem. Our explainer on OAuth 2 scopes covers how providers model them.
Ask incrementally. Request the minimum at connect time, then request more when the user asks for a feature that needs it. Consent tied to a concrete request is easier to grant and easier to justify.
Give each agent its own token. If a research agent and a billing agent both act for the same user, derive two tokens with different scopes rather than sharing one. Then a compromised research agent cannot issue refunds, and the log tells you which agent acted.
Prefer read scopes by default and require an explicit escalation for writes. Combine this with an approval gate on destructive calls, as in our post on AI agent guardrails, so a token that can write is not the only thing standing between the agent and a mistake.
Store, refresh, and revoke
Tokens are credentials, so treat them as credentials.
Storage. Encrypt refresh tokens at rest, keyed per user. Never write them to logs, never put them in prompts, and never let a model see one. A token in context is a token in your trace store, your provider’s logs, and possibly a summary. Our post on tracing agent tool calls covers redacting at the boundary rather than at read time.
Refresh. Access tokens are short-lived by design. The agent should never manage this itself; a token manager in front of the HTTP client refreshes when expiry is near and retries the call once on a 401.
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 details matter. Providers increasingly rotate refresh tokens, issuing a new one on every refresh and invalidating the old, so persist the new one immediately or you will lock the user out. And serialize refreshes per user, since two concurrent refreshes with a rotating provider will race and one will lose.
Revocation. Users revoke access, tokens expire, admins remove accounts. The agent must handle 401 and 403 as terminal rather than retryable. Retrying an auth failure never helps and can trip abuse protections. Return a clear message naming the user and the scope so a human can act, following the error patterns in our post on API error design for agents.
The consent problem
The awkward part of agents plus OAuth: consent needs a human, and agents run unattended.
Separate connect time from run time and it becomes manageable. At connect time, a person authorizes once, with a browser, and you store a refresh token. At run time, the agent uses that grant with no human involved. This works for scheduled and background agents, which is most of them.
Two limits to plan for. Grants expire, sometimes after months of no use, sometimes by policy. Detect an expired grant, stop the run, and notify the user, rather than failing silently every night. And consent has a scope ceiling: an agent that needs a scope the user never granted must ask rather than escalate on its own.
For anything high-stakes, add a second gate at action time. The token proves the agent may act; an approval gate decides whether it should. Those are different questions and both deserve an answer.
Test the flow before an agent meets it
Auth code paths are the least-tested part of most integrations, because exercising them by hand means clicking through a provider’s screens.
Build these five cases:
- Happy path. A valid access token, a successful call. The baseline.
- Expired access token. The provider returns
401, the manager refreshes, the call retries once and succeeds. This is the most common real path and often the least tested. - Revoked refresh token. The refresh returns
invalid_grant. The agent must stop and report, not loop. - Insufficient scope. A
403with a scope error. The agent must not retry, and must say which scope is missing. - Concurrent refresh. Two calls for the same user at once. Exactly one refresh should occur.
Run them against mocks. In Apidog you can define the token endpoint and the protected endpoints, then mock each response including the error bodies, so the whole matrix runs without touching a real provider. Our post on running agents against mocks instead of production covers the wider habit, and our OAuth 2 API testing guide covers the request-level detail.

Three integrations and what they need
A calendar assistant. Reads availability and books meetings for one user. Delegated access, two scopes, connect-time consent in a browser, background runs afterward. The interesting failure is revocation: the user disconnects the integration and the nightly run must notice and stop rather than retrying a dead grant for a week.
A support agent inside a shared inbox. Acts on tickets belonging to a team. Here the identity question gets sharper. Acting as the team’s shared account is defensible, since the resource genuinely belongs to the team, but every reply then looks identical in the audit log. Better is a bot identity with its own scopes plus a record of which human triggered the run, which keeps attribution intact without pretending the agent is a person.
An internal ops agent. Restarts services and reads dashboards in your own infrastructure. No user data, no delegation. A service account with narrow scopes is the right answer, and the work goes into rotation and blast radius rather than into consent.
The dividing line is ownership. If the data belongs to someone who could reasonably want to revoke your access, use delegated auth. If it belongs to you, use a service account and spend the effort on scoping.
Keep the human in the attribution
Delegated auth answers “on whose behalf.” It does not answer “at whose request,” and for agent work you want both. A token proves the agent may act as a user; it does not record which person asked for the run.
Keep that second identity next to the work. Where agents execute assigned tasks, the work-management layer is the natural place: a Sharkly Task records the person responsible for the work alongside the Agent or Crew assigned to execute it, which keeps human accountability and agent execution as two separate, visible facts. The Sharkly documentation describes that split in detail. However you store it, the audit question after an incident is usually “who asked for this,” and a token alone cannot answer it.

Do not let the model hold the credential
One architectural rule prevents most auth incidents in agent systems: the model never sees a token.
Tokens are injected by the executor at the HTTP layer, after the model has chosen a tool and produced arguments. The tool schema has no token parameter, the prompt contains no credential, and the response the model reads has the Authorization header stripped.
This matters more for agents than for ordinary clients because of where model input travels. Anything in context can be summarized into a handoff, written to a trace, echoed in an error message, or returned to a user who asked the agent to explain itself. None of those paths are hostile; they are all normal features that become leaks the moment a credential is in scope.
The same rule applies to the user identity. The executor knows which user this run acts for, and it selects the token from that. Letting the model name the user is an authorization decision made by the least predictable component in the system.
A checklist
- Delegated access wherever the data belongs to a user, service accounts only for your own resources.
- Authorization code with PKCE at connect time, device grant for headless machines.
- Scopes requested per agent, minimal, escalated incrementally.
- Sub-agents get exchanged tokens, not copies of the user’s grant.
- Refresh tokens encrypted at rest and never in prompts, logs, or traces.
- Refresh handled by a token manager, serialized per user, rotation persisted.
401and403treated as terminal, with a message naming the user and scope.- Expired grants detected and surfaced to the user, not retried nightly.
- High-stakes actions gated by approval on top of the token.
- All five auth scenarios tested against mocks in CI.
Delegated auth is more work than a shared key, and it buys you the two things you need when an agent acts for other people: the user can take it back, and the log says who did what. Download Apidog to build the token flow and its failure cases before an agent runs it unattended.
Frequently asked questions
Can the agent complete the OAuth consent flow itself? No, and it should not try. Consent requires a person deciding 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? Separate clients per product integration, and separate tokens per agent within it, usually via token exchange. Distinct clients help when providers apply per-client rate limits or when you want independent revocation.
What happens if the refresh token rotates and I miss the new one? The user is locked out and has to reconnect. Persist the new refresh token in the same transaction that consumes the old one, and serialize refreshes per user so two workers cannot race.
Is it safe to let the model see an access token? No. Tokens belong in the HTTP layer, injected by your executor. Anything a model sees can end up in a trace, a summary, or a response, as covered in our post on least-privilege API keys for agents.
How do I audit which agent did what? Log the user ID, the agent name, the scope used, and the token identifier on every call, never the token itself. Our post on tracing agent tool calls covers the record shape.
What if the provider does not support token exchange? Store separate grants per agent where the provider allows multiple, or enforce scope narrowing in your own gateway so each agent’s calls are filtered to its allowed operations before they leave your network.



