feat: store OAuth tokens in OS keyring via credstore (#926)

## Summary

- Introduce `github.com/go-authgate/sdk-go/credstore` to store OAuth tokens securely in the OS keyring (macOS Keychain / Linux Secret Service / Windows Credential Manager), with automatic fallback to an encrypted JSON file
- Add `AuthMethod` field to `Login` struct; new OAuth logins are marked `auth_method: oauth` and no longer write `token`/`refresh_token`/`token_expiry` to `config.yml`
- Add `GetAccessToken()` / `GetRefreshToken()` / `GetTokenExpiry()` accessors that transparently read from credstore for OAuth logins, with fallback to YAML fields for legacy logins
- Update all token reference sites across the codebase to use the new accessors
- Non-OAuth logins (token, SSH) are completely unaffected; no migration of existing tokens

## Key files

| File | Role |
|------|------|
| `modules/config/credstore.go` | **New** — credstore wrapper (Load/Save/Delete) |
| `modules/config/login.go` | Login struct, token accessors, refresh logic |
| `modules/auth/oauth.go` | OAuth flow, token creation / re-authentication |
| `modules/api/client.go`, `cmd/login/helper.go`, `cmd/login/oauth_refresh.go` | Token reference updates |
| `modules/task/pull_*.go`, `modules/task/repo_clone.go` | Git operation token reference updates |

## Test plan

- [x] `go build ./...` compiles successfully
- [x] `go test ./...` all tests pass
- [x] `tea login add --oauth` completes OAuth flow; verify config.yml has `auth_method: oauth` but no token/refresh_token/token_expiry
- [x] `tea repos ls` API calls work (token read from credstore)
- [x] `tea login delete <name>` credstore token is also removed
- [x] Existing non-OAuth logins continue to work unchanged

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://gitea.com/gitea/tea/pulls/926
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
This commit is contained in:
Bo-Yi Wu
2026-03-12 02:49:14 +00:00
committed by Bo-Yi Wu (吳柏毅)
parent 0346e1cbb5
commit 302c946cb8
13 changed files with 231 additions and 43 deletions

View File

@@ -377,22 +377,17 @@ func createLoginFromToken(name, serverURL string, token *oauth2.Token, insecure
}
}
// Create login object
// Create login object with OAuth auth method
login := config.Login{
Name: name,
URL: serverURL,
Token: token.AccessToken,
RefreshToken: token.RefreshToken,
Token: token.AccessToken, // temporarily set for Client() validation
AuthMethod: config.AuthMethodOAuth,
Insecure: insecure,
VersionCheck: true,
Created: time.Now().Unix(),
}
// Set token expiry if available
if !token.Expiry.IsZero() {
login.TokenExpiry = token.Expiry.Unix()
}
// Validate token by getting user info
client := login.Client()
u, _, err := client.GetMyUserInfo()
@@ -400,6 +395,9 @@ func createLoginFromToken(name, serverURL string, token *oauth2.Token, insecure
return fmt.Errorf("failed to validate token: %s", err)
}
// Clear token from YAML fields (will be stored in credstore)
login.Token = ""
// Set user info
login.User = u.UserName
@@ -415,6 +413,11 @@ func createLoginFromToken(name, serverURL string, token *oauth2.Token, insecure
return err
}
// Save tokens to credstore
if err := config.SaveOAuthToken(login.Name, token.AccessToken, token.RefreshToken, token.Expiry); err != nil {
return fmt.Errorf("failed to save token to secure store: %s", err)
}
fmt.Printf("Login as %s on %s successful. Added this login as %s\n", login.User, login.URL, login.Name)
return nil
}
@@ -443,7 +446,11 @@ func ReauthenticateLogin(login *config.Login) error {
return err
}
// Update the existing login with new token data
if login.IsOAuth() {
return config.SaveOAuthTokenFromOAuth2(login.Name, token, login)
}
// Legacy path for non-OAuth logins
login.Token = token.AccessToken
if token.RefreshToken != "" {
login.RefreshToken = token.RefreshToken
@@ -451,7 +458,5 @@ func ReauthenticateLogin(login *config.Login) error {
if !token.Expiry.IsZero() {
login.TokenExpiry = token.Expiry.Unix()
}
// Save updated login
return config.SaveLoginTokens(login)
}