fix(auth): persist access_token on refresh for providers that need it

Previously, metadataEqualIgnoringTimestamps() ignored access_token for all
providers, which prevented refreshed tokens from being persisted to disk/database.
This caused tokens to be lost on server restart for providers like iFlow.

This change makes the behavior provider-specific:
- Providers like gemini/gemini-cli that issue new tokens on every refresh and
  can re-fetch when needed will continue to ignore access_token (optimization)
- Other providers like iFlow will now persist access_token changes

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Zhi Yang
2026-01-05 10:21:29 +00:00
parent 9f1b445c7c
commit 33aa665555

View File

@@ -74,7 +74,7 @@ func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (str
if existing, errRead := os.ReadFile(path); errRead == nil {
// Use metadataEqualIgnoringTimestamps to skip writes when only timestamp fields change.
// This prevents the token refresh loop caused by timestamp/expired/expires_in changes.
if metadataEqualIgnoringTimestamps(existing, raw) {
if metadataEqualIgnoringTimestamps(existing, raw, auth.Provider) {
return path, nil
}
} else if errRead != nil && !os.IsNotExist(errRead) {
@@ -284,7 +284,10 @@ func jsonEqual(a, b []byte) bool {
// ignoring fields that change on every refresh but don't affect functionality.
// This prevents unnecessary file writes that would trigger watcher events and
// create refresh loops.
func metadataEqualIgnoringTimestamps(a, b []byte) bool {
// The provider parameter controls whether access_token is ignored: providers like
// Google OAuth (gemini, gemini-cli) can re-fetch tokens when needed, while others
// like iFlow require the refreshed token to be persisted.
func metadataEqualIgnoringTimestamps(a, b []byte, provider string) bool {
var objA, objB map[string]any
if err := json.Unmarshal(a, &objA); err != nil {
return false
@@ -295,9 +298,18 @@ func metadataEqualIgnoringTimestamps(a, b []byte) bool {
// Fields to ignore: these change on every refresh but don't affect authentication logic.
// - timestamp, expired, expires_in, last_refresh: time-related fields that change on refresh
// - access_token: Google OAuth returns a new access_token on each refresh, this is expected
// and shouldn't trigger file writes (the new token will be fetched again when needed)
ignoredFields := []string{"timestamp", "expired", "expires_in", "last_refresh", "access_token"}
ignoredFields := []string{"timestamp", "expired", "expires_in", "last_refresh"}
// Providers that issue new access_token on every refresh and can re-fetch when needed.
// For these providers, we also ignore access_token to avoid unnecessary file writes.
providersIgnoringAccessToken := map[string]bool{
"gemini": true,
"gemini-cli": true,
}
if providersIgnoringAccessToken[provider] {
ignoredFields = append(ignoredFields, "access_token")
}
for _, field := range ignoredFields {
delete(objA, field)
delete(objB, field)