Agent infrastructure

Platform integration

OAuth for AI agents: a practical setup guide for delegated access

OAuth for AI agents: a 7-step practical guide to delegated access, PKCE, scopes, token refresh, and the per-user audit trail your security review will demand.

7 minute read
Decorative imagery showcasing Pontil's brand

By the end of this guide, you'll have a working OAuth flow that lets an AI agent act on behalf of an authenticated user — with the right scopes, refresh handling, and an audit trail that survives a security review. The pattern covers any agent that touches a user's SaaS account: internal copilots, customer-facing agents, or an MCP server fronting your own product.

Prerequisites: a SaaS product with an existing OAuth 2.0 authorisation server (or the ability to stand one up), a test client you control, and an agent runtime that can hold credentials securely. Working time: 90 minutes for the happy path, longer if you've never run the authorisation code flow before.

This guide uses OAuth 2.1 conventions — PKCE everywhere, no implicit flow, refresh token rotation on. If your authorisation server is older, the steps still apply; you'll just have a few more knobs to set.

Step 1 — Pick the right OAuth flow for agent on-behalf-of authentication

Agents are confused deputies by default. They run code on behalf of a user, but they aren't the user. The flow you pick decides whether your audit log can tell the difference.

For any agent acting on behalf of a human user, use the authorisation code flow with PKCE. The user authenticates with your authorisation server directly. The agent never sees the password. You get a refresh token scoped to that user, and every API call the agent makes carries that user's identity.

Do not use:

  • Client credentials flow. This authenticates the agent as a service account. You lose per-user permissions and per-user audit. Fine for system-to-system jobs, wrong for user-delegated work.
  • Resource owner password credentials. Deprecated. The agent handles the password. Don't.
  • Device code flow, unless the agent genuinely runs on a device with no browser (CLI agents, IoT). Most production agents run on a server and can redirect a browser.

If your agent does background work after the user has logged out, you still use authorisation code flow — you just lean on the refresh token. The agent is acting as the user, on a delegation the user granted earlier.

Step 2 — Register the agent as an OAuth client

Register the agent in your authorisation server as a confidential client. It has a client secret and runs on infrastructure you control.

You need:

  • A unique client_id per agent. Don't reuse one client across agents — you'll lose the ability to revoke or rate-limit them independently.
  • A redirect URI on infrastructure the agent owns. HTTPS only. Exact match, not wildcard.
  • A token endpoint authentication method. Prefer private_key_jwt over client_secret_post if your authorisation server supports it. Asymmetric keys are easier to rotate without downtime.

Example registration request (RFC 7591 dynamic client registration):

POST /register HTTP/1.1
Host: auth.example.com
Content-Type: application/json

{
 "client_name": "Reporting Agent (prod)",
 "redirect_uris": ["https://agent.example.com/oauth/callback"],
 "grant_types": ["authorization_code", "refresh_token"],
 "response_types": ["code"],
 "token_endpoint_auth_method": "private_key_jwt",
 "scope": "reports:read reports:write"
}

Expected response: a client_id, and (if you're using a secret) a client_secret. Store the secret in a managed secret store. Don't commit it.

Step 3 — Define scopes that match agent actions, not API surface

Scopes are the contract between the user and the agent. The user reads them on the consent screen and decides whether to grant access. Get this wrong and you'll either over-grant (security incident waiting to happen) or under-grant (agent fails halfway through a task).

Design scopes around what the agent does, not what your API exposes. reports:read is a scope. GET /v2/reports/{id} is not.

A workable scope structure:

Granularity
Example
When to use

Resource + action

`invoices:read`, `invoices:write`

Default. Most agents fit here.

Resource only

`invoices`

Only for trusted first-party agents.

Action across resources

`admin:write`

Avoid. Too coarse for user delegation.


Two rules:

  1. Write scopes are separate from read scopes. Always. An agent that summarises invoices should not be able to delete them.
  2. Name scopes the way they'll read on the consent screen. "Read your invoices" is clear. "invoice.v2.list" is not.

If you're building MCP server OAuth, the scope is what the user sees when they connect an agent client to your server. Treat that screen as a UI surface, not an afterthought.

Step 4 — Implement the authorisation code flow with PKCE

PKCE (RFC 7636) stops an attacker who intercepts the authorisation code from exchanging it for a token. Required in OAuth 2.1 — and note that OAuth 2.1 only permits S256 as the code_challenge_method; the legacy plain method is disallowed.

The agent generates a code_verifier (random 43–128 characters) and derives a code_challenge (SHA-256 of the verifier, base64url-encoded). It sends the challenge on the authorisation request, then the verifier on the token request.

Authorisation request (the agent redirects the user's browser here):

GET /authorize?
 response_type=code
 &client_id=agent_prod_42
 &redirect_uri=https://agent.example.com/oauth/callback
 &scope=reports:read+reports:write
 &state=<csrf_token>
 &code_challenge=<base64url(sha256(verifier))>
 &code_challenge_method=S256

The user authenticates, sees the consent screen, approves. The authorisation server redirects back to the agent with ?code=...&state=....

Token request (agent server-to-server):

POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<authorization_code>
&redirect_uri=https://agent.example.com/oauth/callback
&client_id=agent_prod_42
&code_verifier=<original_verifier>

Expected response:

{
 "access_token": "eyJ...",
 "token_type": "Bearer",
 "expires_in": 3600,
 "refresh_token": "def...",
 "scope": "reports:read reports:write"
}

Validate state matches what you sent. Validate the returned scope matches what the user actually granted (they may have unticked some). Store the tokens against the user's identity in your system, not against the agent.

Step 5 — Handle token refresh and rotation

Access tokens are short-lived (an hour is typical). Refresh tokens are long-lived but should rotate on every use.

The pattern:

  1. Before each API call, check if the access token has more than 60 seconds left. If not, refresh.
  2. Send the refresh token to the token endpoint with grant_type=refresh_token.
  3. Store the new access token and the new refresh token. The old refresh token is now invalid.
  4. If the refresh fails with invalid_grant, the user has revoked access (or someone replayed the refresh token). Mark the delegation as broken and force re-auth.

Refresh token rotation matters because agents run unattended. If a refresh token leaks and gets used by an attacker, rotation means your legitimate next call fails — and you find out. Without rotation, both you and the attacker keep working in parallel.

Never cache tokens in memory across deploys. Persist them encrypted, keyed by user ID, with a managed KMS handling the encryption key.

Step 6 — Carry user identity through to every downstream call

The whole point of delegated agent access is that the audit trail reflects the real user. An agent that calls your API with a shared service account erases the user from your logs.

Three things to get right:

  • Every outbound API call uses the user's access token. Not the agent's. Not a service account. The user's.
  • Your API enforces the user's permissions server-side. Don't trust the agent to filter results. The token tells your API who the user is; the API decides what they can see.
  • Your audit log records the user, the agent (via client_id), and the action. All three. "User X did Y" is incomplete. "User X, via agent Z, did Y" is the record you'll want during an incident.

This is the same model that makes a well-built tools layer auditable end-to-end — see orchestrator vs tools layer for where this responsibility actually sits in the stack.

Step 7 — Wire up revocation and consent management

Users must be able to see what they've granted and revoke it. This is table stakes for any production agent, and it's required under GDPR and most enterprise procurement reviews.

Build:

  • A consent dashboard in your product where users see every agent they've authorised, the scopes granted, the last-used timestamp, and a revoke button.
  • A revocation endpoint (RFC 7009) the agent can call when a user deletes their account in the agent's UI.
  • A token introspection endpoint (RFC 7662) for downstream services that need to verify a token is still valid.

When a user revokes, immediately invalidate both the access token and the refresh token. Propagate the revocation to any cached sessions on the agent side within seconds, not minutes.

Common pitfalls

  • Storing the refresh token without encryption. A leaked database backup becomes a credential dump. Encrypt at rest with a managed KMS key.
  • Reusing one client registration across environments. Dev tokens working in prod is a sign you've conflated environments. Separate client_id per environment, always.
  • Treating consent as a one-time event. Users add and remove scopes over time. Re-prompt when the agent needs a scope it doesn't have, rather than failing silently or over-requesting upfront.
  • Skipping state validation. This is the CSRF protection on the authorisation response. Free to add. Painful to add after an incident.
  • Logging tokens. Don't. Not in application logs, not in error reports, not in traces. Redact at the framework layer so a junior engineer can't accidentally land it in Datadog.
  • Service-account fallback when refresh fails. Tempting ("just keep the agent working"). Wrong. If the user's delegation is broken, the agent stops acting as that user. Anything else is a security finding.

If you're applying this pattern across a portfolio of products — multiple agents, multiple authorisation servers, the same users — the connector maintenance cost of doing it by hand compounds fast. That's where the work shifts from writing OAuth flows to running them.

Join our weekly newsletter

Stay up to date on the ever changing agentic landscape.

POSTS

Related content

Agent infrastructure

Platform integration

Orchestrator vs tools layer: where agent work actually happens

7 min read

Agent infrastructure

Platform integration

MCP servers: a practical setup and architecture guide

8 minute read

Agents in production

Platform integration

AI applications in enterprise SaaS: why most can't reach their own products

10 min read