feat(auth): add Anthropic OAuth setup-token login#926
feat(auth): add Anthropic OAuth setup-token login#926BallerIsLeet wants to merge 1 commit intosipeed:mainfrom
Conversation
Add support for Anthropic's OAuth-based setup tokens (sk-ant-oat01-*) as an alternative to API keys. This includes: - New `--setup-token` flag on `auth login` command - Interactive login menu for Anthropic (setup token vs API key) - Setup token validation and credential storage with oauth auth method - Usage endpoint integration to show 5h/7d utilization in `auth status` - Streaming support for OAuth tokens (required by Anthropic API) - Model ID normalization (dots to hyphens) for API compatibility - Remove .env.example (secrets should not be templated) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
nikolasdehor
left a comment
There was a problem hiding this comment.
Review: feat(auth): add Anthropic OAuth setup-token login
Disclosure: I submitted PR #444 (Anthropic API key routing) and have hands-on experience with Anthropic OAuth/OAT token flows.
Overall this is a well-structured PR that adds a much-needed feature. The interactive menu, token validation, usage endpoint, and streaming path are all solid additions.
Security
-
Token validation is correct -- the
sk-ant-oat01-prefix check and minimum length (80 chars) are appropriate guards. Good that you do not log the token value anywhere. -
Usage endpoint auth headers look correct --
Bearer+anthropic-beta: oauth-2025-04-20matches the Anthropic OAuth API surface. -
Good:
.env.exampleremoval -- removing the template that could lead to committed secrets is the right call.
Correctness
-
Streaming requirement for OAuth tokens -- This is correct behavior. Anthropic OAuth tokens require the streaming endpoint. The
chatStreamingimplementation usingAccumulateis clean. One note: if the stream returns zero content blocks (e.g., a refusal), doesparseResponse(&msg)handle that gracefully? Worth a quick check. -
Model ID normalization (dots to hyphens) --
strings.ReplaceAll(model, ".", "-")is correct for the Anthropic API. Their API uses hyphens (claude-sonnet-4-6) while configs often use dots (claude-sonnet-4.6). The test update confirms this. -
anthropic-betaheader on OAuth path -- Correctly added only whentokenSource != nil. Non-OAuth (API key) requests will not get the beta header, which is the right behavior.
Suggestions
-
Hardcoded default model
claude-sonnet-4.6-- InauthLoginAnthropicSetupToken(), the function hardcodesclaude-sonnet-4.6as the default model and setsAuthMethod: "oauth"on the first Anthropic model it finds in config. Consider: what if the user already hasclaude-opus-4-6configured with an API key? This would overwrite their model choice. Maybe only set the default if no Anthropic model exists at all, rather than always overwritingAgents.Defaults.ModelName. -
body, _ := io.ReadAll(resp.Body)in usage.go -- The error fromReadAllis silently discarded. If the read fails, the subsequent status check will use an empty body, which is confusing. Consider handling the error. -
Status code check order in usage.go -- You check
StatusForbiddenbeforeStatusOK, but the generic!= StatusOKblock would also catch 403. The explicit 403 message is nice for UX. Consider nesting it inside the!= StatusOKblock for clarity. -
Missing test coverage --
LoginSetupTokenandFetchAnthropicUsagelack unit tests. The streaming path (chatStreaming) also lacks tests. Given the security sensitivity of the auth flow, tests for edge cases (empty input, wrong prefix, short token) would be valuable.
Minor
- The
isAnthropicModelfunction is called but not shown in the diff -- assuming it exists elsewhere.
Good work overall. The core OAuth + streaming architecture is sound.
| const supportedProvidersMsg = "supported providers: openai, anthropic, google-antigravity" | ||
|
|
||
| func authLoginCmd(provider string, useDeviceCode bool) error { | ||
| func authLoginCmd(provider string, useDeviceCode bool, setupToken bool) error { |
There was a problem hiding this comment.
nit: Perhaps the var should be named useOauth instead of setupToken?
| } | ||
|
|
||
| token := strings.TrimSpace(scanner.Text()) | ||
| if token == "" { |
There was a problem hiding this comment.
nit: with the validation below, this check is redundant, is that right?
There was a problem hiding this comment.
This file should be better named anthropic_usage.go to reflect its scope.
| return nil, err | ||
| } | ||
| req.Header.Set("Authorization", "Bearer "+token) | ||
| req.Header.Set("anthropic-version", "2023-06-01") |
There was a problem hiding this comment.
These two values should better be defined as const above, not hardcoded here.
| opts = append(opts, option.WithAuthToken(tok)) | ||
| opts = append(opts, | ||
| option.WithAuthToken(tok), | ||
| option.WithHeader("anthropic-beta", "oauth-2025-04-20"), |
There was a problem hiding this comment.
magic value flying around is not great.
There was a problem hiding this comment.
I don't know whether the removal of this file should be in scope for this PR.
|
|
||
| scanner := bufio.NewScanner(os.Stdin) | ||
| choice := "1" | ||
| if scanner.Scan() { |
There was a problem hiding this comment.
Do we want a loop here with validation logic inside so user will definition give either 1 or 2?
| }) | ||
| } | ||
|
|
||
| appCfg.Agents.Defaults.ModelName = "claude-sonnet-4.6" |
There was a problem hiding this comment.
I feel this should be another magic value defined as const
xiaket
left a comment
There was a problem hiding this comment.
The general direction LGTM, but the number of minor issues makes it a RC.
Summary
sk-ant-oat01-*) as an alternative to API keys via new--setup-tokenflag and interactive login menuauth status.env.exampleto avoid templating secretsTest plan
picoclaw auth login -p anthropicand verify interactive menu shows setup token + API key optionspicoclaw auth login -p anthropic --setup-tokenand verify it accepts validsk-ant-oat01-*tokenspicoclaw auth statuswith an OAuth credential and verify usage percentages displaygo test ./pkg/providers/anthropic/...🤖 Generated with Claude Code