auth

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: May 4, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Index

Constants

View Source
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).

View Source
const PersistKey = "oauth-tokens"

PersistKey is the base keychain item name prefix for OAuth tokens. Per-account entries use "oauth-tokens-<email>".

View Source
const Service = "com.icehunter.conduit"

Service is the keyring service identifier for conduit.

Variables

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

ErrNotLoggedIn is returned when no token exists for the requested account.

View Source
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.

ScopesAll is the de-duped union of console + claude.ai scopes (decoded/0533.js:6: `ZV6 = unique([...N_q, ...qv_])`).

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"]`).

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]`).

View Source
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

func DeleteForEmail(s secure.Storage, email string) error

DeleteForEmail removes an account's token and accounts.json entry.

func GenerateState

func GenerateState() (string, error)

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

func GenerateVerifier() (string, error)

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

func S256Challenge(verifier string) string

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

type AccountEntry struct {
	Email   string    `json:"email"`
	AddedAt time.Time `json:"added_at"`
}

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

type BrowserOpener interface {
	Open(url string) error
}

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

func (l *CallbackListener) Wait(ctx context.Context, expectedState string) (string, error)

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

func (f *LoginFlow) Login(ctx context.Context, opt LoginOptions) (Tokens, error)

Login runs the full PKCE OAuth flow against the configured endpoints.

Steps:

  1. Generate verifier + challenge + state.
  2. Spin up a localhost callback listener on an OS-assigned port.
  3. Build manual + automatic auth URLs and present them.
  4. Open the automatic URL in the browser (unless suppressed).
  5. Wait for the redirect (or manual paste) and exchange the code.
  6. 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

func (c *TokenClient) CreateAPIKey(ctx context.Context, accessToken string) (string, error)

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).

func (Tokens) String

func (Tokens) String() string

String redacts tokens to keep them out of logs by accident.

Jump to

Keyboard shortcuts

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