Two-Factor Auth
Two-factor authentication is not part of the Voyti core - it lives in its own package
family. The core distribution carries no 2FA code or data at all; it only exposes the
seams that let a 2FA package hook into the login flow. To add 2FA you install the base
package yiirocks/voyti-2fa together with at least one method
package (email, TOTP, WebAuthn, ...). 2FA becomes active as soon as a method is
registered - there is no separate on/off flag.
Installation
Install the base package plus one or more method packages. With
yiisoft/config present (the standard way to run a Yii3 app) everything
wires itself up - the routes, the login-confirmation step, the settings screen, and the
enforcement middleware all register automatically, with no host code.
Each method is its own Composer package. Install one and it registers itself: its button appears on the settings screen's method switcher and it becomes selectable in the login confirmation step. Multiple method packages can be installed side by side; a user's active method is remembered per account.
Mails a fresh six-digit code at the start of the confirmation step; needs only a configured mailer.
TOTP
Time-based One-Time Password (TOTP) using an authenticator app like Google Authenticator, Authy, or Microsoft Authenticator. Users scan a QR code during setup to register the account, then enter time-based codes generated by their app during login.
WebAuthn
Passwordless authentication using biometrics (fingerprint, face recognition) or hardware security keys. The credential is registered on the user's device and verified during login without requiring a typed code.
The base package ships a migration creating its user_two_factor and
user_backup_code tables; some method packages ship their own tables too
(WebAuthn adds user_webauthn_credential). Their migration paths are
registered for yiisoft/db-migration automatically, so a host's migrate
command picks them up. See Routes for the full route list
these packages contribute.
How it works
A successful password login can be interrupted before the session is established by a
login challenge. Voyti core collects every service tagged
voyti.login-challenge and consults them in turn; the first one to return a
response short-circuits login with that response. yiirocks/voyti-2fa
contributes the two-factor challenge: when the user has an enabled, registered method it
stashes the pending credentials, starts the method's step, and renders the confirmation
screen (route voyti/session-confirm). Core references none of this - it only
knows the tag.
A user's 2FA state (enabled flag, active method, method secret) lives in the base
package's UserTwoFactor record, kept out of the core user
table. On enabling any method, Voyti generates ten single-use backup codes (hashed via
the base package's BackupCodeService); a user can regenerate them from
settings/two-factor/backup-codes/regenerate after re-verifying their
current method.
Enforcing 2FA for a permission
To require 2FA for the holders of a given permission, list those permissions under the
base package's forcedPermissions param:
// config/params.php
return [
'yiirocks/voyti-2fa' => [
'forcedPermissions' => ['admin'],
],
];
The base package's TwoFactorAuthenticationEnforceMiddleware redirects such
a user to the setup screen (with an explanatory flash) until they enable 2FA. It is
tagged voyti.enforce-middleware, so it joins core's
VoytiMiddleware chain automatically once the package is installed - a host
using VoytiMiddleware needs no extra wiring. See
Middleware for that chain.
Writing a method plugin
A method plugin is a small Composer package that depends on
yiirocks/voyti-2fa and does two things: register a provider class
implementing TwoFactorMethodInterface, and contribute its setup routes.
Both happen through yiisoft/config, so a host that installs the package
needs no extra wiring.
1. Register the method (DI tag). Tag the provider class with
voyti.two-factor-method in the package's config/di.php. The
base package's TwoFactorMethodRegistry collects every tagged provider, keyed
by its getName():
// config/di.php
use YiiRocks\Voyti\TwoFactor\Totp\TotpTwoFactorMethod;
return [
TotpTwoFactorMethod::class => [
'class' => TotpTwoFactorMethod::class,
'tags' => ['voyti.two-factor-method'],
],
];
2. Contribute setup routes (params splice). Append the method's
routes to the twoFactorMethodRoutes key under yiirocks/voyti
in the package's config/params.php. The base package splices them into its
own settings/ group, so they inherit the login requirement and the session
and CSRF middleware with no duplication:
// config/params.php
use YiiRocks\Voyti\TwoFactor\Totp\Controller\TotpController;
use Yiisoft\Router\Route;
return [
'yiirocks/voyti' => [
'twoFactorMethodRoutes' => [
Route::get('two-factor/totp/')
->name('voyti/user-two-factor-totp')
->action([TotpController::class, 'settings']),
],
],
];
Because params is merged recursively and route lists are appended, any
number of method packages can contribute at once - the lists concatenate rather than
collide. Keep route names and paths unique per method (the
voyti/user-two-factor-<name> convention avoids clashes, and is
the name your method's getSettingsUrl() generates). A client-collected
method that needs a guest-accessible confirmation fragment (like WebAuthn) registers
that one route as its own top-level group, reusing
VoytiRoutes::webMiddleware() for the session and CSRF stack.
The interface. TwoFactorMethodInterface
(namespace YiiRocks\Voyti\TwoFactor, provided by the base package) is the
whole contract:
| Method | Purpose |
|---|---|
getName() | The exclusive name the method is stored under in UserTwoFactor (e.g. 'totp'); also the registry key. |
isAvailable() | Whether the backing library is installed. Unavailable methods are hidden and never chosen as the default - the graceful self-disable hook. |
isCodeBased() | true for a user-typed code (TOTP, email), false for a client-collected payload (WebAuthn). |
requiresCodeDelivery() | Whether a code must be delivered before the user can enter it (email) versus available on demand (TOTP, WebAuthn). Drives the disable flow's "send a code first" pre-step generically. |
verify($user, $data) | Validate the attempt. $data is ['code' => ...] for code-based methods, ['payload' => ...] for client-collected ones. |
getSettingsUrl($url) | GET route for the method's settings screen; the settings page also fetches it to lazy-load the setup fragment. |
getConfirmFragmentUrl($url) | GET route for the login-confirmation fragment, or null for code-based methods (their code form is rendered inline). |
getButtonLabel($translator) | Short label for the method-switch button. |
getEnabledWithMethodName($translator) | Name shown in the "2FA is enabled with {method}" message. |
getErrorMessage() | Translated error from the last failed verify(); empty on success. |
onAuthenticationStepStart($user) | Runs when a login reaches this method's confirmation step (e.g. email a fresh code); a no-op for methods with nothing to send. |
onDisable($user) | Runs when 2FA is disabled entirely, to clear method-specific state. |
Enrollment and storage. A code-based method's setup route persists its
secret to the user's UserTwoFactor record (via
UserTwoFactor::forUser($user)) and renders the setup fragment. Enrollment
then finalizes through the base package's generic
voyti/user-two-factor-enable route, which calls the method's
verify() and, on success, records the active method and issues backup codes.
Disable, re-verification, and method resolution are all handled generically by the base
package's TwoFactorController - a plugin never touches them. A method that
needs its own table (as WebAuthn does for stored credentials) ships its own model and
migration, whose user_id foreign key is declared
ON DELETE CASCADE so the rows are removed with the user.