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:
@@ -13,6 +13,7 @@ import (
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -92,10 +93,13 @@ func LoginBrowser(cfg OAuthProviderConfig) (*AuthCredential, error) {
|
||||
server.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
fmt.Printf("Open this URL to authenticate:\n\n%s\n\n", authURL)
|
||||
|
||||
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.Println("If you're running in a headless environment, use: picoclaw auth login --provider openai --device-code")
|
||||
fmt.Println("Waiting for authentication in browser...")
|
||||
|
||||
select {
|
||||
@@ -114,6 +118,57 @@ type callbackResult struct {
|
||||
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) {
|
||||
reqBody, _ := json.Marshal(map[string]string{
|
||||
"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))
|
||||
}
|
||||
|
||||
var deviceResp struct {
|
||||
DeviceAuthID string `json:"device_auth_id"`
|
||||
UserCode string `json:"user_code"`
|
||||
Interval int `json:"interval"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &deviceResp); err != nil {
|
||||
deviceResp, err := parseDeviceCodeResponse(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing device code response: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -197,3 +197,43 @@ func TestOpenAIOAuthConfig(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user