Concepts · auth
OAuth
OAuth transport middleware, login flow, token refresh, subscription login reuse, and provider integration for Anthropic and OpenAI
- Authenticating with OAuth tokens instead of API keys
- Understanding how token refresh works
- Adding OAuth login to a CLI application
- Connecting any provider with OAuth credentials
- Reusing an existing Claude Code or Codex CLI subscription login
- Supporting OAuth login on headless / SSH / VPS environments
This page gives you the OAuth transport model, login flow, token refresh rules, subscription reuse path, and provider integration points.
The pkg/ai/oauth package provides optional OAuth support as an HTTP transport layer and reusable login flow. Providers continue to work with plain API keys. OAuth is strictly opt-in.
Design: transport, not interface
The package implements OAuth as an http.RoundTripper middleware, not as a change to the Provider interface. This keeps OAuth concerns out of the core SDK. It lets any provider that accepts a custom http.Client gain OAuth support for free.
The transport intercepts every outgoing HTTP request to:
- Detect access token expiry, with a 5-minute safety margin
- Refresh the token if needed, using a provider-specific
TokenRefresher - Inject the
Authorization: Bearer <token>header - Inject any provider-specific headers, for example Anthropic’s
anthropic-betaheader
A mutex protects refresh. Concurrent requests wait for a single refresh instead of racing.
Design: credentials are opaque to providers
Providers receive an oauth.Credentials value. They do not know or care how the application obtained it. Credentials can come from a login flow, a file on disk, an environment variable, or a test fixture. This separation keeps the provider layer focused on API calls. It pushes authentication orchestration to the application layer.
Design: no hardcoded client IDs
The SDK never embeds OAuth client IDs and secrets. Each provider’s Refresher requires them as explicit fields. Convenience constructors like WithOAuth accept them as parameters. The application layer sources these values from environment variables, configuration files, or another local source.
Login flow
The oauth package provides a reusable Login(ctx, LoginConfig) function. It runs the full OAuth authorization code flow with PKCE:
- Generates a PKCE verifier and S256 challenge (
GeneratePKCE()) - Starts a local HTTP callback server on a configured port
- Builds the authorize URL with all required parameters
- Calls a
DisplayURLcallback, injected by the application, to show the URL - Waits for the browser callback with the authorization code
- Exchanges the code for tokens at the provider’s token endpoint
- Returns
Credentialswith access token, refresh token, and expiry
Each provider exposes a LoginConfig(clientID, ...) function. That function returns a pre-filled LoginConfig with the correct endpoints, ports, scopes, and token exchange format. The application only needs to set the DisplayURL callback and call oauth.Login.
LoginConfig fields like UseJSONTokenRequest and IncludeStateInTokenExchange capture provider-specific login details. For example, Anthropic uses JSON token exchange with state. OpenAI uses form-encoded token exchange without state.
Manual code-paste fallback (headless / SSH / VPS)
The localhost callback server only works when the browser can reach the machine running Login. On a headless server, over SSH, or inside a container, that callback never arrives. Setting the optional ReadCode callback enables a paste fallback. The application obtains the authorization code, or the full redirect URL, from the user. It usually reads one stdin line. Login exchanges it exactly like a callback-delivered code.
Design: the callback server and ReadCode run concurrently. Whichever delivers a valid code first wins. The user can let the browser complete the loopback redirect. The user can also copy the redirect URL from the address bar and paste it. parsePastedCode accepts a bare code, a full …/callback?code=…&state=… URL, or the code#state form that some providers show inline. When the pasted value carries a state, Login compares it against the request’s state. When it omits one, Login skips the state comparison. If Login cannot bind the callback port at all, it proceeds with the paste path alone.
Token refresh and persistence
The transport calls the OnRefresh callback after every successful token refresh. The application can persist updated credentials there. If persistence returns an error, the request returns an error. That prevents silent loss of a rotated refresh token. The transport keeps the refreshed credentials in memory. It retries persistence on the next request without refreshing again. The SDK deliberately does not define where credentials are stored, because that is an application concern.
The TokenRefresher interface has a single method. It exchanges one set of credentials for a new set. Each provider implements its own refresher in its own package. That refresher uses the correct token endpoint and request format. All refreshers preserve the original refresh token if the server response omits a new one.
Design: refreshers live with their providers
Provider-specific OAuth code lives in the respective provider package, not in pkg/ai/oauth. This includes refreshers, transport constructors, login configurations, and extra headers. The oauth package is a generic toolkit. It contains Credentials, Transport, TokenRefresher, PKCE, Login, and functional options. This keeps the dependency graph clean. Each provider depends on oauth, but oauth has no knowledge of any provider.
Subscription login: reuse official CLI logins
If a user already signed in with the official Claude Code or Codex CLI, they have a working subscription OAuth login on disk. The application can reuse those credentials instead of asking for another login. This path rides an existing Claude Pro/Max or ChatGPT subscription with zero configuration.
Design: re-read, do not refresh. The official CLIs refresh their own tokens in the background. Instead of running our own refresh, the reuse path supplies a TokenRefresher with a different behavior. Its “refresh” re-reads the CLI’s own credential store and returns the freshest token. This has two consequences that make it the right model:
- No client ID is needed. A real OAuth refresh requires the provider’s public client ID. Re-reading needs nothing. This makes subscription login zero-configuration without embedding any client IDs. See no hardcoded client IDs.
- No token rotation. Refreshing rotates the refresh token, which can silently invalidate the other CLI’s login. Re-reading never writes, so both tools keep working. This mirrors the “token sink” pattern: read credentials from one authoritative place.
If a re-read token is itself expired, the refresher surfaces an error. The error directs the user to re-authenticate with that CLI instead of returning a stale token.
This reuse path needs no SDK changes. It plugs into the existing oauth.Transport via the WithRefresher option. That option overrides each provider’s default HTTP refresher. The credential readers and re-read refresher are application concerns. They live in the application layer (cmd/pi), consistent with credentials being opaque to providers.
Credential sources (as of writing):
- Claude Code — the macOS login Keychain (service
Claude Code-credentials, matched by service name, with the local username as the account), falling back to~/.claude/.credentials.json. Schema:claudeAiOauth.{accessToken, refreshToken, expiresAt}. - Codex CLI —
$CODEX_HOME/auth.json(default~/.codex/auth.json). Schema:tokens.{access_token, refresh_token, account_id}. Codex carries no explicit expiry, so the application derives it from the access token’s JWTexpclaim. The transport reusesaccount_idfor thechatgpt-account-idheader that the Codex backend requires.
On macOS, a per-application Keychain gate protects the read. The first interactive use prompts the user to allow access. In non-interactive contexts, the read returns denied, and the on-disk file fallback applies.
Provider integrations
Anthropic
WithOAuth(clientID, creds, ...opts) wires up everything:
- Sets
option.WithAuthTokenon the SDK client, for Bearer auth instead ofx-api-key - Creates an
oauth.Transportwith the Anthropic refresher and OAuth-specific headers (anthropic-beta,x-app) - Wraps any existing
http.Clienttransport, ifWithHTTPClientprovided one - Starts login at
https://claude.com/cai/oauth/authorizewithcode=true, matching Claude Code’s Claude.ai subscription flow - Uses JSON token exchange and includes the state parameter
Token endpoint: https://platform.claude.com/v1/oauth/token
OpenAI
NewWithOAuth(clientID, creds, ...opts) creates a provider with OAuth:
- Creates an
oauth.Transportwith the OpenAI refresher - Passes the resulting
http.Clientto the OpenAI SDK - Uses form-encoded token exchange without a state parameter
Token endpoint: https://auth.openai.com/oauth/token
ListCodexModels(ctx, clientID, accountID, creds, ...opts) asks the authenticated Codex backend for the ChatGPT account’s available models. It supplies the required Codex client compatibility version. It applies the same OAuth refresh transport and account header as NewForCodexOAuth. It omits models that are hidden from the picker or unsupported by the API.
CLI integration
The cmd/pi CLI demonstrates the full OAuth lifecycle:
pi login <provider>— runs the login flow and stores credentials at~/.pigo/auth.json. The CLI wires in the paste fallback, so login also works over SSH/VPS.pi logout <provider>— removes stored credentials.- Provider detection precedence: explicit
~/.pigo/auth.jsoncredentials first, subscription logins reused from the official Claude Code / Codex CLIs second, API keys / OAuth tokens from environment variables third. pi loginpersists refreshed tokens back toauth.jsonviaOnRefresh. Reused CLI logins never write back because the source CLI owns them.- The
--providerflag selects which provider to use when multiple are available.
Application-level concerns
The SDK intentionally excludes the following concerns:
- Client IDs and secrets — these are application configuration, not SDK constants.
- Token detection — detecting whether a string is an OAuth token, for example the
sk-ant-oatprefix, belongs in the application layer. - Environment variable resolution — which environment variables to read and in what order is application-specific.
- Credential storage — file-based, keychain, or otherwise is the application’s choice.