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 3 days 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

This issue might be a duplicate of existing issues. Please check:

  • #9111: OAuth token expires after inactivity causing 'Token refresh failed: 400' - Same root cause analysis and similar proposed solution
  • #6559: Claude subscription token expires after a period of time - Reports the same token expiration after inactivity
  • #4992: Getting "Unauthorized: token expired" during conversation - Similar token expiration errors during usage

Feel free to ignore if none of these address your specific case.

github-actions[bot] avatar Jan 17 '26 19:01 github-actions[bot]

Closing this issue - the proposed solution (proactive token refresh via keepalive) doesn't solve the problem. Users still get 'Token refresh failed: 400' errors after overnight inactivity, even with the keepalive running.

The issue appears to be that Anthropic's OAuth refresh tokens themselves expire or become invalid after extended periods, which cannot be prevented by periodic pinging or token refresh.

This may be a limitation of Anthropic's OAuth implementation for consumer accounts (Claude Max/Pro).

mguttmann avatar Jan 19 '26 06:01 mguttmann