Skip to content

Concepts · auth

OAuth

OAuth transport middleware, login flow, token refresh, subscription login reuse, and provider integration for Anthropic and OpenAI

Read this when
  • 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:

  1. Detect access token expiry, with a 5-minute safety margin
  2. Refresh the token if needed, using a provider-specific TokenRefresher
  3. Inject the Authorization: Bearer <token> header
  4. Inject any provider-specific headers, for example Anthropic’s anthropic-beta header

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:

  1. Generates a PKCE verifier and S256 challenge (GeneratePKCE())
  2. Starts a local HTTP callback server on a configured port
  3. Builds the authorize URL with all required parameters
  4. Calls a DisplayURL callback, injected by the application, to show the URL
  5. Waits for the browser callback with the authorization code
  6. Exchanges the code for tokens at the provider’s token endpoint
  7. Returns Credentials with 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 JWT exp claim. The transport reuses account_id for the chatgpt-account-id header 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.WithAuthToken on the SDK client, for Bearer auth instead of x-api-key
  • Creates an oauth.Transport with the Anthropic refresher and OAuth-specific headers (anthropic-beta, x-app)
  • Wraps any existing http.Client transport, if WithHTTPClient provided one
  • Starts login at https://claude.com/cai/oauth/authorize with code=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.Transport with the OpenAI refresher
  • Passes the resulting http.Client to 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.json credentials first, subscription logins reused from the official Claude Code / Codex CLIs second, API keys / OAuth tokens from environment variables third.
  • pi login persists refreshed tokens back to auth.json via OnRefresh. Reused CLI logins never write back because the source CLI owns them.
  • The --provider flag 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-oat prefix, 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.
  • Providers — provider capabilities and registration
  • OptionsWithHeaders for per-request header injection