Two-Factor — Reference
The Two-Factor surface is the second-step authentication plane for SGEN. It owns enrollment, verification, recovery, and the policy that decides when a second factor is required. Every operator session that includes a 2FA prompt traces back to a record managed here.
This page is a reference for platform engineers and integrators who need to understand the surface before extending it, scripting against it, or scoping a policy. Customer-facing how-tos live in the customer docs set; this page describes the shape of the surface, not the steps to drive it.
Overview
Two-Factor lives under the Security module in SG-Admin, with operator-facing controls mirrored inside the Profile surface. The module renders three primary views — the enrollment wizard, the per-operator status view, and the site-level enforcement policy — and exposes a small set of write operations for enrollment, verification, regeneration, and reset.
The module is paired by convention: one half renders the views and prepares the enrollment material, the other half handles writes and returns AJAX responses. Engineers reading the SG-Admin source will see this split across two controller files; the reference below describes the combined surface as it appears to a calling integration.
A secondary surface — recovery codes — lives adjacent to the enrollment view. Recovery codes are one-time strings issued at enrollment and presented again only by explicit regeneration. They are treated as a separate concern from the primary factor because they bypass the time-based step.
Where it lives in SG-Admin:
- Sidebar: SG-Admin → Security → Two-Factor
- URL prefix:
/sg-admin/two-factor/ - View templates:
application/views/Admin/Security/TwoFactor/
┌──────────────────────────────────────────────────────────────────────┐│ SG-Admin → Security → Two-Factor │├──────────────────────────────────────────────────────────────────────┤│ Operator Method Enrolled Last verified ││ ───────────────── ──────────── ────────────── ──────────────────││ jerome@sgen.com TOTP (app) 2026-03-14 2 minutes ago ││ james@sgen.com SMS fallback 2026-04-02 yesterday ││ maria@sgen.com — not enrolled — ││ ││ Site policy: Required for Administrators · Optional for others ││ [Enrol] [Regenerate recovery codes] [Reset operator] │└──────────────────────────────────────────────────────────────────────┘Actions
The Two-Factor surface exposes the following operations. Each is described by what it does to the data, not by its internal method name.
Begin enrollment
Issues a fresh secret for the calling operator, encodes it as both a base32 string and a otpauth:// URI suitable for QR rendering, and stores the secret in a pending state. The secret is not yet active — verification of one valid code is required before the surface considers the operator enrolled. A pending enrollment is overwritten if the operator restarts the wizard.
Verify and activate
Accepts a code generated from the pending secret. On a match within the configured time window, the surface marks the operator as enrolled, records the enrollment timestamp, and issues the initial set of recovery codes. The pending secret becomes the active secret.
Verify (per session)
The runtime verification path called by the login surface. Accepts a code, checks it against the active secret with the configured time skew, and returns success or a structured rejection. Repeated failures are subject to throttling — see Permissions below for the lockout rule.
Enrol SMS fallback
Stores a verified phone number as a fallback channel for operators whose primary factor is unavailable. The number is verified by a one-time code at registration. SMS is treated as a fallback, not a primary — the surface does not allow SMS as the only factor on accounts where the site policy requires a primary authenticator.
Regenerate recovery codes
Discards the existing recovery code set for the calling operator and issues a fresh set. The new codes are returned once, in plaintext. Subsequent reads return only a count and a generated-at timestamp. Regeneration is destructive — prior codes stop working immediately.
Use a recovery code
Consumes one recovery code from the operator's set during the login flow. The code is single-use; on a successful match the surface marks the code as spent and proceeds with the session. Recovery code verification bypasses the time-based factor but is logged with a distinct event type.
Reset operator (administrator-only)
Clears all 2FA state for a target operator — active secret, recovery codes, SMS fallback. The target operator will be prompted to re-enrol at next login if site policy requires it. Reset is irreversible and audit-logged.
View site policy
Returns the current enforcement rule — which roles are required to enrol, which are optional, and the grace period in days for newly required roles. The policy is read-only on this surface; writes happen under Settings → Security.
Update site policy
Lives under Settings → Security; mentioned here because the read view on this surface is the operator-facing summary. See the Settings — Reference for the write contract.
Data model
A two-factor record carries the following fields. Field names below are the conceptual shape — the on-disk column names match closely but are not contractually stable across releases.
| Field | Type | Notes |
|---|---|---|
user_id | integer | Foreign key to the Users surface. One record per operator. |
method | enum | totp, sms, or none for unenrolled. |
secret | string | Encrypted at rest. Never exposed in read responses. |
secret_status | enum | pending (awaiting verification) or active. |
phone_e164 | string | Optional. Fallback channel. Verified at registration. |
recovery_codes_hashed | array | Hashed codes. Read responses return count only. |
enrolled_at | timestamp | Set on first successful verification. |
last_verified_at | timestamp | Updated on every successful runtime verification. |
failed_attempts | integer | Resets on success. Drives the throttle below. |
created_at | timestamp | Set on first enrollment attempt. |
updated_at | timestamp | Touched on any state change. |
Recovery code semantics: codes are stored as hashes. The plaintext set exists only in the response that issued them. Operators who lose the plaintext set must regenerate.
TWO-FACTOR RECORD├── user_id integer FK → Users├── method enum totp | sms | none├── secret string encrypted, never returned├── secret_status enum pending | active├── phone_e164 string optional SMS fallback├── recovery_codes array hashed, count returned only├── enrolled_at timestamp set on first verification├── last_verified_at timestamp updated on each success├── failed_attempts integer resets on success└── timestamps created_at, updated_at↓ on admin resetALL FIELDS CLEAREDoperator re-prompted at next login(subject to site policy)Permissions
Access to the Two-Factor surface is gated at two layers.
Layer 1 — admin gate. Every action under SG-Admin passes through the platform's standard admin access check at request entry. An unauthenticated request never reaches the Two-Factor surface. The login-time verification path is the single exception — it accepts a partially authenticated session that has passed the password step and is awaiting the second factor.
Layer 2 — per-action capability. Within SG-Admin, each Two-Factor action checks a capability associated with the calling operator's role. The default role configuration ships with three roles — Administrator, Editor, Viewer — and the capability map is:
| Capability | Administrator | Editor | Viewer |
|---|---|---|---|
| Enrol self | ✔ | ✔ | ✔ |
| Verify self (runtime) | ✔ | ✔ | ✔ |
| Regenerate own recovery codes | ✔ | ✔ | ✔ |
| Enrol SMS fallback | ✔ | ✔ | ✔ |
| View own status | ✔ | ✔ | ✔ |
| View any operator status | ✔ | — | — |
| Reset any operator | ✔ | — | — |
| Read site policy | ✔ | ✔ | ✔ |
| Write site policy | ✔ | — | — |
Throttle and lockout. The runtime verification path enforces a per-operator throttle. Five consecutive failed codes lock the operator out of the second-factor step for a cooldown window (default fifteen minutes). The cooldown resets on a successful code or on an administrator reset.
Audit. Every write — enrolment start, verification success, verification failure, recovery-code use, regeneration, reset, policy read — emits an Activity Log entry. The log records the acting operator, the target record, the method, and the outcome. Recovery-code use is logged with a distinct event type so audit reviewers can trace bypass events separately from primary-factor verifications.
LOGIN REQUEST (password verified)│▼┌───────────────────────────┐│ Policy check │ role requires 2FA → enforce│ (site policy + role) │ not required → bypass└─────────────┬─────────────┘│ required▼┌───────────────────────────┐│ Runtime verification │ code matches → success│ (TOTP / SMS / recovery) │ no match → increment attempts└─────────────┬─────────────┘│ 5 failures▼┌───────────────────────────┐│ Throttle / lockout │ 15-minute cooldown│ │ cleared by success or reset└─────────────┬─────────────┘│▼Activity Log entry(method · outcome · operator)Related references
- Users — Reference. The Two-Factor record is keyed on the user identifier. Permanent delete of a user cascades to remove the second-factor record.
- Settings — Reference. Owns the site enforcement policy, the time-window tolerance, the throttle parameters, and the cooldown window.
- Profile — Reference. The operator-facing entry points for self-enrolment and recovery-code regeneration are surfaced inside the Profile module, even though the writes target this surface.
- Tools — Reference. Administrator reset is listed in the Users surface but logged through the activity-log search under Tools.
- Forms — Reference. Login forms reach this surface through the verification path; failed-attempt counters interact with the form's submission throttle.
- Site Templates — Reference. Branded login screens that embed the second-factor prompt pull their copy from the site templates surface.
/docs/security/two-factor.