opencode icon indicating copy to clipboard operation
opencode copied to clipboard

fix: OAuth tokens expire after inactivity despite /api/oauth/usage polling

Open mguttmann opened this issue 1 day ago • 1 comments

Problem

OAuth tokens expire after periods of inactivity (typically 1-2 hours), causing "Token refresh failed: 400" errors when users return to OpenCode. This has been reported in multiple issues:

  • #6559: Claude subscription token expires after a period of time
  • #4992: Getting "Unauthorized: token expired" during conversation

Root Cause Analysis

The existing token refresh logic in opencode-anthropic-auth plugin only triggers during API calls. If the app is idle or closed:

  1. No API calls are made
  2. The access token expires (~1 hour)
  3. Eventually the refresh token also becomes invalid
  4. On next use: "Token refresh failed: 400"

The previous fix attempt (PR #9112) using /api/oauth/usage endpoint did not work because:

  • That endpoint only returns usage statistics
  • It does not refresh the OAuth token
  • It does not extend the session lifetime

Solution

Implement a proactive token keepalive that:

  1. Refreshes tokens before they expire - Uses Anthropic's OAuth token endpoint (POST /v1/oauth/token) with grant_type: refresh_token
  2. Runs every 30 minutes - More frequent to catch expiring tokens
  3. Refreshes 10 minutes before expiry - Proactive refresh buffer
  4. Pings with Messages API - After refresh, sends minimal request to maintain session activity
  5. Updates stored tokens - Saves refreshed tokens to auth.json

Implementation Details

See PR #9122 for the implementation:

New file: packages/opencode/src/auth/keepalive.ts

// Key functions:
- refreshAnthropicToken() - Calls Anthropic OAuth token endpoint
- updateStoredToken() - Persists refreshed token to auth.json  
- keepAliveAccount() - Checks expiry, refreshes if needed, then pings
- pingAllAnthropicAccounts() - Processes all OAuth accounts

Token Refresh Flow

Every 30 minutes:
  For each Anthropic OAuth account:
    1. Check if token expires within 10 minutes
    2. If yes: POST /v1/oauth/token (refresh_token grant)
    3. Update stored token
    4. POST /v1/messages (minimal ping)

Modified: packages/opencode/src/project/bootstrap.ts

  • Initializes AuthKeepAlive on app startup

Testing

  1. Start OpenCode with Anthropic OAuth authentication
  2. Leave idle for several hours or overnight
  3. Check logs for auth.keepalive service:
    INFO auth.keepalive: starting oauth keepalive {"intervalMs": 1800000}
    INFO auth.keepalive: token expired or expiring soon, refreshing {"recordId": "...", "expiresIn": 300}
    INFO auth.keepalive: token refresh successful {"recordId": "..."}
    INFO auth.keepalive: keepalive ping successful {"recordId": "..."}
    
  4. Resume usage - no token expiration errors

Important Notes

  • Uses the same client_id as opencode-anthropic-auth plugin
  • App must be running for keepalive to work (cannot prevent expiration if app is closed)
  • Token consumption: ~10 tokens per ping (negligible)

mguttmann avatar Jan 17 '26 19:01 mguttmann