fix(api): revoke the native refresh-token family on logout #390

Open
opened 2026-06-30 12:24:09 +00:00 by james · 0 comments
Owner

Follow-up to #385 (native logout now clears the local SecureStore tokens).

Gap

/api/auth/logout (apps/api/app/api/auth/logout/route.ts) only revokes the web carol_session cookie. For native clients (bearer access + refresh tokens, ADR-0027), logout discards the tokens locally but the refresh-token family stays valid server-side until it expires (≤30 days, REFRESH_TOKEN_TTL_SECONDS). A token extracted from a device before logout — or a device that logged out offline — remains usable until then.

Work

  • On logout, when the request carries native bearer credentials, revoke server-side:
    • the presented access token (the logout request already sends the bearer header via getAuthHeader) → AccessTokensRepository.revoke(id), and
    • the refresh-token familyRefreshTokensRepository.revokeFamily(familyId).
    • Both primitives already exist (apps/api/db/repositories/{access-tokens,refresh-tokens}.ts).
  • Decide the contract: simplest is to have the native client POST its refresh token in the logout body ({ refreshToken }, optional) so the server can resolve the family; alternatively resolve the family from the access token. Validate with zod, keep RFC 7807 errors, regenerate the OpenAPI contract.
  • Native client (apps/client/lib/auth/logout.ts performLogout) sends the refresh token (read from getRefreshToken()) before clearing it locally. Best-effort: a failed revoke must not block the local logout.
  • Both-engine tests: logout revokes the family so a subsequent /api/auth/refresh with that token is rejected; web cookie logout unchanged.

Out of scope

Revoking all of a user's sessions/devices on logout (this is per-device). A "sign out everywhere" affordance would be a separate feature.

Follow-up to #385 (native logout now clears the local SecureStore tokens). ## Gap `/api/auth/logout` (`apps/api/app/api/auth/logout/route.ts`) only revokes the web `carol_session` cookie. For native clients (bearer access + refresh tokens, ADR-0027), logout discards the tokens **locally** but the refresh-token family stays **valid server-side** until it expires (≤30 days, `REFRESH_TOKEN_TTL_SECONDS`). A token extracted from a device before logout — or a device that logged out offline — remains usable until then. ## Work - On logout, when the request carries native bearer credentials, revoke server-side: - the presented access token (the logout request already sends the bearer header via `getAuthHeader`) → `AccessTokensRepository.revoke(id)`, and - the refresh-token **family** → `RefreshTokensRepository.revokeFamily(familyId)`. - Both primitives already exist (`apps/api/db/repositories/{access-tokens,refresh-tokens}.ts`). - Decide the contract: simplest is to have the native client POST its refresh token in the logout body (`{ refreshToken }`, optional) so the server can resolve the family; alternatively resolve the family from the access token. Validate with zod, keep RFC 7807 errors, regenerate the OpenAPI contract. - Native client (`apps/client/lib/auth/logout.ts` `performLogout`) sends the refresh token (read from `getRefreshToken()`) before clearing it locally. Best-effort: a failed revoke must not block the local logout. - Both-engine tests: logout revokes the family so a subsequent `/api/auth/refresh` with that token is rejected; web cookie logout unchanged. ## Out of scope Revoking *all* of a user's sessions/devices on logout (this is per-device). A "sign out everywhere" affordance would be a separate feature.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#390
No description provided.