forked from Gitlink/gitlink-cli
fix: use cookie-based auth for login — resolves Windows 401
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 <noreply@anthropic.com>
This commit is contained in:
parent
a27122ab76
commit
95fdee5cd8
|
|
@ -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:<name>=<value>; <name>=<value>" 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.
|
||||
|
|
|
|||
|
|
@ -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:<name>=<value>"
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@gitlink-ai/cli",
|
||||
"version": "0.1.10",
|
||||
"version": "0.1.11",
|
||||
"description": "GitLink 平台官方命令行工具 — 代码托管、协作开发和自动化",
|
||||
"bin": {
|
||||
"gitlink-cli": "bin/cli.js",
|
||||
|
|
|
|||
Loading…
Reference in New Issue