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

chatgptauth

package
v0.82.0 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

ABOUTME: Decodes ChatGPT identity (account ID, plan type) from an id_token's claims and reads ABOUTME: an access_token's exp claim — no signature check, matching Codex (spec §1 "Identity").

ABOUTME: AuthClient, the shared HTTP client for ChatGPT device-code sign-in and token refresh ABOUTME: against auth.openai.com — construction, defaults, and the request shapes.

ABOUTME: Device-code sign-in (design §1, research §B): request a user code, poll until the ABOUTME: user finishes in the browser, then exchange the server-issued PKCE pair for tokens.

ABOUTME: Cross-process refresh lock on Unix: an exclusive flock on chatgpt-auth.lock beside the ABOUTME: token file, polled without blocking so that a canceled context ends the wait.

ABOUTME: Parses the TRACKER_OPENAI_AUTH switch that picks how Tracker authenticates calls to ABOUTME: the openai provider: the default API key, or a signed-in ChatGPT subscription (design §2).

ABOUTME: Refresh-token exchange at auth.openai.com (research §C), and RefreshError, which sorts a ABOUTME: failure into permanent (sign in again) or transient from the token endpoint's JSON error.

ABOUTME: TokenSource hands out the current ChatGPT access token, refreshing it near expiry or after ABOUTME: a 401, one refresh at a time across goroutines (a mutex) and processes (a file lock).

ABOUTME: Derives a point-in-time ChatGPT sign-in summary (plan, refresh and expiry times) from ABOUTME: the token file, folding a missing file into a non-error "not signed in" result.

ABOUTME: The ChatGPT token file: default path, atomic save, and a Load that refuses a ABOUTME: missing, incomplete, symlinked or over-permissive file before touching its contents.

ABOUTME: Unix safety check and no-follow open for the ChatGPT token file: refuses a symlink or ABOUTME: a group/other-accessible mode (mirrors cmd/tracker/envfiles_unix.go's O_NOFOLLOW + Lstat pattern).

ABOUTME: Builds the "tracker/<version> (<GOOS>; <GOARCH>)" User-Agent string every chatgptauth ABOUTME: request sends, deriving <version> from the running binary's embedded build info.

Index

Constants

View Source
const ClientID = "app_EMoamEEZ73f0CkXaXp7hrann"

ClientID is Codex's own OAuth client ID, shared by tracker's sign-in and refresh requests (research §A, §B, §C).

View Source
const EnvVar = "TRACKER_OPENAI_AUTH"

EnvVar is the environment variable that selects openai's auth mode. Registered in internal/envpolicy as NetworkDestination / ConfigEnv: the shell or the operator's ~/.config/tracker/.env may set it, never a project <workdir>/.env (#659).

View Source
const FileName = "chatgpt-auth.json"

FileName is the token file's name under its directory (see DefaultPath).

View Source
const Issuer = "https://auth.openai.com"

Issuer is the ChatGPT OAuth issuer: the default base URL for every sign-in and refresh request (research §A "Issuer / base URL"). Tests override AuthClient.Issuer to point at an httptest server instead.

View Source
const LockFileName = "chatgpt-auth.lock"

LockFileName is the cross-process refresh lock's name, kept next to the token file; lockFile (lock_unix.go) takes an exclusive flock on it for the length of each refresh (design §1 "Refresh") and of each sign-in save (SaveSignIn).

Variables

View Source
var ErrDeviceCodeDisabled = errors.New("device-code sign-in is not enabled for this account (OpenAI returned 404)")

ErrDeviceCodeDisabled is returned by StartDeviceLogin when the server answers the usercode request with 404: a server-side feature flag, not a per-account setting (research §B).

View Source
var ErrDeviceCodeExpired = errors.New("the sign-in code expired after 15 minutes; start again")

ErrDeviceCodeExpired is returned by WaitForDeviceLogin once DeviceCode's deadline passes without the user finishing sign-in.

View Source
var ErrNotSignedIn = errors.New("not signed in to ChatGPT; run `tracker setup` and choose OpenAI → ChatGPT subscription")

ErrNotSignedIn is returned by Load, and folded into ReadStatus, when the token file does not exist.

Functions

func AccessTokenExpiry

func AccessTokenExpiry(accessToken string) (time.Time, bool)

AccessTokenExpiry decodes accessToken's claims and returns its exp claim as a time, or false when the token can't be decoded or carries no exp claim.

func DefaultPath

func DefaultPath() (string, error)

DefaultPath returns the token file's default location: $XDG_CONFIG_HOME/tracker/chatgpt-auth.json, falling back to $HOME/.config/tracker/... when XDG_CONFIG_HOME is unset or relative — see internal/xdgconfig.ConfigHome.

func Save

func Save(path string, t Tokens) error

Save writes t to path atomically: a temp file at mode 0600 in the same directory, fsynced, then renamed over path. MkdirAll creates a missing directory at 0700; it leaves an already-existing directory's mode exactly as found (MkdirAll is a no-op on an existing directory — it never chmods).

func SaveSignIn

func SaveSignIn(ctx context.Context, path string, t Tokens) error

SaveSignIn saves the tokens from a new sign-in while holding the refresh lock (LockFileName beside path). A running tracker process that loaded the token file before the sign-in may be refreshing it right now; it holds that lock until its own Save finishes. Taking the lock makes the sign-in save wait for that refresh and land after it. Without the lock, the refresh could save second and put the old sign-in back. A missing directory is created at 0700 first, because the lock file lives in it. On Windows lockFile is a no-op, so there the save takes no lock.

func UserAgent

func UserAgent() string

UserAgent returns the User-Agent header tracker's ChatGPT sign-in and refresh requests send: "tracker/<version> (<GOOS>; <GOARCH>)".

Types

type AuthClient

type AuthClient struct {
	// Issuer is the base URL every request is sent against. Default: Issuer.
	Issuer string
	// HTTPClient sends every request. Default: a 30-second-timeout client
	// that never follows a redirect (refuseRedirects). A client set here
	// is used exactly as given, its redirect policy included: the guard
	// belongs to the default client only.
	HTTPClient *http.Client
	// DeviceTimeout bounds how long WaitForDeviceLogin polls before
	// returning ErrDeviceCodeExpired. Default: 15 minutes (research §B).
	DeviceTimeout time.Duration
	// UserAgent is sent as every request's User-Agent header. Default:
	// UserAgent().
	UserAgent string
}

AuthClient signs a user in to a ChatGPT subscription via the device-code flow. Construct one with NewAuthClient, which fills in every field's documented default; the zero value is not meant to be used directly.

func NewAuthClient

func NewAuthClient() *AuthClient

NewAuthClient returns an AuthClient with its documented defaults.

func (*AuthClient) Refresh

func (c *AuthClient) Refresh(ctx context.Context, refreshToken string) (Tokens, error)

Refresh spends refreshToken at {issuer}/oauth/token for a new token set (research §C). Unlike the form-encoded authorization-code exchange, the refresh grant is a JSON body. The result holds only the fields the server returned: a response without a refresh_token means the server did not rotate it, and the caller keeps its old one. Every failure is a *RefreshError.

func (*AuthClient) StartDeviceLogin

func (c *AuthClient) StartDeviceLogin(ctx context.Context) (DeviceCode, error)

StartDeviceLogin requests a user code from the server (research §B step 1). ErrDeviceCodeDisabled reports a 404 — device-code sign-in not enabled for this server.

func (*AuthClient) WaitForDeviceLogin

func (c *AuthClient) WaitForDeviceLogin(ctx context.Context, dc DeviceCode) (Tokens, error)

WaitForDeviceLogin polls until the user finishes sign-in in the browser, then exchanges the server-issued code for tokens (research §B steps 2-3). It returns ErrDeviceCodeExpired once dc.ExpiresAt passes, or ctx.Err() promptly on cancellation.

type DeviceCode

type DeviceCode struct {
	// VerificationURL is where the user enters UserCode: Issuer + "/codex/device".
	VerificationURL string
	// UserCode is the one-time code to show the user.
	UserCode string
	// Interval is how long to wait between polls. A caller that needs a
	// faster poll (tests) may lower it after StartDeviceLogin returns.
	Interval time.Duration
	// ExpiresAt is when the code stops being valid: start + DeviceTimeout.
	ExpiresAt time.Time
	// contains filtered or unexported fields
}

DeviceCode is a pending device-code sign-in: the code and link to show the user, and the state WaitForDeviceLogin needs to poll for completion.

type Identity

type Identity struct{ AccountID, PlanType string }

Identity is the ChatGPT account identity carried in an id_token's namespaced auth claim.

func ParseIDToken

func ParseIDToken(idToken string) (Identity, error)

ParseIDToken decodes idToken's claims and reads the account ID and plan type from its "https://api.openai.com/auth" claim. It does not check the token's signature, matching Codex's own id_token handling.

type Mode

type Mode string

Mode selects how Tracker authenticates calls to the openai provider.

const (
	// ModeAPIKey sends calls with OPENAI_API_KEY. It is the default.
	ModeAPIKey Mode = "api-key"
	// ModeChatGPT bills calls to a signed-in ChatGPT subscription instead.
	ModeChatGPT Mode = "chatgpt"
)

func ModeFromEnv

func ModeFromEnv() (Mode, error)

ModeFromEnv reads and parses EnvVar from the process environment.

func ParseMode

func ParseMode(v string) (Mode, error)

ParseMode trims v and validates it against the two known modes. An empty string — TRACKER_OPENAI_AUTH unset — means ModeAPIKey, today's behavior. Any other value is a configuration error naming both valid values.

type RefreshError

type RefreshError struct {
	// Code is the server's error code when it is one this package
	// recognizes (refresh_token_expired, refresh_token_reused,
	// refresh_token_invalidated or invalid_grant), lowercased; "" for none
	// or any other. Unrecognized codes are dropped so that no text the
	// server chose ever reaches an error string.
	Code string
	// Status is the HTTP status, or 0 when no response arrived.
	Status int
	// Permanent is true for any 401, for one of the three refresh-token
	// codes, and for a 400 with invalid_grant.
	Permanent bool
	// contains filtered or unexported fields
}

RefreshError reports a failed refresh-token exchange. Permanent means the sign-in itself is dead — the refresh token expired, was already used, or was revoked — and only signing in again fixes it; anything else (network failures included) may succeed on a retry.

func (*RefreshError) Error

func (e *RefreshError) Error() string

Error sends the user to `tracker setup` for a permanent failure, naming the code (or the status when there is none); a transient one names the status or the underlying failure.

func (*RefreshError) Temporary

func (e *RefreshError) Temporary() bool

Temporary reports whether a retry may succeed: true unless Permanent.

func (*RefreshError) Unwrap

func (e *RefreshError) Unwrap() error

Unwrap returns the transport or decode failure behind the error, if any, so errors.Is still finds a context cancellation.

type Status

type Status struct {
	SignedIn                  bool
	PlanType                  string
	LastRefresh, AccessExpiry time.Time
}

Status is a point-in-time summary of the ChatGPT sign-in, for `tracker setup`, `tracker doctor` and `tracker version` to display.

func ReadStatus

func ReadStatus(path string) (Status, error)

ReadStatus loads the token file at path and summarizes it. A missing file (ErrNotSignedIn) is not an error here — it is the ordinary not-signed-in state — and reports as a zero Status. Any other Load failure (an unsafe file, an incomplete sign-in) is returned as an error so the caller surfaces the real problem instead of silently reporting "not signed in".

type TokenSource

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

TokenSource hands out the access token and account ID from a token file, refreshing first when the token is due. ChatGPT refresh tokens are single-use and rotate, so exactly one caller may spend one: mu serializes callers inside this process, and on Unix a flock on LockFileName beside the token file serializes processes (lockFile is a no-op on Windows). Whoever takes both locks re-reads the file first and adopts a token another holder already refreshed instead of spending the same refresh token again. After a permanent failure (*RefreshError with Permanent set), every refresh returns that same error without a network call until the token file's refresh token changes — the user signed in again.

func NewTokenSource

func NewTokenSource(path string, client *AuthClient) *TokenSource

NewTokenSource returns a TokenSource for the token file at path whose refreshes go through client. It reads nothing until the first call.

func (*TokenSource) ForceRefresh

func (s *TokenSource) ForceRefresh(ctx context.Context, rejected string) error

ForceRefresh replaces rejected, an access token the backend refused, with a new one. If the token file already holds a different, fresh token — another caller or process refreshed first — it adopts that token without a network call.

func (*TokenSource) Token

func (s *TokenSource) Token(ctx context.Context) (accessToken, accountID string, err error)

Token returns the current access token and account ID, refreshing first when the token is due — so a token that expired while the machine slept is refreshed before its first use, not after a 401.

type Tokens

type Tokens struct {
	IDToken      string    `json:"id_token"`
	AccessToken  string    `json:"access_token"`
	RefreshToken string    `json:"refresh_token"`
	AccountID    string    `json:"account_id"`
	LastRefresh  time.Time `json:"last_refresh"`
}

Tokens is the ChatGPT OAuth token set persisted to the token file.

func Load

func Load(path string) (Tokens, error)

Load reads and validates the token file at path.

A missing file is ErrNotSignedIn. A symlink, or (on Unix) a mode with any group or other bit set, is a *UnsafeFileError — checkFileSafe (store_unix.go / store_windows.go) decides this before the content is ever read. A file that parses but is missing its access token, refresh token or account ID is an incomplete sign-in: the error names the problem but never repeats any token value.

type UnsafeFileError

type UnsafeFileError struct{ Path, Problem string }

UnsafeFileError reports that Load refused to read the token file because its symlink status or permission bits could expose it to another account on the same machine.

func (*UnsafeFileError) Error

func (e *UnsafeFileError) Error() string

Error picks its fix hint by the kind of Problem: a symlink is fixed by replacing it with a regular file, loose mode bits by chmod.

Jump to

Keyboard shortcuts

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