Skip to content

Commit fb0e2f9

Browse files
committed
fix: use server-returned code in browser URL during login
The CLI was generating its own code client-side and sending it to /api/auth/cli/start, but the server ignores that and generates its own code. The CLI then opened the browser with the client-generated code, which was never stored in the database, causing 'Authorization Failed - Invalid or expired code' immediately on the /cli-auth page. Fix: remove client-side code generation. The start endpoint already returns both code_id and code — parse the code from the response and use it for both the terminal display and the browser URL. Fixes #11
1 parent 3adda5a commit fb0e2f9

4 files changed

Lines changed: 109 additions & 130 deletions

File tree

internal/auth/auth.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package auth
22

33
import (
4-
"crypto/rand"
54
"encoding/json"
65
"fmt"
7-
"math/big"
86
"os"
97
"path/filepath"
108
"time"
@@ -90,15 +88,3 @@ func IsAuthenticated() bool {
9088
return err == nil && auth != nil
9189
}
9290

93-
func GenerateCode() (string, error) {
94-
const charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
95-
code := make([]byte, 8)
96-
for i := range code {
97-
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
98-
if err != nil {
99-
return "", fmt.Errorf("generate auth code: %w", err)
100-
}
101-
code[i] = charset[n.Int64()]
102-
}
103-
return string(code), nil
104-
}

internal/auth/auth_test.go

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"encoding/json"
55
"os"
66
"path/filepath"
7-
"regexp"
87
"testing"
98
"time"
109

@@ -332,43 +331,6 @@ func TestIsAuthenticated_NoToken(t *testing.T) {
332331
assert.False(t, IsAuthenticated())
333332
}
334333

335-
func TestGenerateCode_Length(t *testing.T) {
336-
code, err := GenerateCode()
337-
require.NoError(t, err)
338-
assert.Equal(t, 8, len(code))
339-
}
340-
341-
func TestGenerateCode_Alphanumeric(t *testing.T) {
342-
alphanumericRegex := regexp.MustCompile(`^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{8}$`)
343-
344-
for i := 0; i < 100; i++ {
345-
code, err := GenerateCode()
346-
require.NoError(t, err)
347-
assert.True(t, alphanumericRegex.MatchString(code), "code %s is not alphanumeric", code)
348-
}
349-
}
350-
351-
func TestGenerateCode_Uniqueness(t *testing.T) {
352-
codes := make(map[string]bool)
353-
354-
for i := 0; i < 1000; i++ {
355-
code, err := GenerateCode()
356-
require.NoError(t, err)
357-
assert.False(t, codes[code], "duplicate code generated: %s", code)
358-
codes[code] = true
359-
}
360-
361-
assert.Equal(t, 1000, len(codes))
362-
}
363-
364-
func TestGenerateCode_NoFixedValue(t *testing.T) {
365-
code1, err := GenerateCode()
366-
require.NoError(t, err)
367-
code2, err := GenerateCode()
368-
require.NoError(t, err)
369-
assert.NotEqual(t, code1, code2)
370-
}
371-
372334
func TestTokenPath_Format(t *testing.T) {
373335
tmpDir := t.TempDir()
374336
t.Setenv("HOME", tmpDir)

internal/auth/login.go

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package auth
22

33
import (
4-
"bytes"
54
"crypto/tls"
65
"encoding/json"
76
"fmt"
@@ -39,12 +38,9 @@ func GetAPIBase() string {
3938
return DefaultAPIBase
4039
}
4140

42-
type cliStartRequest struct {
43-
Code string `json:"code"`
44-
}
45-
4641
type cliStartResponse struct {
4742
CodeID string `json:"code_id"`
43+
Code string `json:"code"`
4844
}
4945

5046
type cliPollResponse struct {
@@ -55,12 +51,7 @@ type cliPollResponse struct {
5551
}
5652

5753
func LoginInteractive(apiBase string) (*StoredAuth, error) {
58-
code, err := GenerateCode()
59-
if err != nil {
60-
return nil, err
61-
}
62-
63-
codeID, err := startAuthSession(apiBase, code)
54+
codeID, code, err := startAuthSession(apiBase)
6455
if err != nil {
6556
return nil, err
6657
}
@@ -109,34 +100,32 @@ func LoginInteractive(apiBase string) (*StoredAuth, error) {
109100
return stored, nil
110101
}
111102

112-
func startAuthSession(apiBase, code string) (string, error) {
113-
body, err := json.Marshal(cliStartRequest{Code: code})
103+
func startAuthSession(apiBase string) (codeID, code string, err error) {
104+
req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/auth/cli/start", apiBase), http.NoBody)
114105
if err != nil {
115-
return "", fmt.Errorf("marshal start request: %w", err)
106+
return "", "", fmt.Errorf("create request: %w", err)
116107
}
117108

118-
req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/auth/cli/start", apiBase), bytes.NewReader(body))
119-
if err != nil {
120-
return "", fmt.Errorf("create request: %w", err)
121-
}
122-
req.Header.Set("Content-Type", "application/json")
123-
124109
resp, err := httpClient.Do(req)
125110
if err != nil {
126-
return "", fmt.Errorf("start auth session: %w", err)
111+
return "", "", fmt.Errorf("start auth session: %w", err)
127112
}
128113
defer resp.Body.Close()
129114

130115
if resp.StatusCode != http.StatusOK {
131-
return "", fmt.Errorf("auth start failed with status %d", resp.StatusCode)
116+
return "", "", fmt.Errorf("auth start failed with status %d", resp.StatusCode)
132117
}
133118

134119
var result cliStartResponse
135120
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&result); err != nil {
136-
return "", fmt.Errorf("parse auth response: %w", err)
121+
return "", "", fmt.Errorf("parse auth response: %w", err)
122+
}
123+
124+
if result.CodeID == "" || result.Code == "" {
125+
return "", "", fmt.Errorf("server returned incomplete auth session")
137126
}
138127

139-
return result.CodeID, nil
128+
return result.CodeID, result.Code, nil
140129
}
141130

142131
// pollTimeout and pollInterval are package-level defaults, overridable in tests.

0 commit comments

Comments
 (0)