Skip to content

The sign-in hand-over delivers a personal token, not a session - #2

Merged
fxck merged 1 commit into
mainfrom
wt/AUTH-PERSONAL-TOKEN
Sep 1, 2026
Merged

The sign-in hand-over delivers a personal token, not a session#2
fxck merged 1 commit into
mainfrom
wt/AUTH-PERSONAL-TOKEN

Conversation

@fxck

@fxck fxck commented Sep 1, 2026

Copy link
Copy Markdown
Member

Platform half: zeropsio/frontend-legacy#300.

app.zerops.io now mints a personal access token for this client instead of redirecting the account's own refresh token. A personal token is already a bearer, so there is nothing to exchange — adoptPersonalToken replaces adoptHandedOverSession, and the /auth/refresh round trip goes with it.

The wire field is #token= rather than #refreshToken=.

What it buys

  • The client and app.zerops.io no longer hold the same credential, so the unmeasured refresh-token rotation question disappears rather than being managed.
  • The token is revocable on its own from Settings without disturbing the browser session.
  • It stays user-scoped, so the project picker still spans every organization — the thing that ruled out an integration token.

Notes for review

  • The token is proven before it is stored. A dead or revoked token that reached storage would render a signed-in-looking UI that fails on its first real call, so it is held in memory, spent on one /user/info read, and persisted only once that comes back. Pinned by a test asserting nothing is stored on a 401.
  • It carries no refresh token, deliberately. On a 401 the request path clears the session instead of trying to refresh — exactly right for a token the user revoked. Asserted rather than left implicit.
  • The nonce, the single-use read (readHandoverOnce), the fragment scrub and the door carve-out are unchanged from the previous PRs.

Verification

366 tests across the Zerops client surface, typecheck clean, lint clean on every changed file.

Exercised against the live API and a real browser, not just unit tests: a personal token minted through POST /user-token on a throwaway account, delivered to /zerops/authorized#token=…&state=…, produced a stored session of exactly { accessToken } — 66 chars, hasRefresh: false — and the picker rendered "1 container across your organizations." The probe token was revoked afterwards.

The full two-app loop cannot be exercised until #300 is deployed; everything up to and including the client's handling of a genuine token is covered.

app.zerops.io now mints a personal access token for this client instead of
redirecting the account's own refresh token. It is already a bearer, so there
is nothing to exchange — `adoptPersonalToken` replaces the refresh call.

What it does instead is prove the token before storing it: a dead or revoked
token that reached storage would render a signed-in-looking UI that fails on
its first real call. So it is held in memory, spent on one `/user/info` read,
and persisted only once that comes back.

It carries no refresh token, which is correct rather than a gap: on a 401 the
request path clears the session instead of trying to refresh, which is exactly
what should happen to a token the user revoked.
@fxck
fxck merged commit 325cb83 into main Sep 1, 2026
11 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