Voyti

Voyti

User management, authentication & authorization

Two-Factor Authentication

Two-factor authentication is optional and pluggable: the core carries no 2FA code of its own, only the seams that let method packages hook into login. Install one or more method packages to activate 2FA.

After password login succeeds, 2FA checks if the user has an enabled method. If so, it holds the session pending 2FA verification, runs the method’s confirmation step (e.g., email code, TOTP entry), and verifies the result. On success, the login completes. Backup codes are auto-generated when enabling 2FA, allowing account recovery if the primary method is unavailable.

Email
Email

Mails a random six-digit code at the start of the confirmation step; needs only a configured mailer.

Route name Method Path Purpose
voyti/user-two-factor-email GET settings/two-factor/email/ Email setup fragment
voyti/user-two-factor-email-send-code POST settings/two-factor/email/send-code Send the email one-time code
TOTP
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.

Route name Method Path Purpose
voyti/user-two-factor-totp GET settings/two-factor/totp/ TOTP setup (QR code)
voyti/user-two-factor-totp-renew POST settings/two-factor/totp/renew Issue a new TOTP secret/QR
WebAuthn
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.

Route name Method Path Purpose
voyti/user-two-factor-webauthn GET settings/two-factor/webauthn/ WebAuthn setup (registration ceremony)
voyti/user-two-factor-webauthn-register POST settings/two-factor/webauthn/register Complete WebAuthn registration
voyti/user-two-factor-webauthn-confirm GET confirm/webauthn WebAuthn login-confirmation fragment (guest-accessible)
2FA Base Package
2FA Base Package

Pulled in automatically as a dependency by method packages, the base package supplies shared routes, backup codes, and database tables.

Route name Method Path Purpose
voyti/session-confirm GET, POST confirm Two-factor login-confirmation step (guest-accessible mid-login)
voyti/user-two-factor GET, POST settings/two-factor/ Two-factor status/entry point
voyti/user-two-factor-enable POST settings/two-factor/enable Enable 2FA - shared by every code-based method’s code-entry form
voyti/user-two-factor-disable POST settings/two-factor/disable/ Disable 2FA
voyti/user-two-factor-disable-send-code POST settings/two-factor/disable/send-code Send the disable-2FA one-time code (for methods that deliver a code)
voyti/user-two-factor-backup-codes GET settings/two-factor/backup-codes Display the user’s backup codes
voyti/user-two-factor-regenerate-backup-codes POST settings/two-factor/backup-codes/regenerate Invalidate existing backup codes and generate a fresh set (requires re-verifying the current method)
// config/params.php
return [
    'yiirocks/voyti' => [
        '2fa' => [
            'forcedPermissions' => ['voyti-admin'],
        ],
    ],
];
forcedPermissions array
[]
RBAC permissions whose holders must have 2FA enabled. Users with any of these permissions are redirected to 2FA setup until they enable a method. Enforced by TwoFactorAuthenticationEnforceMiddleware.
// config/params.php
return [
    'yiirocks/voyti' => [
        '2fa' => [
            'email' => [
                'maxAttempts' => 3,
            ],
        ],
    ],
];
codeLifespan int
600
Seconds for which an emailed verification code remains valid.
maxAttempts int
5
Maximum verification attempts allowed for one emailed code. Requesting a new code resets the limit.

The base package registers one console command under yiisoft/yii-console:

Command Description
voyti:2fa:disable Disable two-factor authentication for a user, bypassing re-authentication
--email string
optional
Disable 2FA for user by email address
--username string
optional
Disable 2FA for user by username
--id int
optional
Disable 2FA for user by ID
php yii voyti:2fa:disable --email=user@example.com
php yii voyti:2fa:disable --username=johndoe
php yii voyti:2fa:disable --id=42
  1. Tag the provider class with voyti.two-factor-method in config/di.php. The registry collects tagged providers, keyed by getName():

    // config/di.php
    use MyNamespace\MyTwoFactorMethod;
    
    return [
        MyTwoFactorMethod::class => [
            'class' => MyTwoFactorMethod::class,
            'tags' => ['voyti.two-factor-method'],
        ],
    ];
  2. Append routes to yiirocks/voyti2famethodRoutes in config/params.php. They’re spliced into the base package’s settings/ group, inheriting login guards and CSRF middleware:

    // config/params.php
    use MyNamespace\MyMethodController;
    use Yiisoft\Router\Route;
    
    return [
        'yiirocks/voyti' => [
            '2fa' => [
                'methodRoutes' => [
                    Route::get('two-factor/my-method/')
                        ->name('voyti/user-two-factor-my-method')
                        ->action([MyMethodController::class, 'settings']),
                ],
            ],
        ],
    ];

    Route lists merge and append, so multiple method packages coexist without collision. Use the voyti/user-two-factor-<name> convention; that’s what getSettingsUrl() generates. Client-collected methods (WebAuthn) register their guest-accessible confirmation fragment as a top-level route group using VoytiRoutes::webMiddleware().

  3. TwoFactorMethodInterface (namespace YiiRocks\Voyti\TwoFactor, provided by the base package) is the contract you must implement:

    Method Purpose
    getName() The exclusive name the method is stored under; 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.
    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).
    getReauthFragmentUrl($url) GET route for the re-authentication fragment used by sensitive actions (disable 2FA, regenerate backup codes), or null for code-based methods (their code is rendered inline). For client-collected methods like WebAuthn, this fragment runs the assertion ceremony.
    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.

Your setup routes persist secrets and render the enrollment UI. The base package handles finalization (verification, backup code generation) and teardown (disable, re-verify) generically - your plugin doesn’t need to touch those. If your method needs a custom table, ship a model and migration with ON DELETE CASCADE foreign keys.