Skip to content

feat: renew browser sessions with refresh tokens - #74

Merged
AntTheLimey merged 6 commits into
mainfrom
feat/browser-login-refresh
Oct 26, 2026
Merged

AntTheLimey merged 6 commits into
mainfrom
feat/browser-login-refresh

Conversation

@AntTheLimey

@AntTheLimey AntTheLimey commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

What changed

Step 2 of browser sign-in, stacked on #73. It removes the forced re-login when the access token expires.

  • Login requests offline_access and keeps Auth0's refresh token in the OS keychain, under its own entry, never in the config file or token cache. With no refresh token or no keychain, login behaves exactly as in feat: browser sign-in for pgedge starfleet #73.
  • Renewal happens when a session is near expiry, at first use or mid --wait. The rotated refresh token is saved before the new access token is cached and used. A per-profile lock file serialises renewal across processes. Under the lock, the cache is re-read so a second process uses the first one's renewal instead of reusing a rotated token, which Auth0 would treat as theft and revoke. saas#2043 also adds a 10s reuse leeway.
  • A refused renewal (invalid_grant) clears the refresh token, but only if it's still the one sent, and gives the existing exit-5 "sign in again" message. A network or 5xx failure keeps the token and falls back to the current access token while it's valid.
  • Logout revokes the refresh token at Auth0 (best-effort), then clears it. A refresh token that a new login or an API-client login replaces is revoked too.
  • auth status and doctor report whether a session renews. An expired access token with a refresh token is "renews on next use", exit 0.
  • --debug masks the refresh token, the token responses and the revoke token field.
  • Docs, llms pages and the starfleet skill say a session renews while in use and ends after a period without use or a fixed maximum.

Checks

  • make test passes. Coverage is 90.0% against the 90.0% threshold. make lint, make docs-check and make lint-docs are clean.
  • 4 new test functions: rotation order, refused renewal, logout revoke, and two concurrent renewals against a stub Auth0 that treats reuse as theft. Four existing tests were extended.
  • Mutation checks, all caught:
    • no rotated save
    • no access-token cache
    • token kept after invalid_grant
    • flag path clearing the session
    • lock removed
    • re-read removed
    • cache written before the keychain
  • One independent review: 1 Important finding (concurrent renewal wiping the session) and 4 Minor, all fixed here.

Risks

  • Coverage sits exactly at the threshold, so the next change that lowers it fails make test.
  • The stale-lock takeover after a crash is untested. The lock is taken over after 45s and waiters give up after 50s.
  • Tested against the dev Auth0 with saas#2043 applied. Sign-in requested offline_access and stored the refresh token in the keychain. Two renewals forced 15s apart, beyond the 10s reuse leeway, each returned a new access token and a working managed database list, so each rotated refresh token was saved and used. The account requires MFA, so renewal also confirms MFA is skipped on refresh. Logout removed the cache entry and the keychain entry; the next call failed with exit 5. Prod gets saas#2043 once its deploy passes the integration gate.

@AntTheLimey
AntTheLimey force-pushed the feat/browser-login-refresh branch from eebf054 to 1b10de8 Compare October 19, 2026 22:09
@AntTheLimey
AntTheLimey force-pushed the feat/browser-login-refresh branch from 1b10de8 to 9385f9d Compare October 19, 2026 23:01
@AntTheLimey
AntTheLimey force-pushed the feat/browser-login-refresh branch from afa72e3 to bbe2f7d Compare October 22, 2026 17:27
Login asks for offline_access and keeps Auth0's refresh token in the
OS keychain. A session near expiry renews itself, saving the new access
token and the rotated refresh token before use. invalid_grant clears
the refresh token and ends the session; logout revokes it. Without a
refresh token or a keychain, step-1 behaviour is unchanged.
A per-profile lock file beside the token cache serialises renewal,
and the cache is re-read under it, so concurrent commands send the
rotating refresh token once. invalid_grant deletes the keychain entry
only if it still holds the token sent. The rotated refresh token is
saved before the access token; a transient renewal failure keeps
using a still-valid token. Discarded refresh tokens are revoked at
login, and doctor says why it skipped the Tenant row.
Cover the per-profile session lock and stale-lock recovery, the
refresh token keychain entry kept apart from the client secret, the
Auth0 refresh and revoke calls, and a refused or unreachable renewal
ending in exit 5.
A refused refresh reported the session as expired or missing, which
sends a blocked user to sign in again for nothing. Pass on the reason
Auth0 gives, such as "user is blocked", when there is one.
@AntTheLimey
AntTheLimey force-pushed the feat/browser-login-refresh branch from bbe2f7d to 2f539c6 Compare October 23, 2026 13:58
@AntTheLimey
AntTheLimey marked this pull request as ready for review October 23, 2026 13:58
@AntTheLimey
AntTheLimey changed the base branch from feat/browser-login to main October 26, 2026 16:47
@AntTheLimey
AntTheLimey merged commit 737085e into main Oct 26, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant