Multi-factor authentication

Flowchart of User Login and Forgot Password MFA escalations

The multi-factor authentication (MFA) flow works by triggering escalations in specific user processes. We have implemented MFA escalation in both the User Login and Forgot Password processes.

When these processes happen and MFA is required, the user is redirected to the MFA page and required to verify with an MFA factor. After verifying successfully via MFA, the user process will continue as usual. In the case of User Login, this will log the user in, and in the case of Forgot Password this will redirect the user to the change password screen.

If MFA verification fails, the user can retry up to the limit specified for the Account lockout threshold setting (Quick-access menu > Security). After this, the user account gets locked and the existing account lockout system can be used to recover the account.

A diagram displaying the User Login and Forgot Password processes for Totara multi-factor authentication.

Implementing an MFA plugin

Each MFA plugin provides a class named factor in the plugin namespace, extending \core_mfa\factor. This class provides information about the factor to the authentication system.

Generally you will want to override the following in the factor class:

  • $name – should be set to the plugin name, the part after mfa_

  • user_can_register() – return true if the user is able to register a new instance of this factor

  • has_register_ui() – does this factor have a registration UI?

  • get_register_data() – data to provide to the registration UI

  • has_verify_ui() – does this factor have UI to show on the login screen?

  • get_verify_data() – extra data to pass to the verify UI

  • verify() – called with data from the front end, when the verify component is submitted

All of these except for $name have default implementations, and can be skipped if they don’t apply.

On the front end, there are two components you'll want to implement. These should be located directly under components, so that the front-end code can find them.

The first one is the Register component. This takes a data prop, containing the result of get_register_data(). This should call a plugin-specific GraphQL mutation in order to create an instance of the factor (see TOTP’s create_instance mutation for an example). It should then emit a saved event with the ID of the new instance.

The second is the Verify component. This takes three props: data (result of get_verify_data()), submitting, and submissionError. Verify should emit a submit event with a data object. The MFA code will then call verify() on the factor with the provided data object. submitting will be set to true while this happens, and submissionError will be set if an error is returned. See mfa_totp for an example of how these get used in practice.

There are also some language strings that will be needed:

  • pluginname – the name of the factor

  • factor_description – a description of the factor, used on factor selection screen (both verify and register)

  • verify_title – title to use on the verification screen

  • register_title – title to use on the register screen

Making an auth plugin MFA-compatible

With the implementation of process escalations for MFA, auth plugins need to make two key changes to support MFA.

Step 1: Create a complete user login callback

The complete_user_login function becomes non-returning when MFA is required. A new parameter ($complete_user_login_callback) has been added. Therefore auth plugins need to provide a complete_user_login_callback (which will contain any business logic used after the complete_user_login function).

An example of what this change looks like is shown below:

$user = authenticate_user_login($username, $key); if($user) { complete_user_login($user); // update user profile picture custom_code_update_profle_picture($user, $a); // update auth_plugin records custom_code_update_other_records($user, $b); redirect("https://totara/custom_url.php"); }


class custom_code_callback { public static function complete_login($a, $b) { global $USER; // Logged-in user $user = $USER; // update user profile picture custom_code_update_profle_picture($user, $a); // update auth_plugin records custom_code_update_other_records($user, $b); redirect("https://totara/custom_url.php"); } } $user = authenticate_user_login($username, $key); if($user) { $callback = \core\login\complete_login_callback::create( [custom_code_callback::class, 'complete_login'], [$a, $b] ); complete_user_login($user,$callback); }

The function/method provided to the complete_user_login_callback MUST be discoverable by the class autoloader.

Step 2: Override supports_mfa method

After creating the complete_user_login callback and testing your auth plugin works as expected, override the supports_mfa method in the auth plugin class to return true. This will help identify that the auth plugin now supports MFA in the system.

public static function supports_mfa(): bool { return false; }

Revoking registered user factor via CLI

By using the revoke_admi_mfa CLI script, an admin user’s registered MFA factors can be revoked. The script prompts for the admin user’s username to find the user to revoke registered factors. You can also pass the username as a parameter to the script as shown below:

Note that Site Administrators can also revoke MFA via the Manage user login page.