Repository navigation
feat(sessions): exchange a provider sign-in for a first-party session (ADR 0003) - #572
Merged
Merged
Conversation
An admin can turn token exchange on for a first-party public native client. Such a client gets the built-in resource with session:exchange and a 60 second token lifetime. Turning it off, or disabling or deleting the client, revokes the sessions it exchanged for. Refs #544
/oidc/sign now signs a session:exchange token only when the scope is exact, the audience is the built-in resource, jti is present and it lives at most 120 seconds. It writes the SessionExchangeToken record the exchange consumes. Refs #544
Replay sequential and concurrent, wrong audience, extra scope, expired record, disabled client, recovery parent, and the X5 assurance copy. Refs #544
A first-party native client presents the single-use provider access token and gets a first-party session linked to the provider sign-in. The record is consumed and the child created in one transaction, so of concurrent presentations one wins and the rest revoke what it created. An authorization code grant copies the parent's assurance and step-up, a device grant nothing. Refs #544
Parent check on validate and refresh, the lifetime cap, parent activity, and each revoke path ending the child and not the reverse. Refs #544
validateSessionRecord and the refresh path refuse a child whose parent no longer resolves to a live session. Rotation carries the link and is capped at the parent's lifetime, a child refresh counts as activity on the parent, and every revoke path cascades. A child is not counted against the concurrent session limit, since evicting a parent to make room for its own child would end both. Refs #544
requireAdmin answers 403 step_up_required to a session exchanged from a device grant until it has a fresh step-up of its own, reads included (ADR 0003, X7). Session lists show the parent, client and grant of a child session. Refs #544
The coverage gate runs without Postgres, so the exchange, signing, lifecycle and admin paths need coverage there too. Refs #544
A device child without a fresh step-up could enroll its own TOTP, then step up with it and pass X7. These fail until factor changes are gated. Refs #544
requireDeviceSessionStepUp answers 403 step_up_required on routes that change how an account signs in or recovers, unless a device child has a fresh step-up of its own: TOTP enrollment and disable, passkey registration, recovery codes, credential deletion, phone registration and cancelling account recovery. X7 shares the check. A device sign-in with no TOTP cannot enroll one, and the user does it from a browser. defineRoute now documents this 403 wherever the check applies, admin reads included. ADR 0003 is amended. Refs #544
It relied on an earlier spec having seeded the table, so it failed whenever it ran first. Refs #544
A device child read member emails and changed roles with no step-up, as a global admin or as an ordinary owner. These fail until the routes are gated. Refs #544
Organization routes check roles without requireAdmin, so a device child with no step-up could read member emails and change roles. Every organization route now uses requireDeviceSessionStepUp, reads included, as a global admin or an ordinary member or owner. Switching stays open: it keeps the same session and grant, and X7 still applies afterwards. Refs #544
Accepted from the registered key on the relying party's origin, refused from another origin or another key. Refs #544
A device sign-in steps up with the user's own factor, and the CLI uses TOTP. Organization routes need the step-up. Signing out other sessions stays open. Refs #544
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.
Implements ADR 0003: a first-party native client signed in through the OpenID Provider reaches the admin API by exchanging a single-use provider token for a child first-party session.
Refs #544, #551. Unblocks fells-code/seamless-cli#316.
Draft until
@seamless-auth/typeswith the new schemas is released. Then runnpm i @seamless-auth/types@^0.36.0and commitpackage.jsonand the lock. Until then CI fails at typecheck; local checks used a packed tarball.What it does
sessionExchangeEnabledfor first-party public native clients, with step-up and audit. Turning it off, or disabling or deleting the client, revokes its exchanged sessions./oidc/signsignssession:exchangeonly with the exact scope, the built-in audience, ajtiand at most 120 seconds, and writes the record the exchange consumes.POST /sessions/exchangeconsumes the token once in one transaction. A replay answers400 invalid_requestand revokes what the first use created.Device sessions (added during the build)
The record's first version gated only admin routes. A device session could enroll its own TOTP and then pass that check, and could replace a victim's TOTP; both were run against real Postgres.
requireDeviceSessionStepUpnow answers 403step_up_requiredwithout a fresh step-up on:/admin/*and everything behindrequireAdmin/totp/enroll/*,/totp/disable,/webauthn/register/*,POST /recovery-codes,DELETE /users/credentials/registration/phone,/registration/phone/verify,DELETE /recovery/requests/:idPOST /organizations/:id/switch, for global admins and ordinary members alikeA device sign-in with no TOTP cannot enroll one; the user does it from a browser. Signing out other sessions stays open, because it gains no access; the known cost is that it could slow the owner's response. A real passkey assertion is accepted as a step-up on a device session. All recorded in the ADR.
For review
Choices the ADR left open:
max_concurrent_sessionsand are never evicted./oidc/signrefusessession:exchangeon a refresh grant.Ship with fells-code/seamless-auth-oidc-provider#61. X8 (no silent sign-in, and the confirmation names the account) is enforced at the edge. Without it, a local program using the CLI's client id could get a silent sign-in for a user with a live browser session.
Security review
A static
/security-reviewof this branch reported nothing at or above its bar. It traced token verification, single use, grant type, the gates, revocation, client eligibility and data exposure. It ran no exploits and did not read the edge repo. It noted, below the bar: the X8 dependency above;isSessionExchangeClientaccepts a client with both native and https redirects (admin-configured only); and a phished device child can keep the approver's browser session alive up to its absolute lifetime (accepted as X4).Checks
From the implementer's final run: lint, typecheck, format and
generate:apiclean; unit and coverage 2913 passed, 99.07% lines;TEST_DB=postgreson a cold cache 3133 passed, 188 of 188 files, 158 insessionExchange.spec.ts. Re-run in review: unit tests undertests/unit(1810) and typecheck; the Postgres suite was not repeated. Test commits precede their implementations; each piece's Postgres tests were seen failing first.8f2ff8falso seedssystem_configinoidcApplications.spec.ts, which failed whenever it ran first.