Help us shape the future of Go! Take the 2026 Go Developer Survey

codexauth

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 29 Imported by: 2

README

codex-auth-go

Go library for authenticating against the ChatGPT subscription-backed Codex endpoint and producing an *http.Client that can call:

https://chatgpt.com/backend-api/codex/responses

Install

go get github.com/mattsp1290/codex-auth-go

Quickstart

ctx := context.Background()

auth := codexauth.NewClient(codexauth.Options{
	AppName: "my-agent",
})

if _, err := auth.Status(ctx); err != nil {
	log.Fatal(err)
}

httpClient, err := auth.HTTPClient(ctx)
if errors.Is(err, codexauth.ErrNotLoggedIn) {
	if _, err := auth.Login(ctx, false); err != nil {
		log.Fatal(err)
	}
	httpClient, err = auth.HTTPClient(ctx)
}
if err != nil {
	log.Fatal(err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, codexauth.CodexEndpoint, body)
if err != nil {
	log.Fatal(err)
}
resp, err := httpClient.Do(req)
if err != nil {
	log.Fatal(err)
}
defer resp.Body.Close()

HTTPClient rewrites outbound requests to the Codex Responses endpoint, injects the bearer token, refreshes tokens before expiry, and preserves the Codex-specific originator and session_id header casing.

Account-aware model catalog

Client.ListModels fetches the current model picker catalog for the authenticated ChatGPT account without exposing credentials or the backend wire schema:

models, err := auth.ListModels(ctx, "your-catalog-compatibility-value")
if errors.Is(err, codexauth.ErrNotLoggedIn) {
	log.Fatal("not logged in")
}
if err != nil {
	log.Fatal(err)
}
for _, model := range models {
	fmt.Printf("%s: %s\n", model.Slug, model.DisplayName)
}

The caller supplies clientVersion as an opaque, non-secret Codex catalog compatibility value. The library passes the exact value through as the client_version query parameter; it does not assume that an application or Go module version has the backend's intended meaning.

Results depend on the authenticated account and the server's current catalog. Only picker-visible entries are returned, stable-sorted by ascending server priority. Each model's reasoning options stay in server order, and effort names are open strings so future values remain usable.

Every call performs a fresh catalog request plus any token refresh required for stale credentials; the library does not cache catalog data. A caller may cache within one UI session, but should refetch after login or account changes and when the user explicitly refreshes. Catalog calls through one Client are serialized to protect rotating refresh tokens. Calls through separate clients or processes are not coalesced.

Cancellation is prompt when credentials are fresh. If credentials are stale, the existing detached refresh and safe persistence step may finish before the call returns; cancellation still prevents the subsequent catalog request. Non-success catalog errors can be inspected with errors.As to *ModelCatalogHTTPError. Catalog and catalog-scoped refresh errors never expose network response bodies.

Login Flows

Client.Login(ctx, false) prefers the browser OAuth + PKCE flow when a browser launcher is available. If no browser launcher can be found, it falls back to the device flow so SSH and headless environments still work.

Use Client.Login(ctx, true) to force the device flow. The device flow prints a browser URL and user code to stderr, polls until authorization succeeds or the device code expires, exchanges the authorization code, and stores credentials. Set Options.DevicePrompt to render the device URL and user code in your own UI instead of using the default stderr prompt.

Package-level Login, LoginBrowser, LoginDevice, Logout, and HTTPClient remain as deprecated advisor-compatibility wrappers. New code should use an explicit Client.

Status

Client.Status(ctx) is local-only. It reads the configured credential store and does not refresh tokens, delete credentials, or make network calls.

StatusInfo.LoggedIn is true when a refresh token is stored. ExpiresAt reports the stored access-token expiry. Stale is true when the stored access token is expired or within the transport refresh margin. AccountID is the account ID cached from the login response, and ConfigPath is the auth.json path for the client's AppName.

AppName

Options.AppName selects the credential directory. NewClient(Options{}) defaults to codex; set a stable app name to isolate credentials for your program:

auth := codexauth.NewClient(codexauth.Options{AppName: "local-symphony"})

The deprecated package-level wrappers use advisor intentionally so existing advisor installations keep reading the same auth.json path without a re-login.

Endpoint override (hermetic tests)

Options.Endpoint overrides the Responses-API URL the transport rewrites every request to. Use it to point the real transport at a loopback SSE fake in tests:

// srv is an httptest.Server — replace srv.URL with your fake's address.
auth := codexauth.NewClient(codexauth.Options{
    AppName:        "my-agent",
    Endpoint:       srv.URL + "/backend/responses", // e.g. http://127.0.0.1:PORT/...
    CredentialPath: fixtureAuthPath,                // path to a staged auth.json
})
httpClient, err := auth.HTTPClient(ctx)

CredentialPath overrides the auth.json location, so you can stage a fixture credential file without touching HOME/XDG. The file must contain a valid openai entry; a file with no openai entry returns ErrNotLoggedIn.

Safety rail: cleartext http:// is permitted only for loopback hosts (127.0.0.0/8, ::1, localhost) so bearer tokens are never sent in plaintext to a remote host. HTTPClient and ListModels return ErrInsecureEndpoint for any other http:// endpoint. https:// to any host is always allowed. ListModels normalizes trailing slashes, replaces the final Responses path component with models, and drops endpoint query/fragment data.

Redirect caveat: when using a loopback http:// endpoint, your fake server must respond directly (e.g. 200 with the SSE body). The client hard-refuses any non-https redirect and returns "codexauth: refusing non-https redirect" — by design. A test fake that issues a 3xx will surface this error, not a 200. Respond directly.

Warning on CredentialPath and writes: on any write (token refresh, Save, Logout), the parent directory of CredentialPath is MkdirAll'd AND its permissions are unconditionally overwritten to 0700 — even if the directory already existed. Pointing CredentialPath at a file in a shared directory (/tmp, $HOME) will re-permission that directory on first write. Use t.TempDir() in tests or any directory you own exclusively.

Security

JWT handling is passive. ExtractAccountID decodes claims from an accepted OAuth response so the account ID can be cached, but it does not verify the JWT signature, issuer, audience, expiry, or not-before time.

Credential writes use mode 0600 for auth.json and tighten the parent directory to 0700 on platforms where POSIX modes apply. Logout performs best-effort RFC 7009 refresh-token revocation and refuses redirects for the revocation POST.

The module intentionally uses OpenAI's public Codex CLI OAuth client ID:

app_EMoamEEZ73f0CkXaXp7hrann

Do not make ClientID configurable or replace it with an application-specific value. That identifier is part of the Codex subscription OAuth flow, and changing it breaks authentication for this endpoint.

The Originator header value remains advisor in v0.1.0 to preserve the source package's on-the-wire behavior. Do not change it without a verified wire trace showing a different value is accepted; a future v0.2.0 may move it to Options.

The extraction plan lives in docs/prompts/extract-codex-auth-go.md.

Documentation

Overview

Package codexauth implements the OAuth2 + PKCE authentication flow for ChatGPT's Codex subscription, token storage and refresh, and the RoundTripper that injects bearer credentials into Responses-API requests.

All constants are verbatim from the registered OpenAI OAuth application. Do not modify them — they are correctness anchors that tie to the live auth.openai.com configuration.

Package codexauth authenticates ChatGPT subscription-backed Codex clients.

The package provides a focused Go library for obtaining an *http.Client that calls the Codex Responses endpoint with a refreshing bearer token and for fetching the authenticated account's picker-visible catalog through Client.ListModels. It is not a CLI, daemon, JSON-RPC server, credential broker, Eino ChatModel wrapper, or provider-classification layer.

JWT handling is intentionally passive. ExtractAccountID decodes claims so the account identifier can be cached with credentials, but it does not verify the JWT signature, issuer, audience, expiry, not-before time, or any other trust boundary. Treat decoded claims as local metadata from an OAuth response that has already been accepted, not as standalone proof of identity.

The credential store writes auth.json with mode 0600 and tightens its parent directory to 0700 on platforms where POSIX modes apply. Logout performs best-effort RFC 7009 refresh-token revocation before deleting local OpenAI credentials, and the revocation HTTP client refuses redirects so a token POST body is not replayed to another target.

The transport refreshes tokens 60 seconds before expiry and coalesces concurrent refresh attempts with singleflight. Client.ListModels additionally serializes its complete operation per Client so separate short-lived transports cannot rotate the same stale refresh token concurrently. It does not retry ordinary 401 responses as a refresh signal; refresh and invalid_grant recovery are handled through the explicit token-refresh path.

See README.md for tutorials and migration examples.

Index

Examples

Constants

View Source
const (
	// ClientID is the OAuth application client identifier registered with
	// auth.openai.com. Used in every OAuth request.
	ClientID = "app_EMoamEEZ73f0CkXaXp7hrann"
	// Issuer is the base URL of the OpenAI authorization server.
	Issuer = "https://auth.openai.com"
	// CodexEndpoint is the Responses-API endpoint for Codex inference.
	CodexEndpoint = "https://chatgpt.com/backend-api/codex/responses"
	// OAuthPort is the local callback server port used during the browser
	// OAuth flow.
	OAuthPort = 1455
	// RedirectURI is the registered OAuth redirect URI for the browser flow.
	RedirectURI = "http://localhost:1455/auth/callback"
	// Scope is the OAuth scope string requested for Codex access.
	Scope = "openid profile email offline_access"
	// PKCEMethod is the PKCE code challenge method. S256 is mandatory per
	// RFC 7636 §4.2 for public clients.
	PKCEMethod = "S256"
	// Originator is the literal value preserved from the source package's wire
	// behavior. Consumers must not change it without a verified wire trace
	// proving a different value is accepted; v0.2.0 may move it to Options.
	Originator = "advisor"
	// DeviceRedirectURI is the redirect_uri used when exchanging an authorization
	// code obtained from the device poll endpoint. The server expects this exact
	// value for codes issued via /api/accounts/deviceauth/token.
	DeviceRedirectURI = "https://auth.openai.com/deviceauth/callback"
)

Variables

View Source
var ErrAuthorizationDenied = errors.New("codexauth: authorization denied")

ErrAuthorizationDenied is sent on errCh when the authorization server redirects back with an ?error= parameter (e.g. "access_denied"). It wraps the raw OAuth error string so callers can errors.Is on the sentinel while still surfacing the details in log output.

View Source
var ErrInsecureEndpoint = errors.New("codexauth: refusing cleartext http endpoint to non-loopback host")

ErrInsecureEndpoint is returned by HTTPClient or ListModels when Options.Endpoint uses cleartext http:// to a non-loopback host (which would leak the bearer token).

View Source
var ErrNoBrowser = errors.New("codexauth: no browser launcher found")

ErrNoBrowser is returned by openBrowser when no suitable browser-launcher binary can be found on the current platform. Callers should treat any non-nil error from openBrowser as a signal to print the URL for the user to copy — ErrNoBrowser specifically indicates the launcher binary was absent, but darwin and windows implementations may return other exec errors on failure.

View Source
var ErrNotLoggedIn = errors.New("codexauth: not logged in")

ErrNotLoggedIn is returned by HTTPClient or ListModels when no OpenAI credentials are stored. Callers should check with errors.Is and prompt the user to run Login.

View Source
var ErrPlanNotIncluded = errors.New("codexauth: plan not included")

ErrPlanNotIncluded marks Codex API responses where the authenticated account does not have Codex subscription access.

View Source
var ErrQuotaExceeded = errors.New("codexauth: quota exceeded")

ErrQuotaExceeded marks Codex API responses where the authenticated account has exhausted its Codex quota.

View Source
var ErrRefreshFailed = errors.New("codexauth: refresh failed")

ErrRefreshFailed marks failures while refreshing stored Codex credentials. ListModels uses it for remote refresh failures whose details are not safe to expose; recognized OAuth errors remain inspectable as a sanitized AuthError.

View Source
var ErrStateMismatch = errors.New("codexauth: OAuth callback state mismatch")

ErrStateMismatch is sent on errCh when the OAuth callback arrives with a state parameter that does not match the value passed to StartCallbackServer. The caller should treat this as a potential CSRF attempt and abort the flow.

View Source
var Version = "0.0.0-dev"

Version is the User-Agent version string. Declared as a var so it can be overridden via -ldflags -X. TODO: replace with internal/version.Version() once that package exists.

Functions

func BuildAuthorizeURL

func BuildAuthorizeURL(challenge, state string) string

BuildAuthorizeURL constructs the OAuth2 authorization URL for the Codex subscription browser flow. All fixed parameters (client_id, redirect_uri, scope, originator, etc.) are sourced from the package constants so that the call site only supplies the per-flow values: the PKCE code challenge and the CSRF state token.

The returned URL is ready to open in a browser; the user will be directed to auth.openai.com to approve the Codex subscription scope.

Required params and their sources:

  • client_id, redirect_uri, scope, code_challenge_method → package constants
  • id_token_add_organizations, codex_cli_simplified_flow → hardcoded "true"
  • originator → hardcoded "advisor"
  • code_challenge, state → caller-supplied

func ChallengeFromVerifier

func ChallengeFromVerifier(v string) string

ChallengeFromVerifier computes the PKCE S256 code challenge from a verifier string. Returns base64url(sha256(verifier)) with no padding, as required by RFC 7636 §4.2.

func ClassifyCodexError added in v0.2.0

func ClassifyCodexError(err error) error

ClassifyCodexError maps Codex API error codes to exported auth sentinels.

Errors with a Code field matching "usage_not_included" wrap ErrPlanNotIncluded; errors with "insufficient_quota" wrap ErrQuotaExceeded. Unrecognised errors are wrapped as generic Codex generation failures.

func Delete

func Delete(provider string) error

Delete removes one provider's credentials from auth.json. Unrecognized provider names are silently ignored (idempotent). Supported values: "openai", "anthropic".

func ExtractAccountID

func ExtractAccountID(idToken string) string

ExtractAccountID returns the account identifier embedded in an OpenAI ID token. It tries four locations in order and returns the first non-empty value found:

  1. Top-level `chatgpt_account_id` — present on standard consumer accounts.
  2. `https://api.openai.com/auth.chatgpt_account_id` — custom claim namespace used by enterprise SSO accounts.
  3. `https://api.openai.com/auth.organization_id` — fallback within the same auth namespace when chatgpt_account_id is absent.
  4. `organizations[0].id` — last resort for accounts linked to an org.

Returns "" if the token is unparseable, if none of the four claim paths are populated, or if the input is malformed in any way. This function does NOT verify the JWT signature — it is a passive consumer of OpenAI's claim. Safe to call with attacker-controlled input: no crypto, no network, and it never panics. It always returns "" on any decode or parse error.

Segment acceptance: the function accepts any token with at least two dot-separated segments and treats segment [1] as the base64url payload. JWE tokens (4–5 segments) and 2-segment tokens will decode successfully but yield empty claims because their second segment is either encrypted or missing expected fields — both cases return "" cleanly.

func GenerateState

func GenerateState() (string, error)

GenerateState returns a 32-byte cryptographically random nonce encoded as base64url with no padding. Used as the OAuth2 state parameter to prevent CSRF (RFC 6749 §10.12).

func GenerateVerifier

func GenerateVerifier() (string, error)

GenerateVerifier returns a 43-character PKCE code verifier sampled from the URL-unreserved alphabet (RFC 7636 §4.1) using rejection-sampling on crypto/rand bytes to eliminate modulo bias. Entropy: 43 × log2(66) ≈ 260 bits, exceeding RFC 7636 §7.1's 128-bit recommendation. The rejection threshold is (256/alphabetN)*alphabetN so each accepted byte maps to exactly one of the 66 alphabet positions with uniform probability.

func HTTPClient deprecated

func HTTPClient(_ context.Context) (*http.Client, error)

HTTPClient returns an *http.Client pre-configured to route requests through the Codex Responses API with automatic OAuth bearer-token injection and refresh. The transport is seeded from the OpenAI credentials currently on disk at the time of the call. Subsequent cross-process token rotations are detected lazily via the invalid_grant recovery path in codexTransport.

ctx is reserved for future use (e.g. tracing the credential load). All per-request context propagation happens via the individual request's context.

Returns ErrNotLoggedIn (sentinel) if no credentials are stored. Callers should errors.Is against it and prompt the user to run Login.

Deprecated: use NewClient(Options{AppName: "advisor"}).HTTPClient instead.

func IsCodexAllowed

func IsCodexAllowed(model string) bool

IsCodexAllowed reports whether model is permitted for use with the Codex subscription endpoint. Admission follows two paths:

  1. Explicit allow-list — models licensed for Codex regardless of their version order in the `gpt-<N>.<M>` space (e.g. gpt-5.3-codex, which is below the regex cutoff but ships with Codex access). Sourced from opencode codex.ts §CODEX_MODELS; see .claude/docs/codex-auth/06-models-and-errors.md.

  2. Version regex — any model ID matching ^gpt-<major>.<minor> where the integer pair (major, minor) is strictly greater than (5, 4). Minor versions are compared as integers, not as floats: gpt-5.10 has minor=10 > 4, so it is allowed; a float parse of "5.10" would give 5.1 < 5.4 and incorrectly exclude it. The regex has no end anchor, so a trailing patch component (e.g. gpt-5.5.0) is ignored — only the first major.minor pair is examined.

The caller is responsible for normalizing model identifiers before calling this function. It is case-sensitive and does not strip provider prefixes (e.g. "openai/gpt-5.5" would not match).

func Logout deprecated

func Logout(ctx context.Context) error

Logout revokes the stored refresh token (RFC 7009 best-effort) and then deletes the on-disk OpenAI credentials. Network errors during revocation are logged to logoutStderr but do not prevent the local delete. Idempotent: returns nil when no credentials are stored.

Deprecated: use NewClient(Options{AppName: "advisor"}).Logout instead.

func Path

func Path() (string, error)

Path returns the canonical path to auth.json.

func Save

func Save(af AuthFile) error

Save persists af to auth.json using the atomic write protocol: MkdirAll → flock(.lock) → re-read → merge → CreateTemp → Write+Sync → Chmod 0600 → Rename → fsync dir → Chmod dir 0700 → release flock.

Fields in af that are nil are not written; existing values for those providers are preserved from the on-disk file (merge-don't-clobber).

func StartCallbackServer

func StartCallbackServer(ctx context.Context, state string) (codeCh <-chan string, errCh <-chan error, shutdown func(), err error)

StartCallbackServer binds loopback listeners on OAuthPort (1455) for both IPv4 (127.0.0.1) and IPv6 ([::1]) and serves a /auth/callback handler.

Both listeners are bound synchronously before the function returns — callers may open the browser immediately after a nil err return. If either bind fails with EADDRINUSE, *PortInUseError is returned and no goroutines are started. If IPv6 is unavailable for reasons other than EADDRINUSE, only IPv4 is used.

The server shuts down automatically when ctx is cancelled or times out. The returned shutdown func may also be called directly (e.g. via defer) to close the server; subsequent calls to shutdown are no-ops.

Types

type AuthError

type AuthError struct {
	Code        string `json:"error"`
	Description string `json:"error_description"`
}

AuthError represents an RFC 6749 error response from the token endpoint. The Code field contains the OAuth error code string (e.g. "invalid_grant", "invalid_client") as defined in RFC 6749 §5.2. Callers use errors.As to inspect Code and map it to the appropriate core.ErrType.

func (*AuthError) Error

func (e *AuthError) Error() string

type AuthFile

type AuthFile struct {
	// OpenAI is nil if no OpenAI creds are stored. When non-nil, IDToken is
	// always "" — the ID token is consumed once by ExtractAccountID and is
	// never persisted.
	OpenAI    *Credentials
	Anthropic *json.RawMessage // raw bytes; never inspected by this package
}

AuthFile is the caller-facing view of auth.json.

On POSIX, auth.json is created with mode 0600 and its parent directory with mode 0700. On Windows, file access is governed by inherited ACLs from %LocalAppData%; Chmod is a best-effort approximation only.

func Load

func Load() (AuthFile, error)

Load reads auth.json and returns its contents. A missing file is not an error; the returned AuthFile has nil fields in that case.

Load acquires a shared (read) flock for cross-process safety, but does NOT hold the lock after returning. Callers performing a read-modify-write (e.g. load → refresh tokens → save) should be aware that another process may persist a rotation in between; the Save call's "re-read under lock + merge" path protects against clobbering the *other* provider's entry but does NOT detect that the OpenAI refresh token the caller is about to overwrite was already rotated by a sibling process. For concurrent-safe refresh, route through a single goroutine / singleflight per process.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client authenticates a single Codex consumer with its own credential namespace and test seams. ListModels calls made through one Client are serialized; distinct Client values and processes are not coalesced.

func NewClient

func NewClient(opts Options) *Client

NewClient returns a Client configured from opts.

Example
package main

import (
	"bytes"
	"context"
	"errors"
	"log"
	"net/http"

	codexauth "github.com/mattsp1290/codex-auth-go"
)

func main() {
	if false {
		ctx := context.Background()
		auth := codexauth.NewClient(codexauth.Options{AppName: "yourapp"})

		status, err := auth.Status(ctx)
		if err != nil {
			log.Fatal(err)
		}
		if !status.LoggedIn {
			if _, err := auth.Login(ctx, false); err != nil {
				log.Fatal(err)
			}
		}

		httpClient, err := auth.HTTPClient(ctx)
		if errors.Is(err, codexauth.ErrNotLoggedIn) {
			log.Fatal("not logged in")
		}
		if err != nil {
			log.Fatal(err)
		}

		body := bytes.NewBufferString(`{"model":"gpt-5.5","input":"hello"}`)
		req, err := http.NewRequestWithContext(ctx, http.MethodPost, codexauth.CodexEndpoint, body)
		if err != nil {
			log.Fatal(err)
		}
		req.Header.Set("Content-Type", "application/json")

		_ = httpClient
		_ = req
	}

}

func (*Client) HTTPClient

func (c *Client) HTTPClient(_ context.Context) (*http.Client, error)

HTTPClient returns an *http.Client pre-configured for this client's stored Codex credentials.

func (*Client) ListModels added in v0.4.0

func (c *Client) ListModels(ctx context.Context, clientVersion string) ([]ModelCatalogEntry, error)

ListModels fetches the current picker-visible model catalog for this client's authenticated account. clientVersion is an opaque, non-secret compatibility value supplied by the caller and is passed through unchanged. The result is stable-sorted by ascending server priority; reasoning options retain their server order. The package does not cache results.

Calls through one Client are serialized so stale rotating refresh tokens are not used concurrently. Distinct clients and processes are not coalesced. Cancellation during a stale-token refresh may return only after the existing detached refresh persistence step finishes, but no catalog request is sent afterward.

Example
package main

import (
	"context"
	"errors"
	"log"

	codexauth "github.com/mattsp1290/codex-auth-go"
)

func main() {
	if false {
		ctx := context.Background()
		auth := codexauth.NewClient(codexauth.Options{AppName: "yourapp"})
		models, err := auth.ListModels(ctx, "your-catalog-compatibility-value")
		if errors.Is(err, codexauth.ErrNotLoggedIn) {
			log.Fatal("not logged in")
		}
		if err != nil {
			log.Fatal(err)
		}
		for _, model := range models {
			_ = model.Slug
			_ = model.SupportedReasoningEfforts
		}
	}

}

func (*Client) Login

func (c *Client) Login(ctx context.Context, forceDevice bool) (Credentials, error)

Login initiates an OAuth flow for this client and returns fresh credentials.

func (*Client) LoginBrowser

func (c *Client) LoginBrowser(ctx context.Context) (Credentials, error)

LoginBrowser implements the browser-based OAuth2 + PKCE authorization flow for this client.

func (*Client) LoginDevice

func (c *Client) LoginDevice(ctx context.Context) (Credentials, error)

LoginDevice implements OpenAI's headless device-authorization flow for this client.

func (*Client) Logout

func (c *Client) Logout(ctx context.Context) error

Logout revokes this client's stored refresh token and deletes local credentials.

func (*Client) Path

func (c *Client) Path() (string, error)

Path returns this client's canonical path to auth.json.

func (*Client) Status

func (c *Client) Status(ctx context.Context) (StatusInfo, error)

Status reads the local credential store and reports this client's login state. It does not perform network refreshes or delete credentials.

type Credentials

type Credentials struct {
	Access    string `json:"-"`
	Refresh   string `json:"-"`
	IDToken   string `json:"-"`
	Expires   int64  `json:"-"` // absolute epoch milliseconds when Access expires
	AccountID string `json:"-"` // populated by caller via ExtractAccountID, not here
}

Credentials is part of the public v0.1.0 API surface. It holds the OAuth token set returned after a successful authorization-code exchange or token refresh.

Expires is an absolute Unix epoch in milliseconds — not a relative duration — so callers can compare it directly against time.Now().UnixMilli().

AccountID is not populated by ExchangeCode or RefreshToken; callers should invoke ExtractAccountID(creds.IDToken) separately and store the result before the ID token is discarded.

func ExchangeCode

func ExchangeCode(ctx context.Context, code, verifier, redirectURI string) (Credentials, error)

ExchangeCode trades an authorization code (plus PKCE code verifier) for a token set. The redirectURI must exactly match the redirect_uri sent during the /authorize request; a mismatch will cause the server to reject the exchange with invalid_grant.

Returns *AuthError if the server responds with an OAuth error. The Credentials.AccountID field is always empty; populate it via ExtractAccountID after calling this function.

func Login deprecated

func Login(ctx context.Context, forceDevice bool) (Credentials, error)

Login initiates an OAuth flow and returns fresh credentials.

If forceDevice is true, the device-authorization flow is used regardless of browser availability. Otherwise, Login probes for a browser launcher via browserAvailableCheck: any non-nil return causes Login to fall back to the device flow. When a browser is available, the PKCE browser flow is used.

Deprecated: use NewClient(Options{AppName: "advisor"}).Login instead.

func LoginBrowser deprecated

func LoginBrowser(ctx context.Context) (Credentials, error)

LoginBrowser implements the browser-based OAuth2 + PKCE authorization flow:

  1. Generates a PKCE verifier, S256 challenge, and CSRF state nonce.
  2. Starts the local callback server (MUST happen before opening the browser).
  3. Opens the browser to the authorize URL; if no browser is available, prints the URL to stderr for manual copy-paste.
  4. Waits up to browserLoginTimeout for the OAuth callback.
  5. Exchanges the authorization code for tokens.
  6. Extracts the account ID and persists credentials.

If port OAuthPort (1455) is already in use, LoginBrowser returns *PortInUseError without opening the browser — the trip-wire invariant in the test suite enforces that openBrowserFn is never called on an EADDRINUSE failure.

Deprecated: use NewClient(Options{AppName: "advisor"}).LoginBrowser instead.

func LoginDevice deprecated

func LoginDevice(ctx context.Context) (Credentials, error)

LoginDevice implements OpenAI's headless device-authorization flow for SSH / CI environments. The flow:

  1. Requests a user_code from the device init endpoint.
  2. Validates user_code charset to guard against terminal injection.
  3. Prompts the user to open the device URL and enter the code.
  4. Polls the token endpoint at (interval+3)s until authorized or expired.
  5. Exchanges the authorization_code for OAuth tokens.
  6. Extracts the account ID and persists credentials to disk.

This is NOT RFC 8628 — OpenAI uses custom /api/accounts/deviceauth/ endpoints and returns an authorization_code that must be exchanged at /oauth/token. See .claude/docs/codex-auth/03-device-flow.md.

Deprecated: use NewClient(Options{AppName: "advisor"}).LoginDevice instead.

func RefreshToken

func RefreshToken(ctx context.Context, refreshTok string) (Credentials, error)

RefreshToken exchanges a refresh token for a new token set. The refresh token rotates on every successful call — the returned Credentials.Refresh must be persisted immediately; the old token is invalidated by the server.

Returns *AuthError if the server responds with an OAuth error. The most common error is "invalid_grant" (refresh token revoked, expired after 90 days of inactivity, or user signed out from all sessions).

func (Credentials) LogValue

func (c Credentials) LogValue() slog.Value

LogValue implements slog.LogValuer. It returns a redacted representation so that slog.Info("creds", "c", creds) never emits token values into logs.

func (Credentials) String

func (c Credentials) String() string

String implements fmt.Stringer. Returns a redacted sentinel so that fmt.Sprintf("%v", creds) and log lines using %v cannot leak token values.

type DeviceError

type DeviceError struct {
	Code    string
	Message string
	Err     error // underlying error, if any; see Unwrap
}

DeviceError is a typed error returned by LoginDevice for flow-level failures: invalid user_code charset, fatal poll response, or expiry. Err holds an optional underlying error so errors.Is/As can traverse the chain.

func (*DeviceError) Error

func (e *DeviceError) Error() string

func (*DeviceError) Unwrap

func (e *DeviceError) Unwrap() error

Unwrap returns the underlying error so errors.Is and errors.As can traverse the chain (e.g. errors.Is(de, context.Canceled) works through a *DeviceError).

type ModelCatalogEntry added in v0.4.0

type ModelCatalogEntry struct {
	Slug                      string
	DisplayName               string
	Description               *string
	Priority                  int
	DefaultReasoningEffort    *string
	SupportedReasoningEfforts []ReasoningEffortOption
}

ModelCatalogEntry describes one picker-visible model available to the authenticated ChatGPT account.

type ModelCatalogHTTPError added in v0.4.0

type ModelCatalogHTTPError struct {
	StatusCode int
}

ModelCatalogHTTPError reports a non-success response from the model catalog. It intentionally exposes only the status code, never the response body.

func (*ModelCatalogHTTPError) Error added in v0.4.0

func (e *ModelCatalogHTTPError) Error() string

type Options

type Options struct {
	// AppName selects the on-disk credential directory. An empty AppName
	// defaults to "codex" for explicit clients.
	AppName string
	// CallbackPort selects the local OAuth callback port. Zero defaults to
	// OAuthPort.
	CallbackPort int
	// Logger receives structured diagnostics. Nil defaults to slog.Default().
	Logger *slog.Logger
	// DevicePrompt is called during device login after a safe user_code is
	// received and before polling begins. Nil preserves the default stderr
	// prompt.
	DevicePrompt func(verificationURI, userCode string) error
	// Endpoint overrides the Responses-API URL that the transport rewrites every
	// request to. Empty preserves the default (CodexEndpoint,
	// https://chatgpt.com/backend-api/codex/responses). The value is parsed and
	// validated lazily in HTTPClient or ListModels (not NewClient): it must be an
	// absolute URL with scheme http or https. ListModels derives a sibling models
	// route by normalizing trailing slashes and replacing the final path segment
	// with "models". As a safety rail, a cleartext http:// endpoint is permitted
	// only for loopback hosts (127.0.0.0/8, ::1, localhost) so a bearer token is
	// never sent in plaintext to a remote host.
	//
	// Note: only Scheme, Host, and Path from Endpoint are used. Any query string
	// embedded in Endpoint is silently dropped; HTTPClient preserves the caller's
	// query string unchanged, while ListModels supplies only its client_version
	// query. ListModels also drops any Endpoint fragment.
	//
	// Redirect caveat: when Endpoint uses cleartext http:// (loopback only), the
	// server must respond directly (e.g. 200 with the SSE body) and must NOT issue
	// a 3xx redirect. The client's redirect policy hard-refuses any non-https
	// redirect hop and returns "codexauth: refusing non-https redirect". This is
	// intentional defense-in-depth, not a bug — design your test fake accordingly.
	Endpoint string
	// CredentialPath overrides the on-disk auth.json location. Empty preserves the
	// default, os.UserConfigDir()/<AppName>/auth.json (where <AppName> defaults to
	// "codex" for explicit NewClient callers). When set, all credential reads and
	// writes for this client target the given path — useful for staging a fixture
	// credential file in tests without touching HOME/XDG.
	//
	// WARNING: point this at a file inside a directory you own EXCLUSIVELY. On any
	// write (Save/Logout/refresh) the PARENT DIRECTORY of this path is MkdirAll'd
	// AND its permissions are unconditionally overwritten to 0700 — even if the
	// directory already existed. Pointing CredentialPath at a file in a shared
	// directory (e.g. /tmp/auth.json, $HOME/auth.json) will re-permission that
	// directory to owner-only on first write. Use t.TempDir() in tests.
	// Reads do not mutate anything.
	CredentialPath string
}

Options configures a Client.

type PortInUseError

type PortInUseError struct {
	Addr string
	Err  error
}

PortInUseError is returned by StartCallbackServer when port OAuthPort (1455) is already bound on either loopback address. The caller should surface this to the user; the fix is to stop the other process holding the port.

func (*PortInUseError) Error

func (e *PortInUseError) Error() string

func (*PortInUseError) Unwrap

func (e *PortInUseError) Unwrap() error

type ReasoningEffortOption added in v0.4.0

type ReasoningEffortOption struct {
	Effort      string
	Description string
}

ReasoningEffortOption describes one server-supported reasoning setting. Effort is intentionally an open string so new server values remain usable.

type StatusInfo

type StatusInfo struct {
	LoggedIn   bool
	Stale      bool
	ExpiresAt  time.Time
	AccountID  string
	ConfigPath string
}

StatusInfo describes the local credential-store state for a Client.

Directories

Path Synopsis
cmd
smoke command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL