feat(auth): silently refresh expired OAuth tokens#74
Closed
lmjabreu wants to merge 2 commits into
Closed
Conversation
Wire the @doist/cli-core/auth `refreshAccessToken` helper into ol's request
hot path so access tokens that expire mid-session are refreshed
transparently instead of forcing a manual `ol auth login`.
- exchangeCode now persists `refresh_token` + `expires_in` from Outline's
/oauth/token response (they were being dropped on the floor before).
- New createOutlineAuthProvider.refreshToken POSTs `grant_type=refresh_token`
against `${account.baseUrl}/oauth/token` using the stored OAuth client_id
(public-client refresh, no client_secret).
- getApiToken now flows through refreshAccessToken with a 60s skew window;
OUTLINE_API_TOKEN env still short-circuits with no refresh attempt.
- New reactive 401-retry path in api.ts: on a stored-token 401, force-refresh
once and retry the request. A second 401 propagates untouched.
- StoredUser schema gained `refresh_token`, `access_token_expires_at`,
`refresh_token_expires_at` (all optional).
- `ol auth status` now shows "Access token expires: in 12m" and
"Refresh: enabled" when applicable, plus matching --json fields.
- AUTH_REFRESH_EXPIRED / AUTH_REFRESH_UNAVAILABLE collapse to the existing
NoTokenError so users see one recovery hint instead of two competing codes.
Existing v1.7.0 users will get a one-time forced re-login the first time
their stored access token expires, since their record predates the refresh
token slot. After that, silent refresh kicks in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ath)
- exchangeCode + refreshToken return `expiresAt` (matches the restored
cli-core ExchangeResult field — no rename to accessTokenExpiresAt).
- createOutlineTokenStore wrapper restores set(account, token: string)
and adds setBundle(account, bundle) delegating to inner.setBundle, so
the wrapper satisfies both the base TokenStore contract and the new
optional bundle path.
- getApiTokenForceRefresh passes an explicit lockPath
(`${getConfigPath()}.refresh.lock`) since cli-core no longer derives
it from recordsLocation.
- Remove the no-longer-exported getRecordsLocation pass-through.
- Test fixture's expectations updated to read `result.expiresAt` (not
`accessTokenExpiresAt`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
8 tasks
Contributor
|
Looking into this on the cli-core side as well as here. CLosing this one in favour of my work. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Wires the new
@doist/cli-core/authrefreshAccessTokenhelper into ol's request hot path so OAuth access tokens that expire mid-session are refreshed transparently. No more manual `ol auth login` every few days when the Outline workspace's OAuth client has a short access-token lifetime (set by the workspace admin, not user-configurable).Context: Outline issues short-lived access tokens + long-lived refresh tokens. ol-cli was dropping the refresh token on the floor at login and forcing a re-auth every time the access token expired. This PR plugs the gap.
What changed
Login path
createOutlineAuthProvider.exchangeCodenow readsrefresh_tokenandexpires_infrom Outline's/oauth/tokenresponse (previously discarded).createOutlineAuthProvider.refreshTokenPOSTsgrant_type=refresh_tokenagainst `${account.baseUrl}/oauth/token` using the stored OAuthclient_id. Public-client refresh, no `client_secret`.Request path
getApiToken()now flows throughrefreshAccessToken(proactive: refresh when fewer than 60s remain on the access token). `OUTLINE_API_TOKEN` env still short-circuits with no refresh attempt.api.ts: on a stored-token 401, force-refresh once and retry the request. A second 401 propagates untouched.AUTH_REFRESH_EXPIRED/AUTH_REFRESH_UNAVAILABLEfrom cli-core collapse to the existingNoTokenErrorso users see one recovery hint ("run: ol auth login") instead of two competing codes.Storage
StoredUserschema gainedrefresh_token,access_token_expires_at,refresh_token_expires_at(all optional).<user-id>/refresh) when the keyring is online; they fall back torefresh_tokenon the plaintext config record when the keyring is unavailable (mirrors the existingtoken/fallbackTokenrule).Status output
`ol auth status` now shows:
```
✓ Authenticated
Team: Doist
User: Luis Abreu (luis@doist.com)
Base URL: https://doist.getoutline.com
Token source: secure-store
Access token expires: in 12m
Refresh: enabled (silent re-auth on expiry)
```
`--json` payload gains `accessTokenExpiresAt` and `hasRefreshToken`.
Migration
Existing v1.7.0 users will get a one-time forced re-login the first time their stored access token expires (their record predates the refresh token slot). After that, silent refresh kicks in for the lifetime of the refresh token.
Test plan
🤖 Generated with Claude Code