# How AI agent refresh tokens should be controlled

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:

1. Who may create the original grant.
2. Which action layer may retain the resulting refresh token.
3. Which agent sessions may request actions under that grant.
4. 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:

1. An agent session asks for a named operation and supplies ordinary action parameters.
2. The action layer identifies the grant tied to that operation, then checks whether the session may use it.
3. If the cached access token is absent or near expiry, the layer sends the refresh token to the provider's token endpoint.
4. The layer calls the destination API with the access token and returns the filtered result to the agent.
5. 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:

1. Send the old refresh token to the token endpoint over TLS.
2. Validate the token response and associate it with the expected provider and grant.
3. Write the new refresh token and metadata to encrypted storage in one durable update.
4. Mark the old token unusable in local state.
5. 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:

```http
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.
