Skip to content

feat: admin-set 'require 2fa' override for key staff#286

Open
leowilkin wants to merge 1 commit into
mainfrom
feat/require-2fa-override
Open

feat: admin-set 'require 2fa' override for key staff#286
leowilkin wants to merge 1 commit into
mainfrom
feat/require-2fa-override

Conversation

@leowilkin

@leowilkin leowilkin commented Jul 3, 2026

Copy link
Copy Markdown
Member

what

adds a require 2fa override to the backend identity overrides section (next to hq_override / disallow slack). it's for key staff who use HCA to sign into sensitive apps but aren't HCA admins — admins are already forced onto TOTP/passkey by Backend::ApplicationController#require_2fa!, but nothing covered this group.

how it works

new two_factor_required boolean on identities (normal-admin editable; goes through the existing reason + admin_update audit flow).

user already has a 2FA methodIdentity#requires_two_factor? now also honors the override, so the existing LoginAttempt machinery demands a second factor on every login. the user can no longer disable the requirement or remove their last method — toggle_2fa and the step-up disable_2fa / remove_totp / remove_passkey completions all refuse (removing one method while another remains is still allowed). the security page shows "required by Hack Club HQ" instead of the disable button.

user has no 2FA method yet (the chicken-and-egg case — you can't enroll without a session) → the email-code login completes, but a new ApplicationController gate (require_two_factor_enrollment!) redirects every request to /security until they enroll. crucially this includes /oauth/authorize (doorkeeper's base_controller is ApplicationController) and SAML, so they can't reach sensitive apps un-enrolled. exemptions are limited to the security page and its htmx panels (totp/passkey/backup-code/sessions/authorized-apps), login, step-up, and logout. htmx requests get an HX-Redirect header instead of a 302 so the panels don't loop.

flipping the override takes effect immediately for existing sessions (the gate runs per-request).

testing

  • request specs: gate blocks / and /oauth/authorize, exempt paths don't loop, gate lifts once a TOTP is verified, unaffected users pass through; all disable/removal refusal paths; last-method vs not-last-method removal
  • model specs: requires_two_factor? / two_factor_enrollment_required? truth tables
  • manually verified end-to-end in dev: set override in backend → email-code login bounces to /security with the enrollment banner → enroll TOTP → next login demands TOTP → disable refused
  • security review run on the branch: no findings

screenshots

Adds a two_factor_required boolean override on identities, editable from
the backend overrides section. Once set:

- if the user has a 2FA method, every login demands a second factor
  (via the existing LoginAttempt required-factors machinery)
- if they don't, they can log in with an email code but every request
  (including /oauth/authorize and SAML) redirects to /security until
  they enroll a TOTP or passkey
- the user can't disable the 2FA requirement or remove their last
  method (toggle_2fa and step-up completion paths refuse)

Known limitation, flagged for follow-up: previously issued OAuth access
tokens keep working (API layer isn't gated); consider revoking tokens
when the override is enabled.
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