claudecodeauth

package
v0.0.6 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package claudecodeauth wraps the `claude auth` subcommand so the Web UI can drive Claude Code login/logout/status from the browser. The login flow is inherently interactive: `claude auth login` prints a one-shot OAuth URL and then blocks on stdin reading a paste-back authorization code. We model that as a server-side session: Start spawns the CLI, captures the URL, and keeps the process handle; SubmitCode writes the code to stdin and waits for the process to exit; Cancel/Stop kills it.

Every operation is scoped to a Claude CLI configuration directory (see identity.go), so an OIDC hub can give each signed-in user their own login, credential and session history. An empty configDir means "use the host default", which is the single-user behaviour this package started with.

Login sessions are keyed the same way, because a hub has as many concurrent operators as it has users: one global in-flight session would mean the second person to click Login silently kills the first person's OAuth flow.

Package claudecodeauth - identity.go allocates one private Claude CLI configuration directory per hub user, which is what makes per-user logins possible at all.

Motivation (Task 20241): `claude auth login` writes its OAuth credential to a single well-known location derived from the process's HOME. On a single-user desktop that is exactly right. On an OIDC hub it means every signed-in user shares one Claude account: whoever logged in last owns the credential, their subscription is billed for everybody's tasks, and the next person to log in silently evicts them. `claude auth logout` logs the whole deployment out.

The CLI reads CLAUDE_CONFIG_DIR in preference to HOME and keeps *all* of its per-account state there — credentials, session transcripts, project history — so pointing each identity at its own directory isolates not just the token but the conversation history that would otherwise leak across tenants.

These directories live under the hub's global config directory and deliberately NOT under any project tree: a task runs an agent inside the project working directory, so a credential stored there would be readable by the very code it is meant to be isolated from.

Index

Constants

This section is empty.

Variables

View Source
var AmbientTokenVars = []string{
	"CLAUDE_CODE_OAUTH_TOKEN",
	"ANTHROPIC_API_KEY",
	"ANTHROPIC_AUTH_TOKEN",
}

AmbientTokenVars are the environment variables that hand the Claude CLI a credential without it ever consulting its configuration directory.

They must be cleared whenever a per-identity configDir is in force. cloop itself populates CLAUDE_CODE_OAUTH_TOKEN from ~/.openclaw/workspace/.env and ~/.env (see pkg/provider/claudecode.loadEnvFiles), and an ambient token beats an empty config directory: measured against the real CLI, a user who has never logged in reports `loggedIn: true, authMethod: oauth_token` and their prompts run on the host's account. Per-user isolation would then be an illusion — every tenant silently sharing one subscription — which is precisely the failure this package exists to prevent.

View Source
var ClaudeOnlyTokenVars = []string{"CLAUDE_CODE_OAUTH_TOKEN"}

ClaudeOnlyTokenVars is the subset of AmbientTokenVars that nothing except the Claude Code path consumes.

The distinction matters because the two environments this package scopes are not the same. A `claude` subprocess may have every ambient credential cleared, since that environment serves one program. A dispatched cloop harness may not: ANTHROPIC_API_KEY is also how pkg/provider/anthropic finds its key, so blanking it there would break a project using the `anthropic` provider on an OIDC hub — a self-inflicted outage in the name of isolating a provider that project is not even using.

View Source
var ErrCodeFromAnotherLogin = errors.New(
	"this authorization code is from a different sign-in attempt: the sign-in was restarted after that link was opened, " +
		"which replaced the one-time code it was issued for. Open the link shown above again, authorize, and paste the new code")

ErrCodeFromAnotherLogin reports a paste whose `state` belongs to a different sign-in attempt than the session that would receive it. Exchanging it is guaranteed to fail, because the PKCE verifier that would be sent with it belongs to this session and the code was minted against another one's challenge.

This is the whole reason the check exists: the CLI cannot make it. Its readline handler splits the paste into `authorizationCode` and `state` and then calls handleManualAuthCodeInput, which uses only the code and discards the state — so a stale code is exchanged against the live session's verifier and the token endpoint answers 400. The CLI surfaces that verbatim as "Login failed: Request failed with status code 400", which names neither the cause nor a remedy. The hub is the only layer holding both states at once.

View Source
var ErrCodeMalformed = errors.New("that does not look like an authorization code")

ErrCodeMalformed reports a paste that is not an authorization code at all.

View Source
var ErrNoIdentity = errors.New("claudecodeauth: no identity")

ErrNoIdentity is returned when a per-user home is requested for an empty owner key. Callers treat it as "use the host default", which is the pre-OIDC behaviour and the correct answer for a single-user deployment.

Functions

func CredentialsPath added in v0.0.2

func CredentialsPath(configDir string) string

CredentialsPath returns where the CLI stores its OAuth credential inside a config directory. Exported so the usage/refresh path in pkg/ratelimit and the tests agree on one definition.

func ForgetHome added in v0.0.2

func ForgetHome(ownerKey string) error

ForgetHome removes an identity's Claude configuration directory entirely, destroying the stored credential along with the CLI's cached session transcripts.

This is the deprovisioning path: `claude auth logout` revokes the session but leaves the directory populated, and a hub that keeps an ex-employee's refresh token on disk has not really removed them. pkg/offboard calls it for every spelling of a departed identity, on every hub member (Task 20400).

It creates nothing on the way: a hub that never kept a per-user home — including one whose config directory is read-only — has nothing to forget, and reports so as success rather than as a failure to create the root.

func ForgetHomeIn added in v0.0.5

func ForgetHomeIn(root, ownerKey string) error

ForgetHomeIn is ForgetHome under an explicit root.

func HasCredential added in v0.0.2

func HasCredential(configDir string) bool

HasCredential reports whether a config directory holds a non-empty credentials file. Used to answer "is this user logged in?" without paying for a CLI subprocess.

func HomeFor added in v0.0.2

func HomeFor(ownerKey string) (string, error)

HomeFor returns the private Claude CLI configuration directory for one identity, creating it 0700 if absent. The returned path is what callers place in CLAUDE_CONFIG_DIR.

An empty owner key yields ErrNoIdentity rather than a shared fallback directory: "no identity" must mean "use the host default", never "use the directory that every anonymous caller shares", which would recreate the pooled-credential problem this package exists to remove.

func HomePath added in v0.0.5

func HomePath(ownerKey string) (string, error)

HomePath returns where an identity's Claude config directory is, or would be, without creating it. Used where looking must not leave a trace: an offboarding's dry run, and the destruction that follows it, which must not first create the directory it is about to remove (Task 20400).

func HomePathIn added in v0.0.5

func HomePathIn(root, ownerKey string) (string, error)

HomePathIn is HomePath under an explicit root rather than this process's — another member's tree, as offboarding names it.

func HomeRoot added in v0.0.2

func HomeRoot() (string, error)

HomeRoot returns the directory holding every identity's Claude config directory, creating it 0700 if absent.

func HomeRootPath added in v0.0.5

func HomeRootPath() (string, error)

HomeRootPath returns where this process keeps every identity's Claude config directory, without creating anything.

Each hub process resolves it from its own environment, so two processes serving one control plane under different users, XDG_CONFIG_HOME values or container filesystems keep two different trees — which is why offboarding asks every hub member to forget a home rather than forgetting one copy (Task 20400).

func IdentitySlug added in v0.0.2

func IdentitySlug(ownerKey string) string

IdentitySlug maps an identity's owner key onto a stable, filesystem-safe directory name.

The owner key is an email address or "sub:<subject>" — both attacker- influenced (an IdP subject is whatever the issuer says it is) and neither safe as a path component: they can contain "/" and ".." , differ only by case on a case-insensitive filesystem, or exceed NAME_MAX. Hashing sidesteps every one of those without needing to enumerate them. 128 bits of the digest is far beyond collision range for a user directory.

The mapping is deliberately deterministic and unsalted so an operator can recompute it: sha256(owner_key) truncated to 32 hex characters.

func Logout

func Logout(ctx context.Context, configDir string) error

Logout invokes `claude auth logout`. The CLI is non-interactive in this path so a 10-second timeout is sufficient. configDir scopes the logout to one identity, so a user signing out of the hub does not sign out every other tenant.

func ScopeEnv added in v0.0.2

func ScopeEnv(env []string, configDir string) []string

ScopeEnv returns env (in "K=V" form) rewritten so the Claude CLI resolves its credential from configDir and nowhere else. Use it for a `claude` subprocess, where clearing every ambient credential is free of collateral damage.

When configDir is empty the environment is returned untouched: a deployment with no OIDC keeps using whatever credential the host is configured with, ambient tokens included.

Clearing is done by appending an empty assignment rather than by dropping the variable, so the result is correct whether the caller hands it to exec (last assignment wins) or scans it for a value.

func ScopeHarnessEnv added in v0.0.2

func ScopeHarnessEnv(env []string, configDir string) []string

ScopeHarnessEnv pins a dispatched cloop harness to one identity's Claude configuration directory, clearing only the variables that belong to the Claude Code path. Other providers' credentials pass through untouched — see ClaudeOnlyTokenVars.

The harness's own claudecode provider re-scopes with the stricter ScopeEnv when it finally spawns the CLI, so the remaining ambient credentials never reach the `claude` binary regardless.

Types

type LoginOptions

type LoginOptions struct {
	// Console switches the OAuth target to api.anthropic.com (console billing)
	// instead of claude.ai subscription auth.
	Console bool
	// Email pre-populates the login page's email field.
	Email string
	// SSO forces the SSO login flow.
	SSO bool
}

LoginOptions controls how `claude auth login` is invoked. The defaults match the CLI's default flow (Claude subscription, no email pre-fill).

type Manager

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

Manager owns the in-flight login sessions, keyed by identity. Methods are safe for concurrent use by multiple HTTP handlers.

The key is the caller's identity (an OwnerKey) so that two users logging in at the same time do not evict each other, and so one user's pasted code can never be delivered to another user's OAuth flow. An empty key is the single-user/host session.

func NewManager

func NewManager() *Manager

NewManager returns a fresh login session manager.

func (*Manager) Cancel

func (m *Manager) Cancel(key string)

Cancel kills one identity's login session (if any) and clears the manager's reference so a fresh Start can begin. Other identities are unaffected.

func (*Manager) Shutdown added in v0.0.2

func (m *Manager) Shutdown()

Shutdown kills every in-flight login session. Called when the hub is stopping so parked `claude auth login` children do not outlive it.

func (*Manager) Snapshot

func (m *Manager) Snapshot(key string) State

Snapshot returns one identity's session state, or an inactive zero State if that identity has no session in flight.

func (*Manager) Start

func (m *Manager) Start(ctx context.Context, key, configDir string, opts LoginOptions) (*Session, error)

Start spawns `claude auth login` scoped to configDir and waits for it to emit the OAuth URL, returning a Session that the caller can later complete by passing the authorization code to SubmitCode. If the same identity already has a session in flight it is killed first so the new flow can take over; other identities' sessions are left alone.

func (*Manager) SubmitCode

func (m *Manager) SubmitCode(key, code string) (State, error)

SubmitCode pipes the OAuth authorization code to the running login session and waits up to 30 seconds for the CLI to exit. On success the session is cleared. The returned State describes the final outcome regardless of whether the CLI exited cleanly. The key selects the caller's own session, so a code pasted by one user can never complete another user's OAuth flow.

type Session

type Session struct {
	StartedAt time.Time
	URL       string
	// contains filtered or unexported fields
}

Session represents a live `claude auth login` subprocess waiting for the pasted authorization code on stdin.

func (*Session) Snapshot

func (s *Session) Snapshot() State

Snapshot returns a serializable view of the session's progress without taking ownership of any goroutines or channels. Safe for concurrent reads.

type State

type State struct {
	Active    bool      `json:"active"`
	StartedAt time.Time `json:"started_at,omitempty"`
	URL       string    `json:"url,omitempty"`
	Done      bool      `json:"done,omitempty"`
	Success   bool      `json:"success,omitempty"`
	Output    string    `json:"output,omitempty"`
	Error     string    `json:"error,omitempty"`
}

State is a snapshot of session progress safe to serialize for the UI.

type Status

type Status struct {
	LoggedIn         bool   `json:"loggedIn"`
	AuthMethod       string `json:"authMethod,omitempty"`
	APIProvider      string `json:"apiProvider,omitempty"`
	Email            string `json:"email,omitempty"`
	OrgID            string `json:"orgId,omitempty"`
	OrgName          string `json:"orgName,omitempty"`
	SubscriptionType string `json:"subscriptionType,omitempty"`
}

Status mirrors the JSON shape that `claude auth status --json` returns. Fields not emitted by the CLI are simply left zero-valued.

func FetchStatus

func FetchStatus(ctx context.Context, configDir string) (*Status, error)

FetchStatus runs `claude auth status --json` and parses the result. The CLI returns exit code 0 even when logged out (loggedIn=false in the JSON), so any non-nil error here is a real environmental failure (binary missing, timeout, malformed output) and the UI should surface it as such. configDir scopes the lookup to one identity's credential; empty means the host default.

Jump to

Keyboard shortcuts

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