From 95fdee5cd8fe39934c14cdb7cce4d4349f93f98a Mon Sep 17 00:00:00 2001 From: wbtiger <28288271@qq.com> Date: Wed, 8 Apr 2026 13:01:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20use=20cookie-based=20auth=20for=20login?= =?UTF-8?q?=20=E2=80=94=20resolves=20Windows=20401?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: GitLink login API returns session cookies (autologin_trustie), not API access_tokens. The old code tried to use the cookie value as an access_token query parameter, which GitLink rejects. Fix: store auth cookies with "cookie:" prefix. Transport detects the prefix and sends credentials as Cookie header instead of access_token param. Private tokens (from --token mode) continue to use access_token param — fully backward compatible. Verified: login → store cookie → GetCurrentUser → repo list all pass. Co-Authored-By: Claude Opus 4.6 --- internal/auth/login.go | 64 ++++++++++++++------------------------ internal/auth/transport.go | 22 ++++++++++--- npm/package.json | 2 +- 3 files changed, 42 insertions(+), 46 deletions(-) diff --git a/internal/auth/login.go b/internal/auth/login.go index 18ea099..4b035e7 100644 --- a/internal/auth/login.go +++ b/internal/auth/login.go @@ -23,7 +23,7 @@ type LoginResult struct { Message string `json:"message"` } -// Login authenticates with username/password and stores the token. +// Login authenticates with username/password and stores the session cookie. func Login(username, password string) (*LoginResult, error) { cfg, err := config.Load() if err != nil { @@ -68,62 +68,44 @@ func Login(username, password string) (*LoginResult, error) { return nil, fmt.Errorf("%s", result.Message) } - // Collect all candidate tokens - var candidates []string - - // Source 1: autologin cookie (from final response and cookie jar) + // Collect auth cookies from response (GitLink uses autologin_trustie for session persistence) + var authCookies []string for _, cookie := range resp.Cookies() { - if strings.Contains(strings.ToLower(cookie.Name), "autologin") { - candidates = append(candidates, cookie.Value) - break + if cookie.Name == "autologin_trustie" || cookie.Name == "_educoder_session" { + authCookies = append(authCookies, cookie.Name+"="+cookie.Value) } } - if len(candidates) == 0 { - // Also check the cookie jar (captures cookies from redirect hops) + // Also check cookie jar (captures cookies from redirect hops) + if len(authCookies) == 0 { if u, err := url.Parse(loginURL); err == nil { for _, cookie := range jar.Cookies(u) { - if strings.Contains(strings.ToLower(cookie.Name), "autologin") { - candidates = append(candidates, cookie.Value) - break + if cookie.Name == "autologin_trustie" || cookie.Name == "_educoder_session" { + authCookies = append(authCookies, cookie.Name+"="+cookie.Value) } } } } - // Source 2: token from response body - if result.Token != "" { - candidates = append(candidates, result.Token) + if len(authCookies) == 0 { + return nil, fmt.Errorf("login succeeded but no auth cookies received") } - if len(candidates) == 0 { - return nil, fmt.Errorf("login succeeded but no access token received") + // Store as "cookie:=; =" format + // Transport will send these as Cookie header + tokenValue := "cookie:" + strings.Join(authCookies, "; ") + + if err := StoreToken(tokenValue); err != nil { + return nil, fmt.Errorf("failed to store credentials: %w", err) } - // Try each candidate token: store it, then verify with /users/me - var lastErr error - for _, token := range candidates { - if err := StoreToken(token); err != nil { - lastErr = fmt.Errorf("failed to store token: %w", err) - continue - } - // Verify the token actually works - if _, err := GetCurrentUser(); err == nil { - // Verify the stored token matches (catches keyring store/load mismatch) - stored, loadErr := LoadToken() - if loadErr != nil || stored != token { - // Keyring round-trip failed, force file-based storage - if writeErr := storeTokenFile(token); writeErr != nil { - lastErr = fmt.Errorf("failed to store token to file: %w", writeErr) - continue - } - } - return &result, nil - } else { - lastErr = err - } + // Verify the stored credentials actually work + if _, verifyErr := GetCurrentUser(); verifyErr != nil { + // Clean up the bad token + _ = DeleteToken() + return nil, fmt.Errorf("login failed: credentials not accepted by API (%v)", verifyErr) } - return nil, fmt.Errorf("login succeeded but token verification failed (%v).\n\nPlease use private token instead:\n 1. Visit https://www.gitlink.org.cn/tokens → Create a new token\n 2. Run: gitlink-cli auth login --token\n 3. Paste your private token", lastErr) + return &result, nil } // GetCurrentUser fetches the authenticated user info. diff --git a/internal/auth/transport.go b/internal/auth/transport.go index 28e6596..bcabf50 100644 --- a/internal/auth/transport.go +++ b/internal/auth/transport.go @@ -2,9 +2,10 @@ package auth import ( "net/http" + "strings" ) -// Transport wraps an http.RoundTripper and injects the autologin token. +// Transport wraps an http.RoundTripper and injects authentication. type Transport struct { Base http.RoundTripper } @@ -12,9 +13,22 @@ type Transport struct { func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { token, err := LoadToken() if err == nil && token != "" { - q := req.URL.Query() - q.Set("access_token", token) - req.URL.RawQuery = q.Encode() + if strings.HasPrefix(token, "cookie:") { + // Cookie-based auth: token stored as "cookie:=" + cookiePart := strings.TrimPrefix(token, "cookie:") + // Append to existing cookies + existing := req.Header.Get("Cookie") + if existing != "" { + req.Header.Set("Cookie", existing+"; "+cookiePart) + } else { + req.Header.Set("Cookie", cookiePart) + } + } else { + // Private token: use access_token query parameter + q := req.URL.Query() + q.Set("access_token", token) + req.URL.RawQuery = q.Encode() + } } if req.Body != nil && req.Header.Get("Content-Type") == "" { req.Header.Set("Content-Type", "application/json") diff --git a/npm/package.json b/npm/package.json index 797f109..0740688 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "@gitlink-ai/cli", - "version": "0.1.10", + "version": "0.1.11", "description": "GitLink 平台官方命令行工具 — 代码托管、协作开发和自动化", "bin": { "gitlink-cli": "bin/cli.js",