OAuth for AI agents: handling tokens without leaking them
An AI agent that works with a person's email, calendar or code needs access to their accounts, and OAuth is the standard way to grant it without sharing passwords. The risk is in the details: a token pasted into a prompt, stored in plain text or requested with far too much power can undo the whole benefit. This post covers how to use OAuth with agents safely: the flow, scopes, refresh tokens, where tokens live, per-user connections, revocation and what to do when access expires.
Key takeaways
- Use the authorization code flow with PKCE and an exact redirect URI; never hand an agent a user's password.
- Request the narrowest scopes and keep a separate read-only option.
- Store tokens encrypted, outside the model's context, and let a server-side component attach them to calls.
- Connect per user, so one person's tokens never serve another's agent.
- Plan for revocation and expired grants with a clear reconnect flow.
What OAuth gives an agent, and what it should never get#
OAuth 2.0 lets a person authorise an application to act on their behalf at another service, with limited permissions, without revealing their password. The application receives tokens: a short-lived access token it presents on each API call, and often a longer-lived refresh token it uses to get new access tokens.
For an agent, the important design point is who holds those tokens. The model should not. A language model is a text predictor that can be steered by whatever text reaches it, including content from emails and web pages. Anything in its context can potentially be repeated, summarised into a log or sent to a place you did not intend.
So the working rule is simple: the model asks for actions, and a trusted component outside the model attaches credentials and performs the call. The model gets results, never secrets. Every other decision in this post follows from that split.
Use the authorization code flow with PKCE#
For user-delegated access, the authorization code flow is the right choice. The user is sent to the provider, signs in and consents, and your redirect URI receives a short-lived code that you exchange for tokens on the back end.
Add PKCE (Proof Key for Code Exchange). Before redirecting, generate a random code_verifier, send its SHA-256 hash as the code_challenge with method S256, and present the original verifier when exchanging the code. If someone intercepts the code, it is useless without the verifier. PKCE was designed for public clients, but it is cheap protection for server-side apps too.
- Always send and verify a random
statevalue to defeat cross-site request forgery. - Register an exact redirect URI and compare it exactly; do not accept wildcards.
- Do the code exchange on the server, so the client secret and tokens never touch the browser or the agent.
- Do not use flows that collect the user's password directly.
Request the narrowest scopes that do the job#
Scopes define what a token can do. Agents tempt developers to request everything, because it is hard to predict which calls the model will make. Resist that. A token that can read a calendar cannot delete a repository, and a stolen or misused token is only as dangerous as its scopes.
Practical habits:
- Map each tool to the minimum scope it needs, and request only the union of the tools you actually enable.
- Offer a read-only connection. Many agent tasks (searching, summarising, reporting) never need write access.
- Ask for extra scopes later, when a feature needs them, rather than all at the start. Many providers support this incremental consent.
- Explain in plain language what you are asking for and why, before the provider's consent screen appears.
Scopes are your outer boundary; approvals and per-app rules are the inner one. Use both. Even with write scope granted, you can still require a human to confirm each risky action.
Refresh tokens, expiry and rotation#
Access tokens are short-lived on purpose. When one expires, the harness uses the refresh token to get a new one, silently, so the agent never notices. Handle this in one place and protect it with a lock, so two parallel tool calls do not both try to refresh and invalidate each other.
Many providers rotate refresh tokens: each refresh returns a new refresh token and the old one stops working. If so, you must save the new token before using it, atomically, or a crash mid-refresh can leave you with a dead credential. Some providers detect reuse of an old refresh token and revoke the whole grant, which is another reason to serialise refreshes.
async function getAccessToken(conn) {
if (conn.expiresAt - Date.now() > 60_000) return conn.accessToken;
return withLock(conn.id, async () => {
const fresh = await refresh(conn.refreshToken); // provider call
await saveTokens(conn.id, fresh); // store before use
return fresh.accessToken;
});
}Refresh a little before expiry, not after, and treat a refresh failure as a signal about the connection, not a transient blip to retry forever.
Where tokens live: encrypted, server-side, never in context#
Store tokens encrypted at rest, with keys held separately from the database. Envelope encryption is a good pattern: each token is encrypted with its own data key, and those data keys are encrypted by a master key you can rotate. A leaked database dump then contains only ciphertext.
Equally important is what you keep out of places tokens leak:
- The prompt and tool results. Never include a token in text the model sees, and never ask the model to pass one as an argument.
- Logs. Scrub authorization headers and URLs with secrets from request logs and traces.
- Error messages. Provider errors sometimes echo request details; sanitise before returning them.
- Client-side code. The browser has no reason to see a provider token.
The tool executor should be the only code that decrypts a token, only at the moment of the call, and only for the connection tied to the current user. Everything else works with opaque connection IDs.
Per-user connections and URLs that act as secrets#
Connect each person's accounts separately, and tie every agent session to exactly one user. A shared service account that serves everyone makes any mistake a cross-user data leak. With per-user connections, one person's agent can only ever reach that person's data, and revoking one connection does not disturb anyone else.
If a person can have several accounts for one app, such as personal and work, store them as distinct connections with clear labels, and make the choice explicit at call time.
Remember that an agent endpoint is itself a credential. If the address your agent connects to is private to one user and grants access to their apps, then anyone who learns it can act as that user. Treat such a URL like a password: do not paste it in public chats or screenshots, do not write it into request logs, and make it easy to rotate if it leaks.
Revocation and reauthorisation flows#
Plan for the end of a grant from day one. It will end in several ways: the person disconnects an app, the provider expires an unused grant, a password change invalidates sessions, or an admin revokes access. Your refresh call then fails, usually with an invalid_grant style error.
Handle that deliberately:
- Mark the connection as needing reconnection, and stop retrying.
- Return a clear, non-secret error to the agent, such as "This account needs to be reconnected by the user", so the model can tell the person instead of looping.
- Notify the user through the dashboard or by email, with a one-click link to reconnect.
- After reconnection, resume cleanly. Do not replay old pending writes without checking they still make sense.
Also give people a visible way to disconnect. When they do, call the provider's revocation endpoint if one exists and delete the stored tokens. Disconnecting should be real, immediate and boring.
How Setu approaches this#
Setu runs the OAuth part for you. It is managed OAuth, so developers and users do not register OAuth apps with each provider. Tokens are encrypted at rest using AES-256-GCM envelope encryption and kept in a vault, away from the model. Each person gets a private MCP URL, and the agent talks to that rather than holding provider credentials.
People can connect several accounts per app, choose read-only connections, and set rules for what needs approval. Supported apps include GitHub, Slack, Notion, Linear, Gmail, Google Drive and more; see the integrations list. For the details of storage and access, read the security documentation.
Frequently asked questions#
How should an AI agent authenticate to Gmail or GitHub?
Use OAuth with the authorization code flow and PKCE so the user grants limited access without sharing a password. Keep the resulting tokens server-side and let a trusted executor attach them to calls.
Should I put API tokens in the prompt for an AI agent?
No. Anything in the model's context can be repeated, logged or exfiltrated through prompt injection. Keep tokens in an encrypted store and have a server-side component add them to requests outside the model.
Do AI agents need refresh tokens?
Usually yes, if the agent must keep working after the short-lived access token expires. Store refresh tokens encrypted, serialise refreshes, and save rotated tokens before using them.
What happens when an OAuth token is revoked while an agent is running?
Calls and refreshes will fail. The harness should mark the connection as needing reconnection, return a clear error to the agent, notify the user and stop retrying until they reconnect.
Skip building OAuth plumbing for every app
Setu handles managed OAuth, the encrypted token vault and a private MCP URL per user, so your agent can use people's apps without ever seeing a token. Begin with the quickstart.
Keep reading.
-
Security and control
Human approval for AI agents: design it so people use it
How to design human approval for AI agents: risk tiers, previews of the exact action, expiry, safe retries, fewer alerts, an audit trail and undo.
-
Workflows
Agentic workflows vs autonomous agents: which should you build?
Agentic workflows vs autonomous agents: compare determinism, flexibility, cost and failure handling, and learn when a fixed chain with LLM steps wins.
-
Harness engineering
How to evaluate AI agents that use tools (without fooling yourself)
A practical guide to evaluating tool-using AI agents: task suites, mocked vs real tools, fixtures, outcome and trajectory checks, flaky runs and release gates.