Repository navigation
Conversation
System config, OAuth provider, user, session, and organization admin writes now run
requireStepUp() after requireAdmin('write') and document the 403 step_up_required body.
Admin reads and enrollment invites are unchanged. Closes #437.
This was referenced Oct 22, 2026
# Conflicts: # openapi.json # src/generated/api.ts # src/routes/systemConfig.routes.ts
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.
Closes #437.
Behaviour
Admin writes that change who can sign in, or what an account can do, now run
requireStepUp()afterrequireAdmin('write'), the same middleware device replacement and the audit export already used. Without a WebAuthn or TOTP step-up on the calling session in the last five minutes they answer:{ "error": "step_up_required", "message": "Recent step-up authentication is required", "fresh": false, "method": null, "verifiedAt": null, "expiresAt": null, "maxAgeSeconds": 300 }The role check still runs first, so a caller without
admin:writekeeps getting403 { "error": "Forbidden" }. Each guarded route declares the403with a newStepUpRequiredErrorSchema(the canonical error body plus the optional step-up status fields), andopenapi.json/src/generated/api.tsare regenerated.adapter-manifest.jsonis unchanged.Admin reads are unguarded.
POST /admin/enrollment/invitesis left unguarded: it emails existing users a sign-in link and changes no account, and the console does not prompt before it.Guarded routes
PATCH /system-config/adminPOST /system-config/oauth-providersPATCH /system-config/oauth-providers/{id}DELETE /system-config/oauth-providers/{id}POST /admin/usersPOST /admin/users/importPATCH /admin/users/{userId}DELETE /admin/usersDELETE /admin/sessions/by-id/{id}DELETE /admin/sessions/{userId}/revoke-allPOST /admin/organizationsPATCH /admin/organizations/{organizationId}DELETE /admin/organizations/{organizationId}POST /admin/organizations/{organizationId}/membersPATCH /admin/organizations/{organizationId}/members/{userId}DELETE /admin/organizations/{organizationId}/members/{userId}PUT /admin/organizations/{organizationId}/oauth-providers/{providerId}/retirementDELETE /admin/organizations/{organizationId}/oauth-providers/{providerId}/retirementPOST /organizations/{organizationId}/members(the self-service route that requiresadmin:write)403documented:POST /admin/users/{userId}/recovery/device-replacement,GET /admin/auth-events/exportService token and non-interactive callers
No exemption, and none exists today for the audit export either. Admin routes take
auth: 'access', which accepts only an access token bound to a live session (validateBearerToken); the internal service token never authenticates an admin route. So the rule is the same for every caller: step up on the session the access token belongs to.Dashboard impact
The console already calls
useStepUpGuardbefore user create, edit and delete, session revocation, system config and OAuth provider writes, organization delete, and member add, update and remove, so those keep working.Two gaps, both in
seamless-auth-admin-dashboard:handleCreateOrganization,handleUpdateOrganizationinsrc/pages/Organizations.tsx) do not callensureStepUp(). Without a fresh step-up they now fail.403 step_up_required:friendlyStatusMessage(403)insrc/lib/api.tsshows "You do not have permission to perform this action." If the five-minute window lapses between the check and the call, or a write has no guard, the operator sees that message rather than a prompt. Suggested follow-up: treaterror === "step_up_required"as a cue to run the guard and retry.CLI impact (no CLI change in this PR)
seamless-cliauthenticates with an instance access token and has no step-up support (seamless-cli#245). Every admin write it makes now fails with403, whichcall()insrc/core/admin.tsmaps to a genericPermissionError:users delete(DELETE /admin/users)/admin/organizations...)migrate(POST /admin/users/import,src/core/migrate.ts)src/core/systemConfig.ts)prepare-device-replacementwas already blockedWhat it needs: the TOTP step-up proposed in #245 (
POST /totp/verify-mfaon the same session, code from a prompt, flag or stdin), applied to all of these commands rather than device replacement only, and a distinct message forstep_up_requiredinstead of the generic permission error.migratesends 200-row batches; a long run has to step up again once the five-minute window lapses. Admins with no TOTP enrolled cannot make these writes from the CLI.Tests
tests/integration/admin/adminStepUp.spec.tsdrives every guarded route through the realattachAuthMiddleware,requireAdminandrequireStepUp(vi.unmock): refused with no step-up, refused with a six-minute-old step-up, let through with a fresh one, role checked before step-up, a role change applied after step-up, and an admin read open without one. It also checks that the step-up403is documented on exactly the guarded routes.src/routesreverted to main, that spec fails 39 of 67 tests (every route not guarded before, plus the documentation check).beforeEach(buildFreshStepUpSession).Checks
npm run typecheck: passnpm run lint: passnpx prettier --check .: all files passnpm run build: passnpm run coverage: 142 files, 2044 passed, 1 todo. Statements 98.9%, branches 95.43%, functions 99.2%, lines 99.13%npm run check:changeset-bump: no major changesets (this one isminor)Contract note
This is a contract change (new
403on existing routes). Affected:seamless-auth-admin-dashboard(gaps above),seamless-cli(above), and any script or integration calling these routes with a plain admin token.seamless-auth-serverforwards API status and body through its shared responder, which is how the existing step-up 403 on device replacement and export already reaches the console; I did not re-test each new route through it.StepUpRequiredErrorSchemalives in this repo for now; moving it to@seamless-auth/typesis optional.