Tfa

Base class for ProcessWire's two-factor authentication (TFA) system

Concrete TFA modules — such as TfaEmail (code sent via email) and TfaTotp (time-based one-time passwords) — extend this class and implement the module-specific validation and configuration logic.

The base class handles the login flow: credential verification, session management, the authentication-code form, browser "remember me" tracking, automatic TFA enforcement, and integration into the user profile editor.

$tfa = $modules->get('Tfa');

if($tfa->success()) {
    // TFA completed — user is now logged in
    $session->redirect('/after/login/url/');

} elseif($tfa->active()) {
    // TFA is in progress — render the code entry form
    echo $tfa->render();

} elseif($input->post('submit_login')) {
    // User submitted login form — start TFA if applicable
    $name = $input->post('name');
    $pass = $input->post('pass');
    $tfa->start($name, $pass);
    // If TFA is not active for this user, $tfa->start() returns true
    // and you proceed with your normal login here

} else {
    // Render your login form
}

Extends WireData. Implements Module and ConfigurableModule.

Expand all      API reference

Login flow

The TFA login flow follows a sequence of redirect-based steps:

  1. User submits credentials → call start($name, $pass). The method authenticates credentials via Session::authenticate(), then checks whether TFA is enabled for the user. If it is, start() redirects to the code-entry step. If not, start() returns true and the caller proceeds with normal login.

  2. Code entryactive() returns true when the URL contains the tfa GET parameter matching the session key. The caller renders the code form via render().

  3. Code submissionsuccess() returns true when the user has submitted a valid code and is now logged in. If the code is invalid, expired, or max attempts exceeded, process() will redirect back to the code entry step.

  4. Completedsuccess() returns true; caller redirects to the post-login destination.

start() ──► (TFA enabled) ──► redirect to code entry
         └─► (TFA not enabled) ──► returns true, normal login proceeds

active()? ──► render() ──► user enters code
success()? ──► true: logged in, redirect to destination
            └─► false: code invalid/expired, redirect back to code entry
Constants
ConstantValueDescription
userFieldName'tfa_type'Name of the FieldtypeModule field added to user templates on install
Properties

All properties are read/write via $tfa->set('key', $value), $tfa->key = $value, or $tfa->setArray([...]). The module configuration screen in the admin provides defaults; runtime overrides take priority.

Settings

PropertyTypeDefaultDescription
codeLengthint6Required length for authentication codes (set by subclass)
codeExpireint180Seconds before a pending code entry times out
codeTypeint0Code type (subclass-specific; see constants)
startUrlstring'./'Base URL for the login/TFA page
rememberDaysint0Days to remember a browser, 0 to disable, -1 for no limit
rememberFingerprintsarray['agentVL','accept','scheme','host']Browser fingerprint attributes to use for "remember me"
autoTypestring''Force a TFA module name for users who haven't enabled one
autoRoleIDsarray[]Role IDs to apply autoType to (empty = all roles)
showCancelbooltrueShow a cancel link below the code entry form
cancelMarkupstring"<p><a href='{url}'>{label}</a></p>"Markup template for cancel link ({url} and {label} placeholders)
formAttrsarray['id'=>'ProcessLoginForm','class'=>'pw-tfa']<form> element attributes for the code entry form
inputAttrsarray['id'=>'login_name','autofocus'=>'autofocus']<input> element attributes for the code field
submitAttrsarray['id'=>'Inputfield_login_submit']Submit button attributes

Text labels (translatable)

PropertyDefaultDescription
cancelLabel'Cancel'Cancel link text
configureLabel'Please configure'Notice shown when TFA type selected but not yet configured
enabledLabel'ENABLED'Badge appended to the TFA fieldset when already enabled
enabledDescLabelDescription when TFA is enabled, explaining how to disable/change
expiredCodeLabel'Expired code'Error for an expired authentication code
fieldTfaTypeLabel'2-factor authentication type'Label for the tfa_type field in the user profile
fieldTfaTypeDescLabelDescription for the tfa_type field
inputLabel'Authentication Code'Label for the code input field
invalidCodeLabel'Invalid code'Error for an incorrect code
maxAttemptsLabel'Max attempts reached'Error when 3 failed code attempts exceeded
rememberLabel'Remember this computer?'Label for the remember-me checkbox
rememberSuccessLabelSuccess message (uses %d for days)
rememberSkipLabelNotice in debug mode when code was skipped
rememberClearLabelLabel for clearing remembered browsers
rememberClearedLabel'Cleared remembered browsers'Message after clearing
sendCodeErrorLabelError when startUser() fails
submitLabel''Override label for submit button (empty = default)
timeLimitLabel'Time limit reached'Error when codeExpire timeout hit
Methods

Flow control

start($name, $pass)

Begins the TFA process. Authenticates the given name/password, then checks whether TFA is enabled for the user. If it is, redirects to the code-entry step. If the user isn't found, can't log in, or credentials fail, returns false. If TFA is not enabled (or the browser is remembered), returns true — the caller should then proceed with normal login.

$tfa->start($input->post('name'), $input->post('pass'));
// If TFA is active, a redirect happens here and the line below is never reached

active()

Returns true when a TFA process is in progress — i.e., the current URL contains the tfa GET parameter matching the session key. When this returns true, render the code form via render().

if($tfa->active()) {
    echo $tfa->render();
}

render()

Builds and renders the authentication code entry form as HTML. Delegates to the specific TFA module instance if called on the base Tfa class.

echo $tfa->render();

success()

Returns true when TFA has completed and the user is logged in. Calls process() internally, which may redirect. Check this before checking active().

if($tfa->success()) {
    $session->redirect('/dashboard/');
}

process()

Processes a submitted authentication code. Validates the code via isValidUserCode(), enforces the max-attempts limit (3) and code expiry (codeExpire), and performs a forced login when the code is valid. Redirects back to the code entry step on failure. Returns the User object on success or false otherwise.

$user = $tfa->process();

Methods to implement in subclasses

Concrete TFA modules must implement these methods:

isValidUserCode($user, $code, $settings)

Validates an authentication code submitted by the user. Modules must implement this method — the base class throws WireException if called directly.

Returns true if valid, false if invalid, or 0 (int) if the code was valid but has expired.

// Example from a hypothetical TfaEmail module
public function isValidUserCode(User $user, $code, array $settings) {
    $storedCode = $this->sessionGet('code');
    if(empty($storedCode)) return false;
    if($code !== $storedCode) return false;
    return true;
}

startUser($user, $settings)

Generates and/or sends the authentication code to the user, then calls parent::startUser($user) to save session state. Returns true on success.

For modules that generate their own codes (like TfaEmail): create the code, save it to session, send it via the appropriate channel, call parent, return true.

For modules that validate but don't send codes (like TfaTotp): the default implementation suffices — it saves session state and returns true.

// TfaEmail-style implementation
public function startUser(User $user, array $settings) {
    $code = sprintf('%06d', random_int(0, 999999));
    $this->sessionSet('code', $code);
    // Send $code to user via email...
    return parent::startUser($user);
}

enabledForUser($user, $settings)

Returns true if TFA is enabled for the given user. The default checks the enabled key in $settings. Subclasses may override for custom logic.

getUserSettingsInputfields($user, $fieldset, $settings)

Provides Inputfields for a user to configure and confirm TFA from their user profile. Called when the user has selected a TFA type but not yet configured it. Subclasses add their fields to the given $fieldset (an InputfieldWrapper).

public function ___getUserSettingsInputfields(User $user, InputfieldWrapper $fieldset, $settings) {
    parent::___getUserSettingsInputfields($user, $fieldset, $settings);
    $f = $this->wire()->modules->get('InputfieldText');
    $f->attr('name', 'secret');
    $f->label = 'Secret Key';
    $fieldset->add($f);
}

processUserSettingsInputfields($user, $fieldset, $settings, $settingsPrev)

Called after the user config form is processed but before settings are saved. Subclasses can modify $settings and return the updated array.

public function ___processUserSettingsInputfields(User $user, InputfieldWrapper $fieldset, $settings, $settingsPrev) {
    $settings = parent::___processUserSettingsInputfields($user, $fieldset, $settings, $settingsPrev);
    $settings['enabled'] = true; // enable after initial configuration
    return $settings;
}

processUserEnabledInputfields($user, $fieldset, $settings, $settingsPrev)

Called after the TFA-enabled user's form is processed. The base implementation handles the "clear remembered browsers" checkbox.

Module identification

getTfaTypeName()

Returns a short name for the TFA type (e.g., 'Email' for TfaEmail). Subclasses should not call parent::getTfaTypeName().

getTfaTypeTitle()

Returns the longer, translatable title — derived from the module's registered title. Subclasses should not call parent::getTfaTypeTitle().

getTfaTypeSummary()

Returns a translatable summary — derived from the module's registered summary.

User settings

getUserSettings($user)

Retrieves per-user TFA settings as an associative array from the database. Throws WireException if called on the base Tfa class instead of a concrete module.

$settings = $tfaModule->getUserSettings($user);
if($settings['enabled']) { /* user has TFA enabled */ }

saveUserSettings($user, $settings)

Saves per-user TFA settings to the database. Returns true on success. Throws WireException if called on the base Tfa class.

$tfaModule->saveUserSettings($user, ['enabled' => true, 'secret' => 'ABC123']);

getDefaultUserSettings($user)

Returns the default settings array (protected, used internally). Subclasses override to add their own default settings.

getUser()

Resolves the current user context — either the logged-in user, or the user being edited in the profile (when in a Process module like ProcessUser or ProcessProfile).

static getUserTfaType($user, $getInstance = false)

Static utility to check whether a user has TFA enabled. Returns the TFA module name (string), a Tfa instance (if $getInstance is true), or false if TFA is not enabled. Use this instead of accessing $user->tfa_type directly — it avoids unnecessarily loading the TFA module and accounts for unconfigured selections.

$tfaType = Tfa::getUserTfaType($user);
if($tfaType) {
    echo "TFA enabled via: $tfaType";
}

Auto-enable

autoEnableSupported($user = null)

Returns true if this TFA module supports being enabled for a user without their input (e.g., TfaEmail if the email is already known). TfaTotp returns false because it requires manual setup.

autoEnableUser($user, $settings = [])

Enables this TFA module for the given user automatically. Throws WireException on all error conditions.

if($module->autoEnableSupported($user)) {
    $module->autoEnableUser($user);
}

Install / uninstall

install()

Creates the tfa_type field (FieldtypeModule, system flag), adds a settings text column to its database table, adds the field to all user templates, and registers it as an editable profile field in ProcessProfile. Subclasses with their own install() must call parent::___install().

uninstall()

Removes the tfa_type field and its assets only when no other TFA modules remain installed. Subclasses with their own uninstall() must call parent::___uninstall().

Hooks

The Tfa system hooks into InputfieldForm to inject TFA configuration fields into the user profile editor automatically.

HookWhenPurpose
InputfieldForm::render (before)Form with tfa_type field is renderedInjects TFA config/enabled fieldset, updates option labels
InputfieldForm::processInput (before)Form with tfa_type field is processedInserts TFA settings fieldset for processing
InputfieldForm::processInput (after)After processing above formSaves TFA settings from submitted fields

Hookable methods

The following methods are hookable (prefixed ___ in source):

MethodDescription
start($name, $pass)TFA login start
buildAuthCodeForm()Build the code entry form
render()Render the code entry form
process()Process submitted code
getUserSettingsInputfields(...)Add config fields for user profile
getUserEnabledInputfields(...)Add fields for already-enabled user
processUserSettingsInputfields(...)Process config submission
processUserEnabledInputfields(...)Process enabled-user submission
install() / uninstall()Setup/teardown
$wire->addHookAfter('Tfa::process', function(HookEvent $event) {
    $tfa = $event->object;  /** @var Tfa $tfa */
    $user = $event->return; /** @var User|bool */
    if($user instanceof User) {
        // Log successful TFA login
        $log->save('tfa-log', "TFA login: $user->name");
    }
});
"Remember this browser"

When rememberDays is greater than 0, the code entry form includes a "Remember this computer?" checkbox. If checked, the browser is fingerprinted and stored so that subsequent logins within the configured days skip the code entry step.

Fingerprints are derived from configurable browser characteristics: agentVL (versionless user agent), accept (HTTP Accept header), scheme (http/https), host, ip, and fwip (forwarded IP). The combination is salted and hashed, providing a secondary security layer on top of a random cookie value.

Up to 10 browsers are remembered per user. Cookies are HttpOnly and Secure (when site uses HTTPS). Users can clear remembered browsers from their profile.

$tfa->rememberDays = 30;
$tfa->rememberFingerprints = ['agentVL', 'accept', 'scheme', 'host', 'ip'];
Writing a TFA module

To create a custom TFA module:

  1. Extend Tfa and implement Module, ConfigurableModule.
  2. Implement isValidUserCode() — validate the submitted code.
  3. Override startUser() if your module generates/sends codes.
  4. Override getUserSettingsInputfields() to collect initial configuration.
  5. Override getDefaultUserSettings() to add module-specific defaults.
  6. Call parent::___install() and parent::___uninstall() if overriding.
class TfaMyModule extends Tfa implements Module, ConfigurableModule {

    public static function getModuleInfo() {
        return [
            'title' => 'My TFA Module',
            'summary' => 'Custom two-factor authentication.',
            'version' => 1,
            'requires' => 'Tfa',
        ];
    }

    public function isValidUserCode(User $user, $code, array $settings) {
        $stored = $this->sessionGet('code');
        return $code && $stored && $code === $stored;
    }

    public function startUser(User $user, array $settings) {
        $code = sprintf('%06d', random_int(0, 999999));
        $this->sessionSet('code', $code);
        // Send $code to user...
        return parent::startUser($user);
    }
}
Notes
  • Always call exit after rendering the code form in a template file if no further output is expected.
  • The tfa GET parameter name and the session namespace are both 'tfa' by default.
  • Max failed code attempts is 3; after which the user must start over.
  • The codeExpire setting (default 180 seconds = 3 minutes) limits how long a pending code entry remains valid.
  • Code reuse prevention: last_code is stored in user settings and compared against submitted codes to prevent replay attacks.
  • The base Tfa class itself is installed as a module. Descending modules (TfaEmail, TfaTotp, etc.) are selected per-user via the tfa_type field.
  • Source file: wire/core/Module/Tfa/Tfa.php
  • Extends: WireData — all WireData property access (get, set, setArray) is available.
  • The companion class RememberTfa is defined in the same file and manages the "remember this browser" feature. It is accessed internally via $tfa->remember($user, $settings).

Submitted by: GLM-5.2

API reference: methods, properties, hooks

This class is for “Tfa” modules to extend. See the TfaEmail and TfaTotp modules as examples.

USAGE

$tfa = new Tfa();

if($tfa->success()) {
$session->redirect('after/login/url/');

} else if($tfa->active()) {
echo $tfa->render();

} else if($input->post('submit_login')) {
$name = $input->post('name');
$pass = $input->post('pass');
$tfa->start($name, $pass);

the start() method performs a redirect if TFA is active for the user
place your regular code to login user here, which will be used if TFA is not active for the user

} else {
render login form
}

SETTINGS

TEXT LABELS

HOOKABLE METHODS


There are 2 Tfa types in the core, plus many more Tfa modules in our directory.

Click any linked item for full usage details and examples. Hookable methods are indicated with the icon. In addition to those shown below, the Tfa class also inherits all the methods and properties of: WireData and Wire.

Show class?     Show args?       Only hookable?    

Common

NameReturnSummary 
Tfa::active()
bool

Returns true if a TFA process is currently active

 
Tfa::autoEnableSupported()
bool

Does this TFA module support automatic enable?

 
Tfa::autoEnableUser(User $user)
None

Auto-enable this TFA module for given $user

 
Tfa::buildAuthCodeForm()
InputfieldForm

Build the form used for two-factor authentication

Tfa::getModule()
Tfa null

Get the TFA module for given user or current session

 
Tfa::getModuleConfigInputfields(InputfieldWrapper $inputfields)
None

Module configuration

 
Tfa::getTfaTypeName()
string

Get translated Tfa type name (short name)

 
Tfa::getTfaTypeSummary()
string

Get translated Tfa type summary

 
Tfa::getTfaTypeTitle()
string

Get translated Tfa type title (longer name)

 
Tfa::getUser()
User

Get current user for TFA

 
Tfa::getUserEnabledInputfields(User $user, InputfieldWrapper $fieldset, array $settings)
None

Get fields for when user already has TFA enabled

Tfa::getUserSettings(User $user)
array

Get TFA data for given user from user_tfa field

 
Tfa::getUserSettingsInputfields(User $user, InputfieldWrapper $fieldset, array $settings)
None

Get fields needed for a user to configure and confirm TFA from their user profile

Tfa::hookAfterInputfieldFormProcess(HookEvent $event)
None

Hook after InputfieldForm::processInput()

 
Tfa::hookBeforeInputfieldFormProcess(HookEvent $event)
None

Hook before InputfieldForm::processInput()

 
Tfa::hookInputfieldFormRender(HookEvent $event)
None

Hook before InputfieldForm::render()

 
Tfa::init()
None

Module init

 
Tfa::install()
None

Module module and other assets required to execute it

Tfa::isValidUserCode(User $user, $code, array $settings)
bool int

Return true if code is valid or false if not

 
Tfa::process()
User bool

Process two-factor authentication code input

Tfa::processUserEnabledInputfields(User $user, InputfieldWrapper $fieldset, array $settings, array $settingsPrev)
array

Called when the user config fieldset has been processed (for enabled user) but before $settings have been saved

Tfa::processUserSettingsInputfields(User $user, InputfieldWrapper $fieldset, array $settings, array $settingsPrev)
array

Called when the user config fieldset has been processed but before $settings have been saved

Tfa::render()
string

Render the code input form

Tfa::saveUserSettings(User $user, array $settings)
bool

Save TFA data for given user to user_tfa field

 
Tfa::start(string $name, string $pass)
bool

Start 2-factor authentication

Tfa::startUser(User $user, array $settings)
bool

Start two-factor authentication for User

 
Tfa::success()
bool

Returns true when TFA has successfully completed and user is now logged in

 
Tfa::uninstall()
None

Uninstall

Tfa::wired()
None

Called when assigned to ProcessWire instance

 

Properties

NameReturnSummary 
Tfa::autoRoleIDs array Role IDs to enforce $autoType or blank for all roles, applies only if $autoType set
DEFAULT: []
 
Tfa::autoType string Automatic/force TFA type (module name) to use when user doesn’t already have TFA enabled
DEFAULT: ''
 
Tfa::cancelLabel string Label to use for Cancel link
DEFAULT: 'Cancel', translatable
 
Tfa::cancelMarkup string Markup to use for the cancel link that appears under auth code form, must have {url} and {label} placeholders. 
Tfa::codeExpire int Codes expire after this many seconds
DEFAULT: 180
 
Tfa::codeLength int Required length for authentication code
DEFAULT: 6
 
Tfa::codeType int Type of TFA code to use, see codeType constants
DEFAULT: 0, which is Tfa::codeTypeDigits
 
Tfa::configureLabel string Indicates that TFA needs to be configured 
Tfa::enabledDescLabel string Describes enabled TFA and how to change settings 
Tfa::enabledLabel string Indicates TFA enabled 
Tfa::expiredCodeLabel string Expired code error 
Tfa::fieldTfaTypeDescLabel string Description of 2-factor auth type 
Tfa::fieldTfaTypeLabel string Select 2-factor auth type 
Tfa::formAttrs array Form <form> element attributes 
Tfa::inputAttrs array Code <input> element attributes 
Tfa::inputLabel string Label for code <input> element 
Tfa::invalidCodeLabel string Invalid code error 
Tfa::maxAttemptsLabel string Max attempts error  
Tfa::rememberClearLabel string Clear remembered browsers 
Tfa::rememberClearedLabel string Message after remembered browsers cleared 
Tfa::rememberDays int Number of days to "remember this browser", 0 to disable option, or -1 for no limit?
DEFAULT: 0
 
Tfa::rememberFingerprints array Fingerprints to remember: agent,agentVL,accept,scheme,host,ip,fwip
DEFAULT: agentVL,accept,scheme,host
 
Tfa::rememberLabel string Label for "remember this browser" option  
Tfa::rememberSkipLabel string Indicates that code entry was skipped because browser is remembered 
Tfa::rememberSuccessLabel string Indicates that browser has been saved/remembered for n days. 
Tfa::sendCodeErrorLabel string Error creating or sending code 
Tfa::showCancel bool Show a cancel link under authentication code form?
DEFAULT: true
 
Tfa::startUrl string URL we are operating from
DEFAULT: './'
 
Tfa::submitAttrs array Submit button attributes 
Tfa::submitLabel string Label for submit button 
Tfa::timeLimitLabel string Time limit reached error 

Additional methods and properties

In addition to the methods and properties above, Tfa also inherits the methods and properties of these classes:

Core Tfa types

See also: Tfa modules in the modules directory

API reference based on ProcessWire core version 3.0.269