Agent infrastructure
Platform integration
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.

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.
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:
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.
Register the agent in your authorisation server as a confidential client. It has a client secret and runs on infrastructure you control.
You need:
client_id per agent. Don't reuse one client across agents — you'll lose the ability to revoke or rate-limit them independently.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.
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:
Two rules:
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.
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.
Access tokens are short-lived (an hour is typical). Refresh tokens are long-lived but should rotate on every use.
The pattern:
grant_type=refresh_token.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.
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:
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.
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:
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.
client_id per environment, always.state validation. This is the CSRF protection on the authorisation response. Free to add. Painful to add after an incident.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.
Stay up to date on the ever changing agentic landscape.