fix(auth): handle string interval in device-code login

planned-date: 2026-02-12

why: Device-code login was failing because the interval field can arrive as a quoted number, which breaks strict integer decoding and blocks login in shell-only environments.

what: Added flexible interval parsing for numeric or quoted values, wired LoginDeviceCode to the parser, printed the browser auth URL before waiting, and added parser tests for numeric, quoted, and invalid interval payloads.

verification: c:\projects\toolchains\go\bin\go.exe test ./pkg/auth -run Test(ParseDeviceCodeResponse|BuildAuthorizeURL)
This commit is contained in:
Jared Mahotiere
2026-02-12 02:15:46 -05:00
parent 13fcbe6c59
commit a7bbda147e
2 changed files with 97 additions and 6 deletions

View File

@@ -13,6 +13,7 @@ import (
"net/url" "net/url"
"os/exec" "os/exec"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time" "time"
) )
@@ -92,10 +93,13 @@ func LoginBrowser(cfg OAuthProviderConfig) (*AuthCredential, error) {
server.Shutdown(ctx) server.Shutdown(ctx)
}() }()
fmt.Printf("Open this URL to authenticate:\n\n%s\n\n", authURL)
if err := openBrowser(authURL); err != nil { if err := openBrowser(authURL); err != nil {
fmt.Printf("Could not open browser automatically.\nPlease open this URL manually:\n\n%s\n\n", authURL) fmt.Printf("Could not open browser automatically.\nPlease open this URL manually:\n\n%s\n\n", authURL)
} }
fmt.Println("If you're running in a headless environment, use: picoclaw auth login --provider openai --device-code")
fmt.Println("Waiting for authentication in browser...") fmt.Println("Waiting for authentication in browser...")
select { select {
@@ -114,6 +118,57 @@ type callbackResult struct {
err error err error
} }
type deviceCodeResponse struct {
DeviceAuthID string
UserCode string
Interval int
}
func parseDeviceCodeResponse(body []byte) (deviceCodeResponse, error) {
var raw struct {
DeviceAuthID string `json:"device_auth_id"`
UserCode string `json:"user_code"`
Interval json.RawMessage `json:"interval"`
}
if err := json.Unmarshal(body, &raw); err != nil {
return deviceCodeResponse{}, err
}
interval, err := parseFlexibleInt(raw.Interval)
if err != nil {
return deviceCodeResponse{}, err
}
return deviceCodeResponse{
DeviceAuthID: raw.DeviceAuthID,
UserCode: raw.UserCode,
Interval: interval,
}, nil
}
func parseFlexibleInt(raw json.RawMessage) (int, error) {
if len(raw) == 0 || string(raw) == "null" {
return 0, nil
}
var interval int
if err := json.Unmarshal(raw, &interval); err == nil {
return interval, nil
}
var intervalStr string
if err := json.Unmarshal(raw, &intervalStr); err == nil {
intervalStr = strings.TrimSpace(intervalStr)
if intervalStr == "" {
return 0, nil
}
return strconv.Atoi(intervalStr)
}
return 0, fmt.Errorf("invalid integer value: %s", string(raw))
}
func LoginDeviceCode(cfg OAuthProviderConfig) (*AuthCredential, error) { func LoginDeviceCode(cfg OAuthProviderConfig) (*AuthCredential, error) {
reqBody, _ := json.Marshal(map[string]string{ reqBody, _ := json.Marshal(map[string]string{
"client_id": cfg.ClientID, "client_id": cfg.ClientID,
@@ -134,12 +189,8 @@ func LoginDeviceCode(cfg OAuthProviderConfig) (*AuthCredential, error) {
return nil, fmt.Errorf("device code request failed: %s", string(body)) return nil, fmt.Errorf("device code request failed: %s", string(body))
} }
var deviceResp struct { deviceResp, err := parseDeviceCodeResponse(body)
DeviceAuthID string `json:"device_auth_id"` if err != nil {
UserCode string `json:"user_code"`
Interval int `json:"interval"`
}
if err := json.Unmarshal(body, &deviceResp); err != nil {
return nil, fmt.Errorf("parsing device code response: %w", err) return nil, fmt.Errorf("parsing device code response: %w", err)
} }

View File

@@ -197,3 +197,43 @@ func TestOpenAIOAuthConfig(t *testing.T) {
t.Errorf("Port = %d, want 1455", cfg.Port) t.Errorf("Port = %d, want 1455", cfg.Port)
} }
} }
func TestParseDeviceCodeResponseIntervalAsNumber(t *testing.T) {
body := []byte(`{"device_auth_id":"abc","user_code":"DEF-1234","interval":5}`)
resp, err := parseDeviceCodeResponse(body)
if err != nil {
t.Fatalf("parseDeviceCodeResponse() error: %v", err)
}
if resp.DeviceAuthID != "abc" {
t.Errorf("DeviceAuthID = %q, want %q", resp.DeviceAuthID, "abc")
}
if resp.UserCode != "DEF-1234" {
t.Errorf("UserCode = %q, want %q", resp.UserCode, "DEF-1234")
}
if resp.Interval != 5 {
t.Errorf("Interval = %d, want %d", resp.Interval, 5)
}
}
func TestParseDeviceCodeResponseIntervalAsString(t *testing.T) {
body := []byte(`{"device_auth_id":"abc","user_code":"DEF-1234","interval":"5"}`)
resp, err := parseDeviceCodeResponse(body)
if err != nil {
t.Fatalf("parseDeviceCodeResponse() error: %v", err)
}
if resp.Interval != 5 {
t.Errorf("Interval = %d, want %d", resp.Interval, 5)
}
}
func TestParseDeviceCodeResponseInvalidInterval(t *testing.T) {
body := []byte(`{"device_auth_id":"abc","user_code":"DEF-1234","interval":"abc"}`)
if _, err := parseDeviceCodeResponse(body); err == nil {
t.Fatal("expected error for invalid interval")
}
}