Documentation
¶
Index ¶
- Constants
- Variables
- func ActiveEmail() string
- func BuildAuthURL(cfg Config, p BuildAuthURLParams) (string, error)
- func DeleteForEmail(s secure.Storage, email string) error
- func GenerateState() (string, error)
- func GenerateVerifier() (string, error)
- func S256Challenge(verifier string) string
- func SaveAccountStore(s AccountStore) error
- func SaveForEmail(s secure.Storage, p PersistedTokens, email string) error
- func SetActive(store *AccountStore, email string) error
- type AccountEntry
- type AccountStore
- type BrowserOpener
- type BuildAuthURLParams
- type CallbackListener
- func (l *CallbackListener) Close() error
- func (l *CallbackListener) HasPendingResponse() bool
- func (l *CallbackListener) Port() int
- func (l *CallbackListener) Register(expectedState string) error
- func (l *CallbackListener) SendErrorRedirect(url string)
- func (l *CallbackListener) SendSuccessRedirect(url string)
- func (l *CallbackListener) SubmitManualCode(state, code string) error
- func (l *CallbackListener) Wait(ctx context.Context, expectedState string) (string, error)
- type Config
- type ExchangeParams
- type LoginDisplay
- type LoginFlow
- type LoginOptions
- type PersistedTokens
- type RefreshOptions
- type SystemBrowser
- type TokenClient
- func (c *TokenClient) CreateAPIKey(ctx context.Context, accessToken string) (string, error)
- func (c *TokenClient) ExchangeCodeForTokens(ctx context.Context, p ExchangeParams) (Tokens, error)
- func (c *TokenClient) RefreshOAuthToken(ctx context.Context, refreshToken string, opt RefreshOptions) (Tokens, error)
- type Tokens
Constants ¶
const ( ScopeUserInference = "user:inference" ScopeUserProfile = "user:profile" ScopeOrgCreateAPIKey = "org:create_api_key" ScopeUserSessionsCC = "user:sessions:claude_code" ScopeUserMCPServers = "user:mcp_servers" ScopeUserFileUpload = "user:file_upload" )
Scope literals (decoded/0532.js:119-121).
const PersistKey = "oauth-tokens"
PersistKey is the base keychain item name prefix for OAuth tokens. Per-account entries use "oauth-tokens-<email>".
const Service = "com.icehunter.conduit"
Service is the keyring service identifier for conduit.
Variables ¶
var ErrNotLoggedIn = errors.New("auth: not logged in")
ErrNotLoggedIn is returned when no token exists for the requested account.
var ProdConfig = Config{
BaseAPIURL: "https://api.anthropic.com",
ConsoleAuthorizeURL: "https://platform.claude.com/oauth/authorize",
ClaudeAIAuthorizeURL: "https://claude.ai/oauth/authorize",
ClaudeAIOrigin: "https://claude.ai",
TokenURL: "https://platform.claude.com/v1/oauth/token",
APIKeyURL: "https://api.anthropic.com/api/oauth/claude_cli/create_api_key",
RolesURL: "https://api.anthropic.com/api/oauth/claude_cli/roles",
ConsoleSuccessURL: "https://platform.claude.com/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code",
ClaudeAISuccessURL: "https://platform.claude.com/oauth/code/success?app=claude-code",
ManualRedirectURL: "https://platform.claude.com/oauth/code/callback",
ClientID: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
MCPProxyURL: "https://mcp-proxy.anthropic.com",
MCPProxyPath: "/v1/mcp/{server_id}",
}
ProdConfig is the production OAuth configuration shipping in Claude Code v2.1.126. The client_id is a public identifier (not a secret).
Note on ClaudeAIAuthorizeURL: the decoded reference (decoded/0533.js:10) uses "https://claude.com/cai/oauth/authorize", which is meant to 307 to claude.ai/oauth/authorize for attribution. Empirically that bounce rejects requests with the same query string we send, returning "Invalid request format". We point straight at the terminal claude.ai URL — same client_id, same accepted query, no attribution bounce.
var ScopesAll = []string{ ScopeOrgCreateAPIKey, ScopeUserProfile, ScopeUserInference, ScopeUserSessionsCC, ScopeUserMCPServers, ScopeUserFileUpload, }
ScopesAll is the de-duped union of console + claude.ai scopes (decoded/0533.js:6: `ZV6 = unique([...N_q, ...qv_])`).
var ScopesClaudeAI = []string{ ScopeUserProfile, ScopeUserInference, ScopeUserSessionsCC, ScopeUserMCPServers, ScopeUserFileUpload, }
ScopesClaudeAI is the Claude.ai subscriber scope set (decoded/0533.js:5: `qv_ = [LJH, VS, "user:sessions:claude_code", "user:mcp_servers", "user:file_upload"]`).
var ScopesConsole = []string{ScopeOrgCreateAPIKey, ScopeUserProfile}
ScopesConsole is the Console OAuth scope set, used when minting an API key via the Console flow (decoded/0533.js:4: `N_q = [SA4, LJH]`).
var ScopesInferenceOnly = []string{ScopeUserInference}
ScopesInferenceOnly is requested for long-lived inference-only tokens (decoded/1220.js:52: `inferenceOnly ? [VS] : ZV6`).
Functions ¶
func ActiveEmail ¶
func ActiveEmail() string
ActiveEmail returns the currently active account email. If active is unset but exactly one account exists, that account is auto-selected and persisted so subsequent calls are consistent. Returns "" only when no accounts are registered.
func BuildAuthURL ¶
func BuildAuthURL(cfg Config, p BuildAuthURLParams) (string, error)
BuildAuthURL constructs the OAuth authorize URL exactly as Claude Code v2.1.126's GI_ function does. Param insertion order is preserved (Go's `url.Values.Encode` would sort alphabetically, which would break the fixture comparison), and values are URL-escaped using the same application/x-www-form-urlencoded scheme JS's URLSearchParams uses (space -> "+", reserved chars percent-encoded).
Reference: decoded/1220.js:32-64.
func DeleteForEmail ¶
DeleteForEmail removes an account's token and accounts.json entry.
func GenerateState ¶
GenerateState returns a fresh OAuth state nonce. Output is base64url- without-padding so it's safe to drop into a query string verbatim.
func GenerateVerifier ¶
GenerateVerifier returns a fresh PKCE code verifier per RFC 7636 §4.1.
Output is base64url-without-padding, which is a strict subset of the allowed unreserved character set (ALPHA / DIGIT / "-" / "." / "_" / "~"). Length is fixed at 43 — between the RFC's [43,128] range, matching the reference implementation's 32-byte draw.
func S256Challenge ¶
S256Challenge returns the BASE64URL(SHA256(verifier)) challenge for the given verifier, per RFC 7636 §4.2. Padding is omitted.
func SaveAccountStore ¶
func SaveAccountStore(s AccountStore) error
SaveAccountStore writes the account store to ~/.claude/accounts.json. Exported so the TUI account panel can remove accounts without a full delete.
func SaveForEmail ¶
func SaveForEmail(s secure.Storage, p PersistedTokens, email string) error
SaveForEmail writes the token to the platform keychain under the email- scoped key and registers the account in accounts.json as active.
func SetActive ¶
func SetActive(store *AccountStore, email string) error
SetActive switches the active account. Requires that the email already has a saved token (from a prior /login).
Types ¶
type AccountEntry ¶
AccountEntry holds metadata for one stored account.
type AccountStore ¶
type AccountStore struct {
Active string `json:"active"`
Accounts map[string]AccountEntry `json:"accounts"`
}
AccountStore is the shape of ~/.claude/accounts.json.
func ListAccounts ¶
func ListAccounts() (AccountStore, error)
ListAccounts returns all registered accounts.
func LoadAccountStore ¶
func LoadAccountStore() (AccountStore, error)
LoadAccountStore reads ~/.claude/accounts.json. Returns an empty store if the file doesn't exist yet.
type BrowserOpener ¶
BrowserOpener opens an external URL in the user's web browser. Pass a nil opener for headless environments — the user copy-pastes.
type BuildAuthURLParams ¶
type BuildAuthURLParams struct {
// CodeChallenge is the BASE64URL(SHA256(verifier)) string.
CodeChallenge string
// State is the OAuth state nonce echoed back at the callback.
State string
// Port is the local listener port for the automatic flow. Ignored when
// IsManual is true.
Port int
// IsManual selects the `MANUAL_REDIRECT_URL` redirect_uri (the user pastes
// the code) instead of `http://localhost:<port>/callback`.
IsManual bool
// LoginWithClaudeAI selects `ClaudeAIAuthorizeURL` over
// `ConsoleAuthorizeURL`. Use true for Max/Pro/Team/Enterprise; false for
// the Console (API key) flow.
LoginWithClaudeAI bool
// InferenceOnly requests the long-lived inference-only scope set
// (`user:inference` only). Used by SDK callers like Cowork.
InferenceOnly bool
// OrgUUID, LoginHint, LoginMethod are optional pre-fill / routing hints.
OrgUUID string
LoginHint string
LoginMethod string
}
BuildAuthURLParams gathers every input the reference's GI_ function takes (decoded/1220.js:32). All fields except CodeChallenge and State are optional in some combination — see the field docs.
type CallbackListener ¶
type CallbackListener struct {
// contains filtered or unexported fields
}
CallbackListener is a temporary localhost HTTP server that receives the OAuth provider's redirect, parses ?code & ?state, validates state for CSRF protection, and delivers the authorization code to a single Wait() caller.
It also supports a manual-paste fallback: when the user can't reach the browser-redirected callback (e.g. on a remote machine), they paste the code into the CLI which calls SubmitManualCode.
Reference: src/services/oauth/auth-code-listener.ts (TS), behavior matched for our tests.
func NewCallbackListener ¶
func NewCallbackListener(callbackPath string) (*CallbackListener, error)
NewCallbackListener binds an OS-assigned localhost port and starts serving. The returned listener is ready to receive a redirect immediately.
func (*CallbackListener) Close ¶
func (l *CallbackListener) Close() error
Close shuts down the HTTP server. Safe to call multiple times.
func (*CallbackListener) HasPendingResponse ¶
func (l *CallbackListener) HasPendingResponse() bool
HasPendingResponse reports whether the listener has parked an HTTP response that's still waiting for SendSuccessRedirect or SendErrorRedirect.
func (*CallbackListener) Port ¶
func (l *CallbackListener) Port() int
Port returns the OS-assigned localhost port.
func (*CallbackListener) Register ¶
func (l *CallbackListener) Register(expectedState string) error
Register sets the expected state nonce. It must be called before any callback request can be matched (incoming requests with a different state are rejected) and before SubmitManualCode. Calling Wait without a prior Register implicitly registers using the state argument.
func (*CallbackListener) SendErrorRedirect ¶
func (l *CallbackListener) SendErrorRedirect(url string)
SendErrorRedirect 302s the parked response to the given URL (typically the same success page, optionally with an error indicator).
func (*CallbackListener) SendSuccessRedirect ¶
func (l *CallbackListener) SendSuccessRedirect(url string)
SendSuccessRedirect 302s the parked response to the given URL. No-op if there is no pending response.
func (*CallbackListener) SubmitManualCode ¶
func (l *CallbackListener) SubmitManualCode(state, code string) error
SubmitManualCode delivers a manually pasted authorization code to the pending Wait. State must match the expectedState given to Wait.
func (*CallbackListener) Wait ¶
Wait blocks until the authorization code is delivered (via redirect or manual paste) or the context is cancelled. It implicitly Register()s the expected state if not already set.
Wait must be called at most once per CallbackListener; a second call returns errAlreadyWaited immediately rather than blocking forever.
type Config ¶
type Config struct {
BaseAPIURL string
ConsoleAuthorizeURL string
ClaudeAIAuthorizeURL string
ClaudeAIOrigin string
TokenURL string
APIKeyURL string
RolesURL string
ConsoleSuccessURL string
ClaudeAISuccessURL string
ManualRedirectURL string
ClientID string
MCPProxyURL string
MCPProxyPath string
}
Config holds the OAuth endpoint URLs and client metadata.
Values come from decoded/0533.js (the prod block of v_q). The leaked TS source allows USER_TYPE=ant + USE_STAGING_OAUTH to swap these out; we don't replicate that — Anthropic-internal staging is out of scope (see plan §"Out of scope").
type ExchangeParams ¶
type ExchangeParams struct {
Code string
State string
CodeVerifier string
Port int
UseManual bool
// ExpiresIn is optional. When > 0 it's sent as `expires_in` in the
// request body (decoded/1220.js:74). Used by inference-only flows.
ExpiresIn int
}
ExchangeParams is the input to ExchangeCodeForTokens. UseManual selects MANUAL_REDIRECT_URL over http://localhost:<Port>/callback, matching decoded/1220.js:65 (function rm6) parameter O.
type LoginDisplay ¶
type LoginDisplay interface {
Show(automaticURL, manualURL string)
// BrowserOpenFailed is called with a non-nil error when the system
// browser launcher returned an error. Implementations should display
// a hint that the user must open the URL manually instead.
BrowserOpenFailed(err error)
}
LoginDisplay handles user-facing presentation of the OAuth URLs. AutomaticURL may be empty when only the manual flow is shown.
type LoginFlow ¶
type LoginFlow struct {
Cfg Config
Tokens *TokenClient
Browser BrowserOpener // optional; stdout-only when nil
// Display is called with both the manual auth URL and (when not nil) the
// automatic URL. It is responsible for showing them to the user. When
// the manual flow is desired, Display can also block on user paste.
Display LoginDisplay
}
LoginFlow ties PKCE generation, the localhost callback listener, and the token exchange into one orchestrator. It is the function `claude login` invokes after deciding which authorization URL to open.
func (*LoginFlow) Login ¶
Login runs the full PKCE OAuth flow against the configured endpoints.
Steps:
- Generate verifier + challenge + state.
- Spin up a localhost callback listener on an OS-assigned port.
- Build manual + automatic auth URLs and present them.
- Open the automatic URL in the browser (unless suppressed).
- Wait for the redirect (or manual paste) and exchange the code.
- Send the success redirect to close out the user's browser tab.
Returns the issued Tokens. The caller is responsible for persisting them.
type LoginOptions ¶
type LoginOptions struct {
// LoginWithClaudeAI selects the Claude.ai (Max/Pro/Team) authorize URL
// rather than the Console URL. Set true for subscription users.
LoginWithClaudeAI bool
// InferenceOnly requests the long-lived `user:inference`-only scope set.
InferenceOnly bool
// OrgUUID, LoginHint, LoginMethod are optional pre-fill / routing hints.
OrgUUID string
LoginHint string
LoginMethod string
// SkipBrowserOpen suppresses BrowserOpener.Open and only shows URLs via
// Display. Used by the SDK control protocol where the SDK client owns
// the user's display.
SkipBrowserOpen bool
// Timeout bounds how long Login will wait for the user to authorize.
// Zero defaults to 5 minutes.
Timeout time.Duration
}
LoginOptions controls flow variants.
type PersistedTokens ¶
type PersistedTokens struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt time.Time `json:"expires_at"`
TokenType string `json:"token_type"`
Scopes []string `json:"scopes"`
APIKey string `json:"api_key,omitempty"`
}
PersistedTokens is the stored token shape: network tokens plus a pre-computed absolute expiry so we don't call the server just to check.
func EnsureFresh ¶
func EnsureFresh(ctx context.Context, s secure.Storage, c *TokenClient, email string, now time.Time, skew time.Duration) (PersistedTokens, error)
EnsureFresh loads the token for email and refreshes it if within skew of expiry. Saves the refreshed token back to the keychain on success.
func FromTokens ¶
func FromTokens(tok Tokens, now time.Time) PersistedTokens
FromTokens converts network Tokens to PersistedTokens with an absolute expiry.
func LoadForEmail ¶
func LoadForEmail(s secure.Storage, email string) (PersistedTokens, error)
LoadForEmail loads and decodes the token for the given email.
func (PersistedTokens) String ¶
func (PersistedTokens) String() string
type RefreshOptions ¶
type RefreshOptions struct {
// Scopes overrides the default ScopesClaudeAI. Pass nil for default.
Scopes []string
// ExpiresIn, when > 0, requests a specific token lifetime.
ExpiresIn int
// ClientID overrides the configured client_id. Used for some SDK paths
// (decoded/1220.js:91: `K ?? Aq().CLIENT_ID`).
ClientID string
}
RefreshOptions matches decoded/1220.js:87 ({scopes, expiresIn, clientId}).
type SystemBrowser ¶
type SystemBrowser struct{}
SystemBrowser opens URLs in the OS default browser using the platform's standard "open this in the user's default app" command.
macOS: /usr/bin/open Linux: xdg-open (provided by xdg-utils on most distros) Windows: rundll32 url.dll,FileProtocolHandler
SystemBrowser returns a non-nil error if the launcher fails immediately; callers should treat the error as advisory (the manual paste flow is always available as a fallback).
func (SystemBrowser) Open ¶
func (SystemBrowser) Open(url string) error
Open implements BrowserOpener.
We Start() the launcher and immediately Release() the OS process so we don't carry a tracked child or a goroutine waiting for it. The launchers (open / xdg-open / rundll32) reparent the actual browser to PID 1, so abandoning them here is safe: the user's browser stays open even if our process exits seconds later.
type TokenClient ¶
type TokenClient struct {
// contains filtered or unexported fields
}
TokenClient performs the OAuth code exchange and refresh round-trips.
It is a thin wrapper around an *http.Client that knows the wire shape of platform.claude.com/v1/oauth/token. Higher-level retry, persistence, and profile fetching live in callers.
func NewTokenClient ¶
func NewTokenClient(cfg Config, httpClient *http.Client) *TokenClient
NewTokenClient returns a TokenClient. If httpClient is nil, http.DefaultClient is used.
func (*TokenClient) CreateAPIKey ¶
CreateAPIKey trades an OAuth access token for a long-lived `sk-ant-oat01-…` API key. The real CLI does this once per session and uses the resulting key as the bearer for /v1/messages — using the OAuth access token directly causes the API to reject the request as a non-Claude-Code client.
Reference: decoded/1220.js:175-195 (function am6), src/services/oauth/client.ts createAndStoreApiKey.
func (*TokenClient) ExchangeCodeForTokens ¶
func (c *TokenClient) ExchangeCodeForTokens(ctx context.Context, p ExchangeParams) (Tokens, error)
ExchangeCodeForTokens trades an OAuth authorization code for tokens. Reference: decoded/1220.js:65-86 (function rm6).
func (*TokenClient) RefreshOAuthToken ¶
func (c *TokenClient) RefreshOAuthToken(ctx context.Context, refreshToken string, opt RefreshOptions) (Tokens, error)
RefreshOAuthToken trades a refresh_token for a new access (and optionally new refresh) token. Reference: decoded/1220.js:87-100 (function r__).
type Tokens ¶
type Tokens struct {
AccessToken string
RefreshToken string
ExpiresIn int
TokenType string
Scopes []string
// Email is populated from account.email_address in the token response,
// when the server includes it. Used for multi-account keychain key scoping.
Email string
}
Tokens is the application-level OAuth token bundle.
Field semantics mirror the reference (decoded/1220.js): RefreshToken is preserved across refreshes if the server omits it; ExpiresIn is the raw `expires_in` seconds value (the absolute expiry timestamp is computed by the caller using a clock interface, so we don't bake `time.Now()` into pure data).