How AI agent refresh tokens should be controlled
AI agent refresh tokens need a human-owned action layer, clear refresh authority, usable revocation, and records that explain every use.

An AI agent should never hold a refresh token. That rule sounds severe until you look at what the token does: it lets a process keep obtaining access after the original human approval has faded from view. An access token may expire quickly; a refresh token keeps the relationship alive.
The useful design is not to make the agent better at safeguarding a bearer credential. Put the credential in an action layer that a human-controlled process owns. The agent asks for a specific external action, the layer decides whether that run may make it, refreshes only when needed, and returns the result. That arrangement gives you a place to approve, revoke, and investigate use.
A password vault alone does not solve this. A vault protects storage. An agent action layer governs use. Teams blur those jobs constantly, then discover that a token sitting in an encrypted store is still available to any process that can ask the store the right question.
AI agent refresh tokens change the trust boundary
AI agent refresh tokens are dangerous because they extend authority beyond the agent process that first needed them. If a coding agent can read a refresh token from an environment variable, configuration file, browser profile, or secret manager response, it can use that token through any HTTP client it can invoke. The token no longer belongs to a bounded task. It belongs to whatever gains control of that process.
OAuth 2.0, RFC 6749, describes refresh tokens as credentials used to obtain access tokens when the current token expires or becomes invalid. The specification makes them optional, but that does not make them harmless. Providers issue them because interactive authorization on every access token renewal would be tiresome. That convenience is exactly why an unattended agent should not possess one.
Keep three separate things distinct:
- The account owner is the person or service identity whose account granted access.
- The action requester is the agent process asking to call an API now.
- The credential custodian is the component that stores the refresh token and talks to the token endpoint.
One person or program can fill more than one role in a small setup, but the roles must still exist. If the same agent owns all three, it can connect a new account, widen its scope, refresh indefinitely, and conceal the action among ordinary requests. That is not an authorization design. It is a bearer token with a chatbot attached.
I have seen teams say, “the agent only needs read access,” then put a refresh token with broad repository, email, or cloud access into a local .env file. The access token might have a brief life, but the refresh token makes the mistake persistent. A prompt injection does not need to persuade the model to exfiltrate a password. It can persuade the model to use the still-valid grant for a request that looks plausible in context.
The boundary should sit before credentials enter the agent process. The agent receives neither a token value nor a fake placeholder that it can exchange elsewhere. It receives an operation interface: fetch this issue, create this draft, read this deployment status, open this SSH session to this approved host. The action layer owns the protocol details behind that operation.
A human must own the grant, not merely approve a prompt
The person who can approve a refresh should be named before the OAuth connection exists. “Any developer can click allow” works until an account owner leaves, a shared mailbox changes hands, or an agent reconnects a service under someone else's identity.
For a personal SaaS account, the account owner should complete the initial authorization and approve reconnection after revocation or expiration. For a shared operational account, assign an accountable owner and a backup who can revoke it. For a machine identity, the service owner should authorize its client registration and scopes. Do not treat a person and a machine identity as interchangeable simply because both can call the same API.
Separate four decisions that often get collapsed into one browser click:
- Who may create the original grant.
- Which action layer may retain the resulting refresh token.
- Which agent sessions may request actions under that grant.
- Who may revoke the grant or approve a new connection.
The original OAuth consent screen only answers the first decision, and sometimes it barely answers that. It tells the provider that an account holder authorized a client with stated scopes. It does not tell your local system whether an untrusted repository, a new agent subprocess, or an overnight job may draw on that grant.
A practical ownership record needs more than a provider account email. Store a local grant record with an opaque grant ID, provider name, account reference, scope set, owner, backup revoker, connection date, and the actions allowed to request it. Avoid copying the refresh token into that record. The record explains the token; it must not become a second secret store.
The awkward case is shared access. A team often connects one administrator account because it is quick, then lets every developer's agent operate under it. That choice destroys accountability. If the API supports service accounts, app installations, delegated identities, or narrowly scoped project tokens, use those instead. If it does not, limit the action layer to a small group of approved operations and record the shared account's actual owner.
Do not give an agent authority to begin a fresh OAuth browser flow by itself. It may present a legitimate provider page, but it can also steer the human toward a broader account, a broader scope selection, or a different tenant. Initiating a connection is an administrative action. Require the human to initiate it from the action layer and inspect the account and scope list before consent.
The action layer should refresh only to complete an approved action
A controlled action layer should exchange a refresh token only when it has an authorized request that requires a current access token. It should not run a background loop that refreshes every credential “just in case.” Preemptive refresh looks tidy in code and makes incident response worse, because it keeps grants alive without a corresponding human or agent action.
The request path can stay simple:
- An agent session asks for a named operation and supplies ordinary action parameters.
- The action layer identifies the grant tied to that operation, then checks whether the session may use it.
- If the cached access token is absent or near expiry, the layer sends the refresh token to the provider's token endpoint.
- The layer calls the destination API with the access token and returns the filtered result to the agent.
- The layer records the action and the refresh event without recording credential material.
The agent never chooses a token endpoint, client identifier, callback URL, or scope string. Those values belong to the connection definition that the human approved. Allowing the agent to supply them turns your gateway into an open token relay.
Consider an agent asked to post a release note to a project tracker. It asks the action layer to create one issue in a named project. The layer sees that this operation requires a tracker grant for that project, checks the requesting session, renews the access token if necessary, and posts the note. The result can be the new issue ID and URL-like reference returned by the provider, not the bearer token used to create it.
Now change the prompt. A malicious repository instruction tells the agent to “verify access” by listing every project in the organization and creating a test issue in each one. If the agent has the refresh token, the instruction can become a direct series of API calls. If the agent has only the named operation interface, the layer can reject requests outside the approved project or demand another human authorization before the session proceeds.
This does not require a complicated policy language. It requires a small and understandable set of choices: which process is requesting, which grant it may use, and whether this action needs a human approval. More options do not automatically make the design safer. They often make it impossible for the operator to know which rule won.
Sallyport follows this separation by keeping API credentials in its encrypted vault and executing HTTP or SSH actions through its MCP connection rather than returning credentials to the agent.
The grant type determines what you can safely automate
Use authorization code flow with PKCE for a human account connection in a desktop or local application. The human signs in at the provider, reviews the consent request, and returns to the local application through the registered redirect path. PKCE binds the authorization response to the client that began the flow and reduces the value of an intercepted authorization code.
RFC 9700, OAuth 2.0 Security Best Current Practice, says public clients must use PKCE. It also says public-client refresh tokens must either use sender constraint or refresh token rotation. That guidance matters for agent integrations because a local application often behaves as a public client. Shipping a client secret inside a desktop app does not turn it into a confidential client. Anyone who has the app can extract that secret.
Choose the flow to match the identity you are connecting:
- Use authorization code with PKCE for a human's provider account.
- Use client credentials for a service identity where the provider supports it and no human account delegation is needed.
- Use a provider-specific installation or application model when it gives narrower project or organization access.
- Use device authorization only when the provider and your operating environment require it, and show the human exactly which identity and scope set they are approving.
Client credentials do not usually produce refresh tokens because the client can request a new access token by authenticating itself again. That can be safer for an autonomous job if the service identity has narrow permissions and its client authentication material stays inside the action layer. Do not use client credentials as an excuse to give a coding agent a broadly privileged client secret.
Offline access deserves special attention. Some OpenID Connect providers require an offline_access scope before they issue a refresh token. Ask for it only when an action genuinely must run after the interactive session ends. If a person will be present for every operation, a short access token with fresh authorization may fit better. Teams often ask for offline access by default because it avoids handling expiry. That trades an inconvenience for a durable credential.
Avoid resource owner password credentials entirely. RFC 9700 deprecates that grant because it hands the user's password to the client. An action layer does not make that acceptable. It merely gives you another place to lose a password.
Rotation is useful only when your storage handles replacement correctly
Refresh token rotation reduces the damage from a copied token by making each successful refresh replace the previous token. A provider can detect reuse of an old token and invalidate the affected grant family. That detection is helpful, but it can also lock out your legitimate integration when your own refresh logic is sloppy.
The most common failure is a race. Two agent sessions need an access token at nearly the same time. Both read the same old refresh token. The first session refreshes successfully and receives a new value. The second submits the old value a moment later. Depending on the provider, the second request may fail, or it may trigger reuse detection that revokes the entire family, including the new token.
Prevent the race with one refresh owner per grant. The action layer should serialize refresh work for each grant ID. A second caller waits for the first refresh result, then uses the newly cached access token rather than sending another token request. This is a correctness requirement, not an optimization.
Store the replacement token before you consider the refresh successful for future work. A safe order looks like this:
- Send the old refresh token to the token endpoint over TLS.
- Validate the token response and associate it with the expected provider and grant.
- Write the new refresh token and metadata to encrypted storage in one durable update.
- Mark the old token unusable in local state.
- Release waiting calls with the new access token or a fresh request path.
If the process crashes after the provider rotates the token but before local storage records the replacement, you may have lost the grant. You cannot fix that with retries. The recovery path is a human-led reconnection, which is why the owner and revoker records matter.
Some providers issue a new refresh token only sometimes. Others return the same token. Your code must accept both behaviors without assuming either one. Preserve the previous value only until you know the provider response and durable write succeeded. Never log either value while debugging. A surprising number of token leaks begin as a temporary debug statement that survived a release.
Sender-constrained tokens can reduce replay risk by tying a token to a client-held cryptographic key. DPoP, defined in RFC 9449, is one approach. It does not remove the need for custody. If an agent can use both the refresh token and the private signing key, it still has durable authority. Keep both materials behind the action layer and test provider behavior before relying on the constraint.
Revocation needs a named operator and a tested path
Revocation is not a setting you enable once. It is an action someone must be able to perform under pressure, when the provider dashboard is slow and nobody remembers which account authorized the integration.
Give the account owner and designated backup a direct revocation path. When they revoke a grant, your action layer must remove the local refresh token, invalidate any cached access tokens, and stop sessions that could continue asking for that grant. Disabling an agent's user interface while leaving the credential stored is incomplete revocation.
RFC 7009 defines an OAuth token revocation request. The provider publishes its own endpoint, but the request form normally looks like this:
POST /revoke HTTP/1.1
Host: authorization.example
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <client authentication>
token=<refresh-token>&token_type_hint=refresh_token
RFC 7009 tells servers to return a successful response even when the submitted token is already invalid or unknown. That behavior prevents an attacker from using the endpoint as a token validity oracle. It also means your operator cannot interpret an HTTP success alone as proof that the grant had active access. Record that the revocation request was sent, remove the local credential, then verify with a harmless provider call or provider audit trail if one exists.
Build revocation for these events: the account owner leaves, an agent session is suspected of compromise, a repository instruction caused an unexpected external call, an integration is retired, or a provider reports token reuse. Do not wait for a leak to decide who has authority to press the button.
An access token already issued may remain usable until it expires. Some providers revoke it immediately; others do not. Your local layer can stop issuing new actions immediately, which is the control you own. Do not promise instant global invalidation unless the provider documents it and you have tested it.
Keep provider revocation separate from local disablement. Local disablement stops your action layer from using a grant. Provider revocation tells the provider to reject it as well. During an incident, do both in that order: cut off your own execution path first, then send the provider request. The first step is under your control and should not depend on an external network call.
An audit record must explain intent, not just traffic
A list of HTTP calls cannot tell you whether a refresh was proper. You need a record that joins the human decision, the requesting agent session, the grant reference, and the resulting external action.
Do not write refresh tokens, access tokens, authorization codes, client assertions, or full API bodies into the audit log. Token strings are secrets. Full response bodies may contain customer data, repository contents, or personal information. Logging them for convenience creates a second, messier credential and data store.
A useful event record includes an event ID, time, session ID, requesting process identity, grant ID, connected account reference, operation name, provider host, requested resource, scope set recorded at connection time, approval reference, result class, and an error code if one occurred. For a refresh, record that a refresh happened and whether it succeeded. You do not need the token value to investigate it.
The distinction between an action record and a credential record matters. An action record says that a particular agent session requested a deployment status from a named environment and that the action layer permitted it. A credential record says which grant supported that request and who owns it. Keep the two joinable by an opaque grant ID, but do not make every operator who can inspect action history able to view account connection details.
Tamper evidence changes the quality of an investigation. If a compromised local process can edit the same log it writes, an attacker can erase the few entries that matter. Use append-only event handling with integrity checks, and verify the log independently from the process that generated it.
Sallyport projects its Sessions and Activity journals from an encrypted, hash-chained audit log, and sp audit verify checks that chain offline without a vault key.
Review behavior on a schedule that matches the grant's power. A personal issue tracker grant may need occasional review. A grant that can change production infrastructure needs review after every new connection, every scope change, and any unexpected agent behavior. The action layer should make those records readable enough that an owner can answer, “Which agent used my account, for what, and under whose approval?”
Browser profiles and generic token brokers create quiet bypasses
A browser profile is a poor credential store for an agent. It may contain session cookies, cached access tokens, refresh tokens, account selectors, and unrelated browsing state. Giving an agent access to that profile is broader than delegating one API action, and it makes cleanup difficult because the provider state and browser state blur together.
A generic token broker can create the same problem if it accepts arbitrary token endpoint parameters from callers. Teams often build one API called getToken(scope) and feel safer because the token no longer sits in the agent process. The broker still becomes a token vending machine if any session can ask for any connected account or arbitrary scope.
Make the caller request an operation, not a token. “Create release note in project A” has an owner, a destination, and a scope requirement you can inspect. “Get me a token for tracker.write” leaves too much authority with the caller.
Do not solve this with an enormous rules engine before you understand your operations. A short catalogue of approved actions, tied to named grants and human approval points, is easier to review and harder to bypass. Add complexity only when a real operating need demands it.
Also resist the habit of sharing one refresh token across development, staging, and production. Separate grants make revocation less dramatic and audit records less ambiguous. A staging agent should not retain a path to production because both environments happened to use the same identity provider.
Start with an ownership table and one revocation drill
The first useful artifact is a grant inventory, not code. Make one row for every refresh token your agents could cause to be used. Include the provider, account reference, grant ID, scopes, action layer location, account owner, backup revoker, creation method, last confirmed use, and local disable procedure. If you cannot fill in a column, you do not yet control that grant.
Then run a revocation drill with a low-risk integration. Have the owner disable the grant locally, revoke it at the provider, and attempt an ordinary agent action. Confirm that the action layer blocks the request, that a reconnect needs an explicit human action, and that the audit trail identifies the prior session. This exercise exposes assumptions quickly: missing provider endpoints, unknown account ownership, tokens stored in old developer machines, and cached access tokens that outlive local expectations.
Set an expiry review for grants that do not have provider-enforced limits. Long lived access is sometimes necessary for unattended work, but indefinite access should be a deliberate exception with a named owner. When a team cannot name the owner, the grant should not remain usable.
The standard to hold is simple: an agent may request work, but it must not inherit an account's ability to renew itself forever. Put the refresh credential where a human can control its use, and make revocation a practiced action rather than an emergency search through browser tabs.
FAQ
What is a refresh token in OAuth?
A refresh token lets a client obtain a new access token without sending the user through an interactive sign-in again. It usually has more staying power than an access token, so an agent that gets one can keep acting long after the original prompt has disappeared. Treat it as a credential with its own ownership and revocation plan.
Can an AI agent safely use an OAuth refresh token?
An agent can use one safely only when the agent never receives the token value and cannot choose its own scope or refresh policy. A controlled action layer should hold the grant, request tokens when an approved action needs them, and return the API result. Giving the token to an agent process turns every prompt injection and local process compromise into a credential incident.
Who should authorize an agent to refresh OAuth access?
The person or team that owns the connected account should authorize the original grant. A separate operator may run the action layer, but they should not silently expand scopes or reconnect another person's account. Write down the owner before the first authorization, because the token itself rarely tells you who approved its use.
Which OAuth flow should connect a human account to an agent?
Use authorization code flow with PKCE when a human connects their account through an interactive browser session. Do not use a device code flow merely because it looks easier for a command-line agent, since it creates a second approval surface that people often fail to monitor. Client credentials fit machine identities, not a person's SaaS account.
What is refresh token rotation?
Refresh token rotation means the authorization server issues a replacement refresh token each time the old one is used. The action layer must store the replacement before it makes another refresh attempt, then discard the old value. If two processes race to refresh, one may trigger reuse detection and invalidate the grant family.
What should happen when an OAuth refresh token expires?
An expired or revoked grant should stop the action and produce a clear reconnect request for the account owner. Do not fall back to another saved account, silently request broader permissions, or retry for hours. A failed refresh is often the correct signal that the previous authorization no longer belongs to the current task.
How do I revoke an OAuth refresh token?
Use the provider's revocation endpoint when it offers one, then remove the local credential and stop active agent sessions that can request it. RFC 7009 defines a revocation request format, though providers differ in what they revoke when you submit a refresh token. Confirm the result through a harmless API call or provider audit record when the service supports that check.
What should an OAuth agent audit log record?
The log should identify the requesting agent process or session, the human approval that allowed that session, the connected account reference, the destination, the scopes used, and the outcome. It should not store bearer tokens, authorization codes, or sensitive API response bodies. A timestamp alone cannot explain whether a refresh was legitimate.
Is a secrets manager enough for agent OAuth tokens?
A secret manager protects storage, which is necessary, but it does not decide whether a particular agent run may spend the credential. An action layer adds a decision point between the agent and the provider. You need both if autonomous processes can make external calls.
Can I inspect an OAuth refresh token to see what it can access?
No. Many providers allow opaque refresh tokens, and an opaque value cannot tell you its scopes, owner, expiry, or revocation state by inspection. Keep those facts in your own authorization inventory when you create the grant, and verify behavior with the provider rather than trusting token shape.