Skip to content
English
  • There are no suggestions because the search field is empty.

04.02 Multi-Factor Authentication

SimpleRisk Core ships TOTP-based MFA via Google Authenticator or any compatible authenticator app (Authy, 1Password, Microsoft Authenticator). Enable per-user enrollment voluntarily, or set the system-wide MFA Required toggle to force every user through enrollment on next login.

Why this matters

A password alone is one credential. Even with strong policy enforcement (length, complexity, expiration, lockout), credential theft via phishing, malware, or breach-data reuse stays the single most common path to account compromise. MFA mitigates that path: an attacker with the password still can't authenticate without the second factor (the time-based one-time password from the user's authenticator app). For an application that holds risk register, audit findings, and compliance evidence, the cost of activating MFA is minutes; the cost of not activating it shows up the day a credential gets reused from a breach corpus.

The honest scope to know up front: SimpleRisk Core ships TOTP only. There's no support for WebAuthn/FIDO2 hardware tokens, no SMS-based codes (which is fine — NIST SP 800-63B has deprecated SMS as a second factor), no push-based authenticator integration. TOTP via an authenticator app is the only second-factor option. For most programs this is sufficient (it satisfies NIST SP 800-63B AAL2 when combined with a memorized secret); for programs that require AAL3, the TOTP-only posture isn't enough on its own.

The other thing worth knowing: Core has no built-in recovery code mechanism. If a user loses their authenticator device (phone replaced, app deleted, device wiped), there's no "recovery code from when you enrolled" path to log them back in. The recovery is admin-initiated: an admin clears the user's MFA secret, the user re-enrolls on next login. Plan for this; without an admin-managed recovery path, lost-device events become "reset the user via the database" operations.

The third thing: MFA applies to the local-auth login path. Users authenticated via SAML or LDAP/AD have their MFA posture determined by the IdP; the SimpleRisk MFA toggle doesn't insert a TOTP step into the SSO flow. If your IdP doesn't enforce MFA, SimpleRisk won't either for SSO users; configure MFA at the IdP layer for SSO-authenticated users.

Before you start

Have these in hand:

  • Admin access to Configure → Settings (for the mfa_required toggle) and to Configure → User Management (for clearing a user's MFA secret on lost-device recovery).
  • A communication plan for users if you're enabling mfa_required. The toggle forces every user through enrollment on their next login; users who don't have an authenticator app installed can't proceed until they do. A 24–48-hour heads-up reduces the day-of help-desk volume.
  • An authenticator app standard for the organization, if you have one (1Password, Authy, Microsoft Authenticator, Google Authenticator are all compatible). The Core flow renders a QR code that any RFC 6238 / TOTP-compatible app can read; you don't have to standardize, but having a recommendation in the user-facing announcement reduces "which app should I use?" questions.
  • A recovery process documented for "user lost their device." Without this, the first lost-device event becomes an unplanned database operation. The process is straightforward; just write it down.

Step-by-step

1. Decide whether to require MFA system-wide or leave it per-user

Two postures:

  • Voluntary per-user MFAmfa_required is off. Users can opt into MFA themselves at My Account → Multi-Factor Authentication. The bar to entry is on the user; some will enroll, most won't. Appropriate only for low-risk programs or as a stepping stone to forced-MFA.
  • System-wide required MFAmfa_required is on. Every user is forced through enrollment on their next login (or immediately, if currently logged in). This is the right posture for any production program; the security delta from "voluntary" to "required" is meaningful and the operational cost is small (the enrollment flow takes 60 seconds).

Default to required. Voluntary MFA is a configuration that audits keep flagging.

2. Enable MFA system-wide

Sidebar: Configure → Settings. Find MFA Required (or the equivalent label; the underlying setting is mfa_required in the settings table). Toggle on, save.

What happens immediately:

  1. Every user without user.multi_factor = 1 (the per-user enrollment flag) is forced through the enrollment flow on their next login.
  2. Currently-logged-in users without MFA enrolled keep their session until it expires; they'll hit the enrollment flow on the next login.
  3. The next login attempt for an unenrolled user lands on the QR-code enrollment page; they can't proceed to the application until they scan the code and verify a token.

The system-wide setting also exposes the admin function enable_mfa_for_all_users() which can be triggered to force-enroll every user immediately rather than on next login; less common operationally but available.

3. The user enrollment flow

What the user sees on first MFA-enforced login:

  1. Username + password as normal.
  2. Enrollment page: a QR code rendered by get_mfa_qr_code_image(), plus the secret as a text string (for users who can't scan QR codes — manual entry).
  3. The user opens their authenticator app, scans the QR code (or types the secret).
  4. The app starts producing 6-digit codes that rotate every 30 seconds.
  5. The user enters the current code into the Verify Token field.
  6. The page validates the token via does_mfa_token_match(). Success: user.multi_factor is set to 1, the secret is stored in user_mfa, the user proceeds to the application.
  7. On every subsequent login: username + password + token from the authenticator app.

The QR-code image is rendered inline (no external service called). The secret is stored in the user_mfa table; the last_mfa_token column is updated on each successful verification to provide replay protection (the same token can't be used twice).

4. Per-user voluntary enrollment (when mfa_required is off)

Users open My Account → Multi-Factor Authentication to opt in. The flow is the same QR-code-then-verify enrollment described above. Once enrolled, every subsequent login requires a token; the user can disable MFA from the same page (which clears user.multi_factor and removes their user_mfa row).

When mfa_required is on, the disable option is suppressed (or fails silently); users can't opt out of system-wide-required MFA.

5. Test the enrollment with a non-admin account

Verify the configuration end-to-end before enforcing it across the organization:

  1. Create a test non-admin user.
  2. Log in as that user; confirm the enrollment flow appears.
  3. Enroll an authenticator app, complete the verification.
  4. Log out and log back in; confirm the token prompt appears and an entered token is accepted.
  5. Log out, attempt to log in with a wrong token; confirm the rejection.
  6. Log out, attempt to log in with the previously-used token (replay); confirm rejection (the last_mfa_token check prevents reuse).

If the enrollment flow doesn't appear when expected, check that mfa_required is actually 'true' (not the literal string '1' or vice versa — the codebase uses 'true'/'false' for many boolean settings).

6. Recovery: user lost their authenticator device

When a user's authenticator app is gone (new phone, deleted app, factory reset), the recovery path is admin-initiated:

  1. Verify the user's identity out-of-band (phone call, in-person, badge check — whatever your program requires).
  2. Open Configure → User Management → [user].
  3. Find the Reset MFA action (or equivalent label). This clears the user's user_mfa row and resets user.multi_factor = 0.
  4. The user logs in next; they're forced back through enrollment with a fresh QR code.
  5. They scan the new code with their replacement authenticator and complete verification.

Document the identity-verification step. Without it, "I lost my phone" becomes a social-engineering vector for credential theft. A phone call to a number on the user's HR record, plus a question or two only the real user would know, is the minimum bar; programs with stricter requirements use video verification or in-person.

7. Handle org-wide events that affect MFA

A few patterns to plan for:

  • Mass device migration (organization moves from BYOD authenticator to managed authenticator app): users will need to re-enroll. Either trigger admin resets en masse or schedule a maintenance window where users are guided through the migration.
  • Authenticator app deprecation (Google Authenticator's lack of cloud backup historically led to lost-token events when users switched phones): consider standardizing on an authenticator with cloud backup (Authy, 1Password, Microsoft Authenticator).
  • Departing admin with the only admin account on MFA: if the admin can't be recovered, the MFA secret can be cleared directly in the database (DELETE FROM user_mfa WHERE uid = ; UPDATE user SET multi_factor = 0 WHERE id = ; ) but this is a break-glass move. The right preventive: have at least two admin accounts, both enrolled in MFA.

8. Combine with SSO (Authentication Extra users)

For users who authenticate via SAML or LDAP (the Authentication Extra), the SimpleRisk MFA flow doesn't apply — their user.type is not 'simplerisk' and the local-auth path that triggers MFA isn't taken. MFA enforcement for those users belongs to the IdP:

  • SAML IdPs (Azure AD, Okta, ADFS, Auth0, etc.) all support MFA configuration at the IdP layer. Configure MFA there.
  • LDAP/AD with MFA: the LDAP bind itself doesn't carry MFA. If MFA is required for AD accounts, it has to happen at a layer the LDAP bind sees (e.g., Conditional Access in Azure AD, third-party MFA solutions integrated with on-prem AD).

Don't assume "we have MFA" because SimpleRisk's MFA is on; verify the SSO-side MFA posture independently.

Common pitfalls

A handful of patterns recur with MFA.

  • Toggling mfa_required without warning users. Users hit the enrollment flow on next login, can't proceed without an authenticator app installed, and call the help desk. Warn 24–48 hours in advance with a recommended app name.

  • Voluntary MFA as the long-term posture. Voluntary MFA produces low enrollment rates and audits keep flagging it. The path of least resistance is usually to flip to required and absorb the one-day support spike.

  • No documented lost-device recovery. The first lost-device event becomes an unplanned database operation. Document the recovery and the identity-verification step before you need them.

  • Skipping identity verification on MFA reset. "Hi, I lost my phone, please reset my MFA" is a social-engineering script. Always verify out-of-band before clearing the secret.

  • Assuming SSO users have MFA because SimpleRisk's MFA is on. SSO users bypass SimpleRisk's MFA; the SSO IdP's posture is what matters. Confirm separately.

  • Confusing the mfa_required system-wide toggle with per-user enrollment. mfa_required is a requirement, not an auto-enrollment. Users still enroll themselves (via the forced flow on next login); the toggle just removes the opt-out. There's no "auto-enroll all users with these specific secrets" path.

  • Storing the QR-code secret somewhere recoverable. The QR-code secret is sensitive (anyone with it can produce the user's TOTP codes). Don't paste it into the support ticket, the chat thread, or the helpdesk note. The user scans it; the system stores the hash in user_mfa; nobody else needs it.

  • Disabling MFA to "let the user in just this once." A user's MFA being disabled means their account is now password-only; if the password was the compromised credential, the disable just opened the door. Either reset MFA properly (via the enrollment flow) or have the user use a temporary admin-issued password and re-enroll immediately.

  • Locking out the only admin account with MFA when the device is gone. Recoverable from the database, but better avoided. Have a second admin account.

Related

Reference

  • Permission required: check_admin for the system-wide toggle and for the per-user reset; users can self-enroll/disable from their own profile when mfa_required is off.
  • API endpoint(s): None specific to MFA configuration; user-management endpoints don't expose MFA secrets.
  • Implementing files: simplerisk/includes/mfa.php (get_mfa_qr_code_image(), does_mfa_token_match(), is_mfa_verified_for_uid(), process_mfa_verify(), enable_mfa_for_all_users(), disable_mfa_for_unverified_users()); simplerisk/account/mfa.php (the user-facing enrollment surface); simplerisk/admin/user_management.php (admin-initiated reset).
  • Database tables: user (multi_factor column — per-user enrollment state); user_mfa (uid, secret, timestamp, last_mfa_token, verified — the per-user secret and replay-protection state).
  • config_settings keys: mfa_required (system-wide enforcement toggle).
  • TOTP library: PragmaRX\Google2FA (in simplerisk/vendor/) — RFC 6238 TOTP implementation; QR codes rendered inline with no external service.
  • External dependencies: An authenticator app on the user's device (Google Authenticator, Authy, 1Password, Microsoft Authenticator, etc. — any RFC 6238 implementation).