Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,38 @@
[![tests](https://github.com/nails/module-multi-factor-auth/actions/workflows/build_and_test.yml/badge.svg )](https://github.com/nails/module-multi-factor-auth/actions)

This is the MFA module for Nails, it provides two factor authentication support for `module-auth` powered by drivers.

## Configuration

Set these as config properties (e.g. in `config/app.php`):

- `MFA_TRUSTED_DEVICE_TTL` — how long, in seconds, a device stays trusted when the user opts to not be asked again. Defaults to 14 days.
- `MFA_TRUST_SURVIVES_LOGOUT` — whether a trusted device stays trusted after the user signs out. Defaults to `false`, i.e. signing out ends that device's trust.

The service's constants can also be overridden by extending it at app level, most usefully `TOKEN_TTL` (how long a challenge lasts), `MAX_VERIFICATION_ATTEMPTS` (incorrect codes allowed per challenge), `MAX_RESENDS_PER_TOKEN` (replacement codes a user can request per challenge), and `MAX_TOKEN_MINTS_PER_HOUR` (new challenges issued per user per hour).

## Views

MFA pages use Nails' blank header and footer by default. Applications can inject their own page shell by providing `application/modules/mfa/views/structure/header.php` and `application/modules/mfa/views/structure/footer.php`; individual MFA views can be overridden in the same module view directory.

## Console utilities

Run commands through the Nails console:

```bash
php vendor/nails/module-console/console.php mfa:config
```

Available MFA commands:

- `mfa:config` — show installed/enabled drivers and group policies.
- `mfa:driver:enable --driver=<package>` — enable an installed driver.
- `mfa:driver:disable --driver=<package>` — disable a driver while retaining user enrollments.
- `mfa:driver:setting --driver=<package> [--key=<key> [--value=<value>]]` — inspect or update driver app settings. Use `--json` for structured values.
- `mfa:group:policy --group=<id-or-slug> [--mode=DISABLED|OPTIONAL|REQUIRED]` — inspect or update a group policy.
- `mfa:user:status --user=<id-email-or-username>` — show a user's effective policy and enrolled methods.
- `mfa:user:method:add --user=<user> --driver=<package> [--default]` — enroll a non-interactive driver such as Email.
- `mfa:user:method:remove --user=<user> --driver=<package>` — remove an enrollment.
- `mfa:user:method:default --user=<user> --driver=<package>` — change the user's default method.

Omit `--driver`, `--user`, or `--group` in an interactive terminal to be prompted. `--force` and `--no-interaction` skip prompts and require those options to be set. Mutating commands request confirmation. Pass `--force` for unattended execution. Drivers which hold a user secret, such as Authenticator, must be enrolled interactively so the secret and QR code are delivered directly to the user.
22 changes: 22 additions & 0 deletions admin/views/User/Group/tabs/mfa.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/**
* @var \Nails\Auth\Resource\User\Group $oGroup
* @var array<string, string> $aModes
* @var string $sPolicyMode
*/

echo form_field_dropdown([
'key' => 'mfa_group_policy',
'label' => 'Policy',
'class' => 'select2',
'default' => $sPolicyMode,
'options' => $aModes,
'info' => implode('', [
'<div class="alert alert-info" style="margin:0;">',
'<strong>Disabled</strong> skips MFA for this group.',
'<br />' . '<strong>Optional</strong> challenges users once they enrol a method.',
'<br />' . '<strong>Required</strong> always challenges users, and forces setup on their next login if nothing is enrolled.',
'</div>',
]),
]);
68 changes: 68 additions & 0 deletions admin/views/User/tabs/mfa.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

/**
* @var \Nails\Auth\Resource\User $oUser
* @var string $sGroupMode
* @var array<string, string> $aModes
* @var \Nails\MFA\Resource\UserMethod[] $aMethods
* @var array<string, string> $aDriverLabels
*/

$fnLabel = static fn(string $sDriver): string => $aDriverLabels[$sDriver] ?? $sDriver;

echo form_field([
'key' => 'mfa_group_policy',
'label' => 'Group Policy',
'default' => $aModes[$sGroupMode] ?? $sGroupMode,
'readonly' => true,
'info' => 'Inherited from the user\'s group; change it on the group itself.',
]);

if (empty($aMethods)) {

echo form_field([
'key' => 'mfa_methods',
'label' => 'Enrolled Methods',
'default' => 'None',
'readonly' => true,
'info' => implode('', [
'<div class="alert alert-warning" style="margin:0;">',
'This user must enrol a method themselves; secrets and QR codes are never shown here.',
'</div>',
]),
]);

} else {

echo form_field_radio([
'key' => 'mfa_default_driver',
'label' => 'Default Method',
'options' => array_values(array_map(
static fn($oMethod) => [
'value' => (string) $oMethod->driver,
'label' => $fnLabel((string) $oMethod->driver),
'selected' => (bool) $oMethod->is_default,
],
$aMethods
)),
'info' => 'The method the user is challenged with by default.',
]);

echo form_field_checkbox([
'key' => 'mfa_reset_driver[]',
'label' => 'Reset Methods',
'options' => array_values(array_map(
static fn($oMethod) => [
'value' => (string) $oMethod->driver,
'label' => $fnLabel((string) $oMethod->driver),
],
$aMethods
)),
'info' => implode('', [
'<div class="alert alert-warning" style="margin:0;">',
'Resetting removes the enrolment; the user must set the method up again. ',
'Secrets and QR codes are never shown here.',
'</div>',
]),
]);
}
Loading
Loading