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 ¶
- Variables
- func CredentialsPath(configDir string) string
- func ForgetHome(ownerKey string) error
- func HasCredential(configDir string) bool
- func HomeFor(ownerKey string) (string, error)
- func HomeRoot() (string, error)
- func IdentitySlug(ownerKey string) string
- func Logout(ctx context.Context, configDir string) error
- func ScopeEnv(env []string, configDir string) []string
- func ScopeHarnessEnv(env []string, configDir string) []string
- type LoginOptions
- type Manager
- type Session
- type State
- type Status
Constants ¶
This section is empty.
Variables ¶
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.
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.
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.
var ErrCodeMalformed = errors.New("that does not look like an authorization code")
ErrCodeMalformed reports a paste that is not an authorization code at all.
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
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
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.
func HasCredential ¶ added in v0.0.2
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
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 HomeRoot ¶ added in v0.0.2
HomeRoot returns the directory holding every identity's Claude config directory, creating it 0700 if absent.
func IdentitySlug ¶ added in v0.0.2
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 ¶
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
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
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 (*Manager) Cancel ¶
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 ¶
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 ¶
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 ¶
Session represents a live `claude auth login` subprocess waiting for the pasted authorization code on stdin.
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 ¶
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.