feat(auth): add Anthropic setup-token authentication

Add support for Claude CLI setup-tokens (sk-ant-oat01-*) as an
authentication method for the Anthropic provider, complementing the
existing API key paste flow.

Changes:
- pkg/auth/token.go: add LoginSetupToken() with prefix and length
  validation, keeping LoginPasteToken untouched
- pkg/providers/claude_provider.go: add NewClaudeProviderWithBearerToken()
  that uses Authorization Bearer with required headers (anthropic-beta:
  claude-code-20250219,oauth-2025-04-20, user-agent, x-app) to enable
  OAuth on the Anthropic API; add useAPIKey field to distinguish between
  x-api-key (regular keys) and Bearer (setup-token/oauth) at request time
- pkg/providers/http_provider.go: route setup-token credentials through
  the Bearer provider; recognize "setup-token" as valid auth method in
  both provider selection paths
- cmd/picoclaw/main.go: add --setup-token CLI flag for anthropic login,
  new authLoginSetupToken() handler, updated help text

Usage: picoclaw auth login --provider anthropic --setup-token

Author: Edgar Gomero
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
edgargomero 2026-02-15 06:01:44 -03:00
parent 9a3f3611c3
commit 32a5bf841c
4 changed files with 97 additions and 6 deletions

View file

@ -777,11 +777,13 @@ func authHelp() {
fmt.Println("Login options:")
fmt.Println(" --provider <name> Provider to login with (openai, anthropic)")
fmt.Println(" --device-code Use device code flow (for headless environments)")
fmt.Println(" --setup-token Use Claude CLI setup-token (anthropic only)")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" picoclaw auth login --provider openai")
fmt.Println(" picoclaw auth login --provider openai --device-code")
fmt.Println(" picoclaw auth login --provider anthropic")
fmt.Println(" picoclaw auth login --provider anthropic --setup-token")
fmt.Println(" picoclaw auth logout --provider openai")
fmt.Println(" picoclaw auth status")
}
@ -789,6 +791,7 @@ func authHelp() {
func authLoginCmd() {
provider := ""
useDeviceCode := false
useSetupToken := false
args := os.Args[3:]
for i := 0; i < len(args); i++ {
@ -800,6 +803,8 @@ func authLoginCmd() {
}
case "--device-code":
useDeviceCode = true
case "--setup-token":
useSetupToken = true
}
}
@ -813,13 +818,40 @@ func authLoginCmd() {
case "openai":
authLoginOpenAI(useDeviceCode)
case "anthropic":
authLoginPasteToken(provider)
if useSetupToken {
authLoginSetupToken()
} else {
authLoginPasteToken(provider)
}
default:
fmt.Printf("Unsupported provider: %s\n", provider)
fmt.Println("Supported providers: openai, anthropic")
}
}
func authLoginSetupToken() {
cred, err := auth.LoginSetupToken(os.Stdin)
if err != nil {
fmt.Printf("Login failed: %v\n", err)
os.Exit(1)
}
if err := auth.SetCredential("anthropic", cred); err != nil {
fmt.Printf("Failed to save credentials: %v\n", err)
os.Exit(1)
}
appCfg, err := loadConfig()
if err == nil {
appCfg.Providers.Anthropic.AuthMethod = "setup-token"
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
fmt.Printf("Warning: could not update config: %v\n", err)
}
}
fmt.Println("Setup-token saved for Anthropic!")
}
func authLoginOpenAI(useDeviceCode bool) {
cfg := auth.OpenAIOAuthConfig()

View file

@ -31,6 +31,40 @@ func LoginPasteToken(provider string, r io.Reader) (*AuthCredential, error) {
}, nil
}
const anthropicSetupTokenPrefix = "sk-ant-oat01-"
func LoginSetupToken(r io.Reader) (*AuthCredential, error) {
fmt.Println("Paste your setup-token from Claude CLI (claude setup-token):")
fmt.Print("> ")
scanner := bufio.NewScanner(r)
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("reading setup-token: %w", err)
}
return nil, fmt.Errorf("no input received")
}
token := strings.TrimSpace(scanner.Text())
if token == "" {
return nil, fmt.Errorf("setup-token cannot be empty")
}
if !strings.HasPrefix(token, anthropicSetupTokenPrefix) {
return nil, fmt.Errorf("invalid setup-token: must start with %s", anthropicSetupTokenPrefix)
}
if len(token) < 80 {
return nil, fmt.Errorf("invalid setup-token: too short (expected at least 80 characters)")
}
return &AuthCredential{
AccessToken: token,
Provider: "anthropic",
AuthMethod: "setup-token",
}, nil
}
func providerDisplayName(provider string) string {
switch provider {
case "anthropic":

View file

@ -13,14 +13,26 @@ import (
type ClaudeProvider struct {
client *anthropic.Client
tokenSource func() (string, error)
useAPIKey bool
}
func NewClaudeProvider(token string) *ClaudeProvider {
client := anthropic.NewClient(
option.WithAuthToken(token),
option.WithAPIKey(token),
option.WithBaseURL("https://api.anthropic.com"),
)
return &ClaudeProvider{client: &client}
return &ClaudeProvider{client: &client, useAPIKey: true}
}
func NewClaudeProviderWithBearerToken(token string) *ClaudeProvider {
client := anthropic.NewClient(
option.WithAuthToken(token),
option.WithBaseURL("https://api.anthropic.com"),
option.WithHeader("anthropic-beta", "claude-code-20250219,oauth-2025-04-20"),
option.WithHeader("user-agent", "claude-cli/2.1.2 (external, cli)"),
option.WithHeader("x-app", "cli"),
)
return &ClaudeProvider{client: &client, useAPIKey: false}
}
func NewClaudeProviderWithTokenSource(token string, tokenSource func() (string, error)) *ClaudeProvider {
@ -29,6 +41,12 @@ func NewClaudeProviderWithTokenSource(token string, tokenSource func() (string,
return p
}
func NewClaudeProviderWithTokenSourceBearer(token string, tokenSource func() (string, error)) *ClaudeProvider {
p := NewClaudeProviderWithBearerToken(token)
p.tokenSource = tokenSource
return p
}
func (p *ClaudeProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
var opts []option.RequestOption
if p.tokenSource != nil {
@ -36,7 +54,11 @@ func (p *ClaudeProvider) Chat(ctx context.Context, messages []Message, tools []T
if err != nil {
return nil, fmt.Errorf("refreshing token: %w", err)
}
opts = append(opts, option.WithAuthToken(tok))
if p.useAPIKey {
opts = append(opts, option.WithAPIKey(tok))
} else {
opts = append(opts, option.WithAuthToken(tok))
}
}
params, err := buildClaudeParams(messages, tools, model, options)

View file

@ -205,6 +205,9 @@ func createClaudeAuthProvider() (LLMProvider, error) {
if cred == nil {
return nil, fmt.Errorf("no credentials for anthropic. Run: picoclaw auth login --provider anthropic")
}
if cred.AuthMethod == "oauth" || cred.AuthMethod == "setup-token" {
return NewClaudeProviderWithTokenSourceBearer(cred.AccessToken, createClaudeTokenSource()), nil
}
return NewClaudeProviderWithTokenSource(cred.AccessToken, createClaudeTokenSource()), nil
}
@ -251,7 +254,7 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
}
case "anthropic", "claude":
if cfg.Providers.Anthropic.APIKey != "" || cfg.Providers.Anthropic.AuthMethod != "" {
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" {
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" || cfg.Providers.Anthropic.AuthMethod == "setup-token" {
return createClaudeAuthProvider()
}
apiKey = cfg.Providers.Anthropic.APIKey
@ -348,7 +351,7 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
}
case (strings.Contains(lowerModel, "claude") || strings.HasPrefix(model, "anthropic/")) && (cfg.Providers.Anthropic.APIKey != "" || cfg.Providers.Anthropic.AuthMethod != ""):
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" {
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" || cfg.Providers.Anthropic.AuthMethod == "setup-token" {
return createClaudeAuthProvider()
}
apiKey = cfg.Providers.Anthropic.APIKey