Skip to content

feat: break-glass admin that no sign-in policy can lock out - #494

Open
Bccorb wants to merge 14 commits into
mainfrom
feat/break-glass-admin
Open

Bccorb wants to merge 14 commits into
mainfrom
feat/break-glass-admin

Conversation

@Bccorb

@Bccorb Bccorb commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Closes #401.

Depends on #491 (recovery codes, which merges #482 types 0.30.0 and #489 auth secrets) and #485 (admin identity change notice, for sendNoticeEmail). This branch merges both, so the diff shrinks once they land.

Behaviour

BREAK_GLASS_EMAILS (comma separated, trimmed, lower-cased) names admin accounts that sign-in policy cannot lock out. An entry counts only when the account exists, is not revoked and holds a role satisfying admin:write (hasScopedRole). Boot logs how many listed entries are not admin accounts, never the addresses.

Where the exemption applies, for the account's own sign-in:

  • /login: adds the account's passkey when login_methods leaves passkeys out, and lets an account holding only TOTP or recovery codes continue (with a real pre-auth token) where it would otherwise get a decoy. The rest of loginMethods is shaped like any account's, so the answer does not single out a privileged account; an empty list (which only this account could produce) is never returned.
  • POST /totp/verify-login: skips the passkey-required check (phishing_resistant_only, or passkey with passkey_login_fallback_enabled: false). Lockout still applies.
  • Session issuance backstop (issueSessionAndRespond): passkey, TOTP and recovery code are allowed for the account in phishing-resistant-only mode.
  • Recovery codes: create and redeem work while recovery_codes.enabled is false. Redeeming still issues the restricted recovery session.
  • WebAuthn registration: the pinned authenticator_policy.attachment and the evaluateAuthenticatorPolicy verdict (synced passkeys, AAGUID allow/deny lists, requireKnownAuthenticator) do not refuse the account. The override is marked on registration_success with severity: "high" and breakGlassWaived.

What it still needs: a passkey, TOTP or a recovery code. Email OTP and SMS OTP (send and verify), the verified-account path of /otp/verify-email-otp, magic links (request and poll) and OAuth (finishOAuthLogin) answer 403 login_method_disabled, and session issuance throws BreakGlassFactorError for those methods as a backstop.

Audit and notices: a sign-in that relied on the exemption (the policy in force would otherwise have refused it) records break_glass_sign_in (category login). The audit model has no severity column, so the metadata carries severity: "high", plus factor, waived (login_methods, phishing_resistant_only, passkey_required, recovery_codes_disabled) and notices (sent, failed). Each non-revoked admin (admin:read or higher) with a verified email gets a notice via sendNoticeEmail. Notices go out after the session is issued and a failure never fails the sign-in.

Shared helper: src/config/breakGlass.ts exports breakGlassEmails(env = process.env) and isBreakGlassAdmin(user, env) (plus isBreakGlassConfigured), so #492's policyDryRun.ts can switch to the same parser at merge time.

Not done: no /health/status field. Its body is deliberately empty while healthy and schema-validated, so a boolean there is a contract change with no natural home.

Contract

break_glass_sign_in is added to the auth event type enum (openapi.json / src/generated/api.ts). No route or response schema changes. Behaviour visible to clients: a break-glass account gets 403 login_method_disabled from email/SMS OTP, magic link and OAuth even when they are enabled, and its /login list can include methods it is then refused.

Checks

  • npm run typecheck: pass
  • npm run lint: pass
  • npx prettier --check .: pass
  • npm run build: pass
  • npm run coverage: pass, 148 files, 2091 tests passed (1 todo); statements 98.84%, branches 95.37%, functions 99.04%, lines 99.06%
  • npm run generate:api: output committed
  • The new tests fail with src reverted (22 failures across the break-glass integration spec, the OAuth spec, the session issuance spec, and the two new unit specs, which do not load).

Bccorb added 14 commits October 22, 2026 14:46
A user can generate single-use codes and sign in with one; the session it issues can only
enroll a new factor until one is enrolled.
Accounts in BREAK_GLASS_EMAILS that hold admin:write are exempt from login policy for their own
sign-in but need a passkey, TOTP or a recovery code. Every use that relied on the exemption is
audited at high severity and emailed to every admin.

Closes #401.
A locked account with no passkey, or one the policy leaves with no method, continues with a
pre-auth token when it holds an unused recovery code.
…reak-glass-admin

# Conflicts:
#	src/controllers/authentication.ts
…hange-notice

# Conflicts:
#	src/controllers/admin.ts
# Conflicts:
#	docs/security-posture.md
#	resources/coverage-badge.svg
#	src/controllers/authentication.ts
#	src/services/sessionIssuance.ts
# Conflicts:
#	src/controllers/admin.ts
#	src/controllers/oauth.ts
#	src/controllers/otp.ts
#	src/services/sessionIssuance.ts
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.

feat: break-glass admin that no policy can disable

1 participant