Description
Third party MCP agents authenticate with a 30 minute access token and a 30 day refresh token stored on the same AccessToken row. A few things around refreshing them need attention.
Nightly cleanup deletes active grants (witnessed in real use)
The daily expireTokens housekeeper task deletes every token whose expiresAt has passed, without checking refreshTokenExpiresAt. MCP access tokens expire every 30 minutes, so the sweep removes grants that are still refreshable. The agent's next refresh fails with "Invalid refresh_token" and the token vanishes from the UI.
Fix: only delete rows where the refresh token is also expired or missing, the same guard getOrExpire already has.
Missing refresh_token returns a 500
A token request with grant_type=refresh_token and no refresh_token hits sha256(undefined) and throws.
Fix: validate the field and return 400 invalid_request, like the authorization_code branch does.
Refresh cache is per process
Concurrent refresh dedup uses an in-memory cache. With multiple replicas each one mints its own access token and overwrites the same row, so a token handed out by another replica stops working until the next refresh.
Fix: might be worth a shared cache, or dropping the dedup entirely.
Cached refreshes don't extend the window
A refresh served from the cache skips the refreshTokenExpiresAt bump, so the sliding window only extends on some refreshes.
Fix: bump it on every refresh.
MCP tokens look like PATs in the UI
They're stored with ownerType: 'user' and a name, so they show up in the Personal Access Tokens list with the 30 minute access expiry. That already misled a user into thinking his MCP access dies every half hour. Editing one there rewrites expiresAt but never refreshTokenExpiresAt, and clearing the expiry makes the token never expire.
Fix: show the real grant lifetime for MCP tokens or move them out of the PAT list, and keep the PAT edit path away from them.
Description
Third party MCP agents authenticate with a 30 minute access token and a 30 day refresh token stored on the same AccessToken row. A few things around refreshing them need attention.
Nightly cleanup deletes active grants (witnessed in real use)
The daily
expireTokenshousekeeper task deletes every token whoseexpiresAthas passed, without checkingrefreshTokenExpiresAt. MCP access tokens expire every 30 minutes, so the sweep removes grants that are still refreshable. The agent's next refresh fails with "Invalid refresh_token" and the token vanishes from the UI.Fix: only delete rows where the refresh token is also expired or missing, the same guard
getOrExpirealready has.Missing refresh_token returns a 500
A token request with
grant_type=refresh_tokenand norefresh_tokenhitssha256(undefined)and throws.Fix: validate the field and return
400 invalid_request, like the authorization_code branch does.Refresh cache is per process
Concurrent refresh dedup uses an in-memory cache. With multiple replicas each one mints its own access token and overwrites the same row, so a token handed out by another replica stops working until the next refresh.
Fix: might be worth a shared cache, or dropping the dedup entirely.
Cached refreshes don't extend the window
A refresh served from the cache skips the
refreshTokenExpiresAtbump, so the sliding window only extends on some refreshes.Fix: bump it on every refresh.
MCP tokens look like PATs in the UI
They're stored with
ownerType: 'user'and a name, so they show up in the Personal Access Tokens list with the 30 minute access expiry. That already misled a user into thinking his MCP access dies every half hour. Editing one there rewritesexpiresAtbut neverrefreshTokenExpiresAt, and clearing the expiry makes the token never expire.Fix: show the real grant lifetime for MCP tokens or move them out of the PAT list, and keep the PAT edit path away from them.