diff --git a/.doclintrc b/.doclintrc new file mode 100644 index 00000000..a6b65270 --- /dev/null +++ b/.doclintrc @@ -0,0 +1 @@ +docs/en/ diff --git a/README.md b/README.md index 8add4818..6d94b244 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![CI](https://github.com/silverstripe/silverstripe-mfa/actions/workflows/ci.yml/badge.svg)](https://github.com/silverstripe/silverstripe-mfa/actions/workflows/ci.yml) [![Silverstripe supported module](https://img.shields.io/badge/silverstripe-supported-0071C4.svg)](https://www.silverstripe.org/software/addons/silverstripe-commercially-supported-module-list/) -### With thanks to Simon `Firesphere` Erkelens +## With thanks to Simon `Firesphere` Erkelens This module was based on pioneering work by Simon. It differs from the original implementation in its use of a pluggable React UI + JSON API architecture, and its enhanced management UI within the CMS. You can find Simon's original module @@ -11,57 +11,13 @@ React UI + JSON API architecture, and its enhanced management UI within the CMS. ## Installation -```sh +```bash composer require silverstripe/mfa ``` -You should also install one of the additional multi-factor authenticator modules: - -* [silverstripe/totp-authenticator](https://github.com/silverstripe/silverstripe-totp-authenticator) -* [silverstripe/webauthn-authenticator](https://github.com/silverstripe/silverstripe-webauthn-authenticator) - -## Setup - -After installing this module _and_ a supported factor method module (e.g. TOTP), the default member authenticator -will be replaced with the MFA authenticator instead. This will provide no change in the steps taken to log in until -an MFA Method has also been configured for the site. The TOTP and WebAuthn modules will configure themselves -automatically. - -After installing the MFA module and having at least one method configured, MFA will automatically be enabled. By default -it will be optional (users can skip MFA registration). You can make it mandatory via the Settings tab in the admin area. - -The MFA flow will only be applied to members with access to the CMS or administration area. See '[Broadening the scope of MFA](docs/en/broadening-the-scope-of-mfa.md)' for more detail. - -You can disable MFA on an environment by setting a `BYPASS_MFA=1` environment variable, -or via YAML config - see [local development](docs/en/local-development) for details. - -### Configuring custom methods - -If you have built your own MFA method, you can register it with the `MethodRegistry` to enable it: - -```yaml -SilverStripe\MFA\Service\MethodRegistry: - methods: - - MyCustomMethod - - Another\Custom\Method\Here -``` - ## Documentation -This module provides two distinct processes for MFA; verification and registration. This module provides a decoupled -architecture where front-end and back-end are separate. Provided with the module is a React app that interfaces with -default endpoints added by this module. Please refer to the docs for specific information about the included -functionality: - -- [Debugging](docs/en/debugging.md) -- Creating new MFA methods - - [Frontend](docs/en/creating-mfa-method-frontend.md) - - [Backend](docs/en/creating-mfa-method-backend.md) -- [Local development](docs/en/local-development.md) -- [Encryption providers](docs/en/encryption.md) -- [Data store interfaces](docs/en/datastores.md) -- [Security](docs/en/security.md) -- [Integrating with other authenticators](docs/en/other-authenticators.md) +Read the [documentation](docs/en/index.md). ## Module development diff --git a/composer.json b/composer.json index f8c503cf..42aa7d2d 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ "require-dev": { "phpunit/phpunit": "^9.6", "squizlabs/php_codesniffer": "^3", + "silverstripe/documentation-lint": "^1", "silverstripe/standards": "^1", "phpstan/extension-installer": "^1.3" }, @@ -52,6 +53,11 @@ "client/lang" ] }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, "autoload": { "psr-4": { "SilverStripe\\MFA\\": "src/", diff --git a/docs/en/01_authenticators/index.md b/docs/en/01_authenticators/index.md new file mode 100644 index 00000000..661babd5 --- /dev/null +++ b/docs/en/01_authenticators/index.md @@ -0,0 +1,7 @@ +--- +title: Authenticators +--- + +# Authenticators + +[CHILDREN includeFolders] diff --git a/docs/en/debugging.md b/docs/en/01_debugging.md similarity index 72% rename from docs/en/debugging.md rename to docs/en/01_debugging.md index 1ced038a..7604d092 100644 --- a/docs/en/debugging.md +++ b/docs/en/01_debugging.md @@ -1,11 +1,15 @@ +--- +title: Debugging +--- + # Debugging The MFA module ships with a PSR-3 logger configured by default (a [Monolog](https://github.com/Seldaek/monolog/) implementation), however no Monolog handlers are attached by default. To enable developer logging, you can -[attach a handler](https://docs.silverstripe.org/en/4/developer_guides/debugging/error_handling/#configuring-error-logging). +[attach a handler](https://docs.silverstripe.org/en/developer_guides/debugging/error_handling/#configuring-error-logging). An example that will log to a `mfa.log` file in the project root: -```yaml +```yml SilverStripe\Core\Injector\Injector: Psr\Log\LoggerInterface.mfa: calls: @@ -20,10 +24,18 @@ SilverStripe\Core\Injector\Injector: You can inject this logger into any MFA authenticator module, or custom app code, by using dependency injection: ```php +// app/src/MFA/Handlers/MyCustomLoginHandler.php +namespace App\MFA\Handlers; + +use Exception; +use Psr\Log\LoggerInterface; +use SilverStripe\MFA\Model\RegisteredMethod; +use SilverStripe\MFA\Store\StoreInterface; + class MyCustomLoginHandler implements LoginHandlerInterface { private static $dependencies = [ - 'Logger' => '%$' . \Psr\Log\LoggerInterface::class . '.mfa', + 'Logger' => '%$' . LoggerInterface::class . '.mfa', ]; protected $logger; @@ -38,7 +50,7 @@ class MyCustomLoginHandler implements LoginHandlerInterface { try { $method->doSomething(); - } catch (\Exception $ex) { + } catch (Exception $ex) { $this->logger->debug('Something went wrong! ' . $ex->getMessage(), $ex->getTrace()); } } diff --git a/docs/en/creating-mfa-method-frontend.md b/docs/en/02_creating-new-mfa-methods/01_creating-mfa-method-frontend.md similarity index 93% rename from docs/en/creating-mfa-method-frontend.md rename to docs/en/02_creating-new-mfa-methods/01_creating-mfa-method-frontend.md index 4dcf6431..8e1e22f3 100644 --- a/docs/en/creating-mfa-method-frontend.md +++ b/docs/en/02_creating-new-mfa-methods/01_creating-mfa-method-frontend.md @@ -1,4 +1,8 @@ -# Creating a new MFA method: Front-end +--- +title: Creating a new MFA method: front-end +--- + +# Creating a new MFA method: front-end ## Introduction @@ -8,7 +12,7 @@ with React / Redux is recommended. The front-end components of MFA make use of [`react-injector`](https://github.com/silverstripe/react-injector/) (Injector) to allow sharing of React components and Redux reducers between separate JS bundles. You can find more -documentation on the Injector API in the [Silverstripe docs](https://docs.silverstripe.org/en/4/developer_guides/customising_the_admin_interface/reactjs_redux_and_graphql/#the-injector-api). +documentation on the Injector API in the [Silverstripe docs](https://docs.silverstripe.org/en/developer_guides/customising_the_admin_interface/reactjs_redux_and_graphql/#the-injector-api). You'll find it easiest to get up and running by matching the NPM dependencies and Webpack configuration used in the TOTP and WebAuthn modules, with a single entry point that handles registering your components with Injector. We also suggest @@ -35,7 +39,7 @@ Your component for registration will need to accept a couple of key props: A Register component for Basic Math might look like this: -```jsx +```js import React, { Component } from 'react'; class BasicMathRegister extends Component { @@ -88,7 +92,7 @@ Your verification component will look similar to your registration one - it shou - `onCompleteVerification`: A callback that should be invoked when the user has completed the challenge presented, with any data that your `VerifyHandlerInterface::verify()` implementation needs to confirm the user's identity. **NOTE:** - It is _imperative_ that your backend code is involved in the verification process, as providing secrets to the browser + It is *imperative* that your backend code is involved in the verification process, as providing secrets to the browser or otherwise relying solely on it to approve the authentication can result in significant security flaws. - `moreOptionsControl`: A React component to render in your UI, which presents a button for users to pick a different method to authenticate with. We recommend referencing the layout of the TOTP / WebAuthn implementations. @@ -97,7 +101,7 @@ Your verification component will look similar to your registration one - it shou A Verify component for Basic Math might look like this: -```jsx +```js import React, { Component } from 'react'; class BasicMathVerify extends Component { @@ -157,21 +161,21 @@ class BasicMathVerify extends Component { export default BasicMathVerify; ``` -## Register components with Injector +## Register components with `Injector` In order for your components to be found and rendered by the MFA module, you'll need to register them with Injector. Your JS entrypoint (the file Webpack is pointed at) should contain the following: ```js +import Injector from 'lib/Injector'; // available via expose-loader import BasicMathRegister from './components/BasicMathRegister'; import BasicMathVerify from './components/BasicMathVerify'; -import Injector from 'lib/Injector'; // available via expose-loader // Injector expects dependencies to be registered during this event, and initialises itself afterwards window.document.addEventListener('DOMContentLoaded', () => { Injector.component.registerMany({ - BasicMathRegister, - BasicMathVerify, + BasicMathRegister, + BasicMathVerify, }); }); ``` @@ -182,12 +186,12 @@ You can then specify the component names via `VerifyHandlerInterface::getCompone ## Method availability If your method needs to rely on frontend environment state to determine whether it's available (such as the browser -being used), you can [define a Redux reducer](https://docs.silverstripe.org/en/4/developer_guides/customising_the_admin_interface/reactjs_redux_and_graphql/#using-injector-to-customise-redux-state-data) +being used), you can [define a Redux reducer](https://docs.silverstripe.org/en/developer_guides/customising_the_admin_interface/reactjs_redux_and_graphql/#using-injector-to-customise-redux-state-data) that will initialise some "availability" information in the Redux store, which the MFA module will look for when it determines whether a method is available to be used or not. For example: -```jsx -// File: webauthn-module/client/src/state/availability/reducer.js +```js +// webauthn-module/client/src/state/availability/reducer.js export default (state = {}) => { const isAvailable = typeof window.AuthenticatorResponse !== 'undefined'; const availability = isAvailable ? {} : { @@ -202,8 +206,8 @@ export default (state = {}) => { You must register this reducer with Injector with a name that matches the pattern `[urlSegment]Availability`. This is required for the MFA module to find this part of the redux state. For example: -```jsx -// File: webauthn-module/client/src/boot/index.js +```js +// webauthn-module/client/src/boot/index.js import Injector from 'lib/Injector'; import reducer from 'state/availability/reducer'; diff --git a/docs/en/creating-mfa-method-backend.md b/docs/en/02_creating-new-mfa-methods/02_creating-mfa-method-backend.md similarity index 74% rename from docs/en/creating-mfa-method-backend.md rename to docs/en/02_creating-new-mfa-methods/02_creating-mfa-method-backend.md index 1b7baf03..2beff9cc 100644 --- a/docs/en/creating-mfa-method-backend.md +++ b/docs/en/02_creating-new-mfa-methods/02_creating-mfa-method-backend.md @@ -1,11 +1,18 @@ -# Creating a new MFA method: Backend +--- +title: Creating a new MFA method: backend +--- + +# Creating a new MFA method: backend ## Method availability If your method isn't available in some situations, and you can determine this via server-side state, you can provide -this information to the frontend via `MethodInterface::isAvailable()`, for example: +this information to the frontend via [`MethodInterface::isAvailable()`](api:SilverStripe\MFA\Method\MethodInterface::isAvailable()), for example: ```php +// app/src/MFA/Methods/MyMethod.php +namespace App\MFA\Methods; + class MyMethod implements MethodInterface { public function isAvailable(): bool diff --git a/docs/en/02_creating-new-mfa-methods/index.md b/docs/en/02_creating-new-mfa-methods/index.md new file mode 100644 index 00000000..4314854f --- /dev/null +++ b/docs/en/02_creating-new-mfa-methods/index.md @@ -0,0 +1,7 @@ +--- +title: Creating new MFA methods +--- + +# Creating new MFA methods + +[CHILDREN includeFolders] diff --git a/docs/en/local-development.md b/docs/en/03_local-development.md similarity index 81% rename from docs/en/local-development.md rename to docs/en/03_local-development.md index 1f5c798a..ba4c58db 100644 --- a/docs/en/local-development.md +++ b/docs/en/03_local-development.md @@ -1,17 +1,21 @@ +--- +title: Local development +--- + # Local development When running development versions of a project using this module, you may want to disable multi-factor authentication while you test other features. This will not redirect you to multi-factor authentication registration or verification screens when logging in. -The easiest way is to set an [environment variable](https://docs.silverstripe.org/en/4/developer_guides/configuration/environment_variables/): +The easiest way is to set an [environment variable](https://docs.silverstripe.org/en/developer_guides/configuration/environment_variables/): -``` +```text BYPASS_MFA=1 ``` Alternatively, YAML configuration affords you more control over the conditions: -```yaml +```yml --- Name: mydevconfig Only: diff --git a/docs/en/encryption.md b/docs/en/04_encryption.md similarity index 58% rename from docs/en/encryption.md rename to docs/en/04_encryption.md index a6137d31..5681cc7a 100644 --- a/docs/en/encryption.md +++ b/docs/en/04_encryption.md @@ -1,17 +1,22 @@ +--- +title: Configuring encryption providers +--- + # Configuring encryption providers By default this module uses [defuse/php-encryption](https://github.com/defuse/php-encryption) as its encryption adapter for secret information that must be persisted to a data store, such as a TOTP secret. You can add your own implementation if you would like to use something different, by implementing -`EncryptionAdapterInterface` and configuring your service class with Injector. The interface is deliberately simple, +[`EncryptionAdapterInterface`](api:SilverStripe\MFA\Service\EncryptionAdapterInterface) and configuring your service class with Injector. The interface is deliberately simple, and takes `encrypt()` and `decrypt()` methods with a payload and an encryption key argument. -```yaml +```yml SilverStripe\Core\Injector\Injector: SilverStripe\MFA\Service\EncryptionAdapterInterface: class: App\MFA\ReallyStrongEncryptionAdapter ``` -**Please note:** this is different from the `PasswordEncryptor` API provided by silverstripe/framework -because we need two-way encryption (as opposed to one-way hashing) for MFA. +> [!NOTE] +> This is different from the `PasswordEncryptor` API provided by silverstripe/framework +> because we need two-way encryption (as opposed to one-way hashing) for MFA. diff --git a/docs/en/05_datastores.md b/docs/en/05_datastores.md new file mode 100644 index 00000000..cf2b4ec8 --- /dev/null +++ b/docs/en/05_datastores.md @@ -0,0 +1,45 @@ +--- +title: Data store interfaces +--- + +# Data store interfaces + +Since the MFA architecture is largely designed to be decoupled, we use a [`StoreInterface`](api:SilverStripe\MFA\Store\StoreInterface) implementation to retain +data between requests. The default implementation for this interface is [`SessionStore`](api:SilverStripe\MFA\Store\SessionStore) which stores data using the +Silverstripe CMS [`Session`](api:SilverStripe\Control\Session) API provided by silverstripe/framework. + +If you need to use a different storage mechanism (e.g. Redis, DynamoDB etc) you can implement and configure your +own `StoreInterface`, and register it with Injector: + +```yml +SilverStripe\Core\Injector\Injector: + SilverStripe\MFA\Store\StoreInterface: + class: App\MFA\RedisStoreInterface +``` + +> [!NOTE] +> The store should always be treated as a server side implementation. It's not a good idea to implement +> a client store e.g. cookies. + +## Adjusting what goes into the store + +By default, the entire [`HTTPRequest`](api:SilverStripe\Control\HTTPRequest) object is saved to the store during the multi-factor authentication process. We +exclude the `Password` field from the request by default, but if you need to exclude other fields, you can add an +extension, for example: + +```php +// app/src/MFA/Extensions/MyLoginHandlerExtension.php +namespace App\MFA\Extensions; + +use SilverStripe\Control\HTTPRequest; +use SilverStripe\MFA\Store\StoreInterface; + +// Apply extension to SilverStripe\MFA\Authenticator\LoginHandler +class MyLoginHandlerExtension extends Extension +{ + public function onBeforeSaveRequestToStore(HTTPRequest $request, StoreInterface $store): void + { + $request->offsetUnset('MySecretField'); + } +} +``` diff --git a/docs/en/06_security.md b/docs/en/06_security.md new file mode 100644 index 00000000..9dcdedd5 --- /dev/null +++ b/docs/en/06_security.md @@ -0,0 +1,24 @@ +--- +title: Security +--- + +# Security + +## Login attempts + +The MFA module makes use of the framework's [`LoginAttempt`](api:SilverStripe\Security\LoginAttempt) API to ensure that a user can only attempt to register +or verify a MFA method a certain number of times. Since it re-uses the core API, it also shares the maximum number +of attempts with login attempts themselves. + +For example: if the maximum number of login attempts ([`Member.lock_out_after_incorrect_logins`](api:SilverStripe\Security\Member->lock_out_after_incorrect_logins)) is 5, and a user +incorrectly enters their password twice, correctly enters it once, then incorrectly enters a TOTP code three times, +they will be registered as locked out for a specified period of time ([`Member.lock_out_delay_mins`](api:SilverStripe\Security\Member->lock_out_delay_mins)). In this case, +the user will be shown a message when trying to verify their TOTP code similar to "Your account is temporarily locked. +Please try again later." + +For more information on this, see [Secure Coding](https://docs.silverstripe.org/en/developer_guides/security/secure_coding/#other-options). + +## Related links + +- [MFA encryption providers](encryption.md) +- [silverstripe/security-extensions documentation](https://github.com/silverstripe/silverstripe-security-extensions) diff --git a/docs/en/other-authenticators.md b/docs/en/07_other-authenticators.md similarity index 62% rename from docs/en/other-authenticators.md rename to docs/en/07_other-authenticators.md index 130faa55..3b94082d 100644 --- a/docs/en/other-authenticators.md +++ b/docs/en/07_other-authenticators.md @@ -1,12 +1,14 @@ -# Integrating with other authenticators +--- +title: Integrating with other authenticators +--- -**This version relates to Silverstripe CMS 4.x configuration.** +# Integrating with other authenticators -If your project uses a non-standard authentication module, such as silverstripe/ldap, you will +If your project uses a non-standard authentication module, such as [`silverstripe/ldap`](https://github.com/silverstripe/silverstripe-ldap), you will need to implement some customisations to connect the modules together. The following notes should serve as a guide for parts of the code to be aware of, and things to do in order to achieve this. -For the purposes of comparisons in this document, we will use [silverstripe/ldap](https://github.com/silverstripe/silverstripe-ldap)'s authenticator. +For the purposes of comparisons in this document, we will use `silverstripe/ldap`'s authenticator. ## Concepts @@ -18,28 +20,27 @@ down for a hypothetical example. ### Authenticator -The Authenticator entrypoint class in the MFA module is `SilverStripe\MFA\Authenticator\MemberAuthenticator`. This -class extends the default `SilverStripe\Security\MemberAuthenticator` class in order to override the default login -form with `SilverStripe\MFA\Authenticator\LoginForm`, and the change password handler with -`SilverStripe\MFA\Authenticator\ChangePasswordHandler`. +The Authenticator entrypoint class in the MFA module is [`SilverStripe\MFA\Authenticator\MemberAuthenticator`](api:SilverStripe\MFA\Authenticator\MemberAuthenticator). This +class extends the default [`SilverStripe\Security\MemberAuthenticator`](api:SilverStripe\Security\MemberAuthenticator) class in order to override the default login +form with [`LoginForm`](api:SilverStripe\MFA\Authenticator\LoginForm), and the change password handler with [`ChangePasswordHandler`](api:SilverStripe\MFA\Authenticator\ChangePasswordHandler). -silverstripe/ldap does the same thing - it also configures itself to override the `default` authenticator. Since the -MFA replacement for the default authenticator has MFA logic added to it, and LDAP has the same with LDAP logic added, +`silverstripe/ldap` does the same thing - it also configures itself to override the `default` authenticator. Since the +MFA replacement for the default authenticator has MFA logic added to it, and LDAP has the same with LDAP logic added, you will need to reimplement it so that both MFA and LDAP apply their logic together. In order to combine these two authenticators, you may choose to add your own `LDAPMFAAuthenticator` class and configure that instead of either MFA or LDAP's authenticators. See further down for a hypothetical example. -### LoginHandler +### `LoginHandler` -The MFA `LoginHandler` class is the point where MFA flows are injected into core. In silverstripe/ldap, this +The MFA [`LoginHandler`](api:SilverStripe\MFA\Authenticator\LoginHandler) class is the point where MFA flows are injected into core. In silverstripe/ldap, this class performs the same function: to inject LDAP authentication logic into core. As above, in order to have both work together you may choose to add your own `LDAPMFALoginHandler` class and configure that in your custom Authenticator. This class would need to combine the logic from both `SilverStripe\LDAP\Forms\LDAPLoginHandler` and `SilverStripe\MFA\Authenticator\LoginHandler` in order to function correctly for both cases. -### ChangePasswordHandler +### `ChangePasswordHandler` Both the LDAP and MFA modules provide their own implementations of the `ChangePasswordHandler`, and in both cases these are referenced from the `MemberAuthenticator` subclass of each module. Similarly to the `LoginForm` example @@ -49,7 +50,7 @@ Similarly to `LoginForm` above, in order to reduce duplication of code we recomm `\SilverStripe\MFA\Authenticator\LoginHandler` and duplicating the contents of `SilverStripe\LDAP\Authenticators\LDAPChangePasswordHandler` which is substantially smaller. -### LoginForm and ChangePasswordForm +### `LoginForm` and `ChangePasswordForm` The LDAP module overrides a couple of the default Form implementations: `LDAPLoginForm` and `LDAPChangePasswordForm` form. The way that these classes are written likely indicates that there will not be any conflicts here with the @@ -59,7 +60,7 @@ MFA module, which does not extend these classes from core. The following example classes should be enabled in your project with Injector configuration. For example: -```yaml +```yml SilverStripe\Core\Injector\Injector: SilverStripe\MFA\Authenticator\MemberAuthenticator: class: LDAPMFAAuthenticator @@ -68,21 +69,28 @@ SilverStripe\Core\Injector\Injector: Note that this example overrides the default injection class for MemberAuthenticator, which will allow MFA's configuration to register the method and set it as the default authenticator to continue. If you have [configured the LDAP authenticator](https://github.com/silverstripe/silverstripe-ldap/blob/master/docs/en/developer.md#show-the-ldap-login-button-on-login-form) -you will want to remove this now - MFA configures itself automatically. +you will want to remove this now - MFA configures itself automatically. -### A custom MemberAuthenticator +### A custom `MemberAuthenticator` ```php -class LDAPMFAMemberAuthenticator extends \SilverStripe\LDAP\Authenticators\LDAPAuthenticator +// app/src/MFA/Authenticators/LDAPMFAMemberAuthenticator.php +namespace App\MFA\Authenticators; + +use SilverStripe\LDAP\Authenticators\LDAPAuthenticator; +use SilverStripe\MFA\Authenticator\ChangePasswordHandler; +use SilverStripe\MFA\Authenticator\LoginHandler; + +class LDAPMFAMemberAuthenticator extends LDAPAuthenticator { public function getLoginHandler($link) { - return \SilverStripe\MFA\Authenticator\LoginHandler::create($link, $this); + return LoginHandler::create($link, $this); } public function getChangePasswordHandler($link) { - return \SilverStripe\MFA\Authenticator\ChangePasswordHandler::create($link, $this); + return ChangePasswordHandler::create($link, $this); } } ``` @@ -91,30 +99,42 @@ In this example, we have copied the small amount of logic from the MFA module in class from the core `MemberAuthenticator` to `LDAPAuthenticator`, and will change the injection class name with the configuration above so it is used instead of MFA or LDAP. -### A custom LoginHandler +### A custom `LoginHandler` In this example, the logic from silverstripe/ldap is much smaller, so is preferable to duplicate while extending the MFA `LoginHandler` which contains much more logic. ```php -class LDAPMFALoginHandler extends \SilverStripe\MFA\Authenticator\LoginHandler +// app/src/MFA/Handlers/LDAPMFALoginHandler.php +namespace App\MFAHandlers; + +use SilverStripe\LDAP\Forms\LDAPLoginForm; +use SilverStripe\MFA\Authenticator\LoginHandler; + +class LDAPMFALoginHandler extends LoginHandler { private static $allowed_actions = ['LoginForm']; public function loginForm() { - return \SilverStripe\LDAP\Forms\LDAPLoginForm::create($this, get_class($this->authenticator), 'LoginForm'); + return LDAPLoginForm::create($this, get_class($this->authenticator), 'LoginForm'); } } ``` -### A custom ChangePasswordHandler +### A custom `ChangePasswordHandler` As with the `LoginHandler` example above, the logic from silverstripe/ldap's `ChangePasswordHandler` is much smaller, so is used for this example. ```php -class LDAPMFAChangePasswordHandler extends \SilverStripe\MFA\Authenticator\ChangePasswordHandler +// app/src/MFA/Handlers/LDAPMFAChangePasswordHandler.php +namespace App\MFA\Handlers; + +use SilverStripe\LDAP\Forms\LDAPChangePasswordForm; +use SilverStripe\MFA\Authenticator\ChangePasswordHandler; + +class LDAPMFAChangePasswordHandler extends ChangePasswordHandler { private static $allowed_actions = [ 'changepassword', @@ -123,7 +143,7 @@ class LDAPMFAChangePasswordHandler extends \SilverStripe\MFA\Authenticator\Chang public function changePasswordForm() { - return \SilverStripe\LDAP\Forms\LDAPChangePasswordForm::create($this, 'ChangePasswordForm'); + return LDAPChangePasswordForm::create($this, 'ChangePasswordForm'); } } ``` diff --git a/docs/en/backup-codes.md b/docs/en/08_backup-codes.md similarity index 74% rename from docs/en/backup-codes.md rename to docs/en/08_backup-codes.md index ef0dffa9..e39c5de1 100644 --- a/docs/en/backup-codes.md +++ b/docs/en/08_backup-codes.md @@ -1,3 +1,7 @@ +--- +title: Backup codes +--- + # Backup codes Backup codes are random strings that are provided as recovery options in case a member loses their ability to @@ -5,14 +9,18 @@ verify with their registered multi-factor authentication methods, e.g. they lost ## Generating backup codes -Backup codes are generated by the `BackupCodeGeneratorInterface` service, which has a default implementation of -`SilverStripe\MFA\Service\BackupCodeGenerator`. This service will hash the generated backup codes using the -default Silverstripe CMS `PasswordEncryptor` service and algorithm, and returns an array of -`SilverStripe\MFA\State\BackupCode` instances. +Backup codes are generated by the [`BackupCodeGeneratorInterface`](api:SilverStripe\MFA\Service\BackupCodeGeneratorInterface) service, which has a default implementation of +[`BackupCodeGenerator`](api:SilverStripe\MFA\Service\BackupCodeGenerator). This service will hash the generated backup codes using the +default Silverstripe CMS [`PasswordEncryptor`](api:SilverStripe\Security\PasswordEncryptor) service and algorithm, and returns an array of +[`BackupCode`](api:SilverStripe\MFA\State\BackupCode) instances. You can use this service class to generate codes: ```php +use SilverStripe\MFA\Service\BackupCodeGeneratorInterface; + +// ... + $generator = Injector::inst()->get(BackupCodeGeneratorInterface::class); $codes = $generator->generate(); ``` @@ -23,6 +31,10 @@ To verify a backup code, you can reconstruct a `BackupCode` instance from stored instance and use `BackupCode::isValid()` to verify the input. For example: ```php +use SilverStripe\MFA\State\BackupCode; + +// ... + $storedCodeData = json_decode($registeredMethod->Data, true); foreach ($storedCodeData as $codeCandidate) { // Expected structure: ['hash' => 'abc123', 'algorithm' => 'blowfish', 'salt' => 'bae'] @@ -37,7 +49,7 @@ foreach ($storedCodeData as $codeCandidate) { The default implementation of this logic is in `VerifyHandler::verify()`. -## BackupCodeGenerator +## `BackupCodeGenerator` This configuration documentation applies to the default `BackupCodeGenerator` implementation of `BackupCodeGeneratorInterface`. @@ -47,7 +59,7 @@ This configuration documentation applies to the default `BackupCodeGenerator` im You can adjust either the length of the backup code strings, or the number of them that are generated, by setting YAML configuration: -```yaml +```yml SilverStripe\MFA\Service\BackupCodeGenerator: # Should be 12 characters long backup_code_length: 12 @@ -61,6 +73,9 @@ By default, backup codes will be generated using lowercase letters. If you would the entropy of your backup codes by adding extra character types, you can do so by adding an extension: ```php +// app/src/MFA/Extension/BackupCodeGeneratorExtension.php +namespace App\MFA\Extension; + class BackupCodeGeneratorExtension extends Extension { /** diff --git a/docs/en/broadening-the-scope-of-mfa.md b/docs/en/09_broadening-the-scope-of-mfa.md similarity index 77% rename from docs/en/broadening-the-scope-of-mfa.md rename to docs/en/09_broadening-the-scope-of-mfa.md index db5e8a9e..04664662 100644 --- a/docs/en/broadening-the-scope-of-mfa.md +++ b/docs/en/09_broadening-the-scope-of-mfa.md @@ -1,3 +1,7 @@ +--- +title: Broadening the scope of the MFA flow +--- + # Broadening the scope of the MFA flow ## Default behaviour @@ -10,15 +14,15 @@ By default, the MFA flow will only be presented during the login process to memb You can broaden the scope of the MFA flow so it applies to all members, regardless of whether they have CMS or administration privileges or not by setting the following configuration: -```yaml +```yml SilverStripe\MFA\Service\EnforcementManager: requires_admin_access: false ``` However, note that users without access to the CMS will be unable to access their personal MFA settings and perform actions such as: -* adding additional MFA methods; -* removing, resetting, and changing default MFA methods; and -* resetting recovery codes. +- adding additional MFA methods; +- removing, resetting, and changing default MFA methods; and +- resetting recovery codes. -A custom implementation would be required to provide this functionality. Otherwise it would be limited to Silverstripe CMS Administrators to [reset MFA settings](https://userhelp.silverstripe.org/en/4/optional_features/multi-factor_authentication/administrator_manual/resetting_accounts/) for a member on their behalf. +A custom implementation would be required to provide this functionality. Otherwise it would be limited to Silverstripe CMS Administrators to [reset MFA settings](https://userhelp.silverstripe.org/en/optional_features/multi-factor_authentication/administrator_manual/resetting_accounts/) for a member on their behalf. diff --git a/docs/en/datastores.md b/docs/en/datastores.md deleted file mode 100644 index a3ed7761..00000000 --- a/docs/en/datastores.md +++ /dev/null @@ -1,33 +0,0 @@ -# Data store interfaces - -Since the MFA architecture is largely designed to be decoupled, we use a `StoreInterface` implementation to retain -data between requests. The default implementation for this interface is `SessionStore` which stores data using the -Silverstripe CMS `Session` API provided by silverstripe/framework. - -If you need to use a different storage mechanism (e.g. Redis, DynamoDB etc) you can implement and configure your -own `StoreInterface`, and register it with Injector: - -```yaml -SilverStripe\Core\Injector\Injector: - SilverStripe\MFA\Store\StoreInterface: - class: App\MFA\RedisStoreInterface -``` - -Please note that the store should always be treated as a server side implementation. It's not a good idea to implement -a client store e.g. cookies. - -## Adjusting what goes into the store - -By default, the entire HTTPRequest object is saved to the store during the multi-factor authentication process. We -exclude the `Password` field from the request by default, but if you need to exclude other fields, you can add an -extension, for example: - -```php -// Apply extension to \SilverStripe\MFA\Authenticator\LoginHandler -class MyLoginHandlerExtension extends Extension -{ - public function onBeforeSaveRequestToStore(HTTPRequest $request, StoreInterface $store): void - { - $request->offsetUnset('MySecretField'); - } -``` diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 00000000..e98775db --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,48 @@ +--- +title: Multi-factor authentication (MFA) +--- + +# Multi-factor authentication (MFA) + +This module provides bases classes for implementing multi-factor authentication (MFA) in Silverstripe CMS. You should also install one of the additional multi-factor authenticator modules: + +- [silverstripe/totp-authenticator](https://github.com/silverstripe/silverstripe-totp-authenticator) +- [silverstripe/webauthn-authenticator](https://github.com/silverstripe/silverstripe-webauthn-authenticator) + +This module provides two distinct processes for MFA; verification and registration. This module provides a decoupled +architecture where front-end and back-end are separate. Provided with the module is a React app that interfaces with +default endpoints added by this module. + +## Installation + +```bash +composer require silverstripe/mfa +``` + +## Setup + +After installing this module *and* a supported factor method module (e.g. TOTP), the default member authenticator +will be replaced with the MFA authenticator instead. This will provide no change in the steps taken to log in until +an MFA Method has also been configured for the site. The TOTP and WebAuthn modules will configure themselves +automatically. + +After installing the MFA module and having at least one method configured, MFA will automatically be enabled. By default +it will be optional (users can skip MFA registration). You can make it mandatory via the Settings tab in the admin area. + +The MFA flow will only be applied to members with access to the CMS or administration area. See '[Broadening the scope of MFA](docs/en/broadening-the-scope-of-mfa.md)' for more detail. + +You can disable MFA on an environment by setting a `BYPASS_MFA=1` environment variable, +or via YAML config - see [local development](docs/en/local-development) for details. + +### Configuring custom methods + +If you have built your own MFA method, you can register it with the [`MethodRegistry`](api:SilverStripe\MFA\Service\MethodRegistry) to enable it: + +```yml +SilverStripe\MFA\Service\MethodRegistry: + methods: + - MyCustomMethod + - Another\Custom\Method\Here +``` + +[CHILDREN includeFolders] diff --git a/docs/en/security.md b/docs/en/security.md deleted file mode 100644 index a646a2ef..00000000 --- a/docs/en/security.md +++ /dev/null @@ -1,20 +0,0 @@ -# Security - -## Login attempts - -The MFA module makes use of the framework's `LoginAttempt` API to ensure that a user can only attempt to register -or verify a MFA method a certain number of times. Since it re-uses the core API, it also shares the maximum number -of attempts with login attempts themselves. - -For example: if the maximum number of login attempts (`Member.lock_out_after_incorrect_logins`) is 5, and a user -incorrectly enters their password twice, correctly enters it once, then incorrectly enters a TOTP code three times, -they will be registered as locked out for a specified period of time (`Member.lock_out_delay_mins`). In this case, -the user will be shown a message when trying to verify their TOTP code similar to "Your account is temporarily locked. -Please try again later." - -For more information on this, see [Secure Coding](https://docs.silverstripe.org/en/4/developer_guides/security/secure_coding/#other-options). - -## Related links - -* [MFA encryption providers](encryption.md) -* [silverstripe/security-extensions documentation](https://github.com/silverstripe/silverstripe-security-extensions) diff --git a/docs/en/userguide/01_User_manual/01_Setting_up_MFA.md b/docs/en/userguide/01_User_manual/01_Setting_up_MFA.md index 7b2978dd..d47987e3 100644 --- a/docs/en/userguide/01_User_manual/01_Setting_up_MFA.md +++ b/docs/en/userguide/01_User_manual/01_Setting_up_MFA.md @@ -3,7 +3,7 @@ title: Setting Up MFA summary: Running initial configuration of an MFA method for your account --- -# Setting Up MFA +# Setting up MFA ## 1. Prompted to setup MFA @@ -15,7 +15,7 @@ prompted to set it up for your account. If MFA is optional, you can choose to skip setup, and you won’t be prompted again. You can manually add MFA from your profile in Silverstripe CMS at a later time. -If an administrator has configured MFA as _required_, it will prompt you every +If an administrator has configured MFA as *required*, it will prompt you every time you attempt to log in. You will be unable to access the CMS until you have completed this process. @@ -35,10 +35,9 @@ verification you set up will become your default method. This method displays first when logging in. You can add more methods or change the default method via your CMS profile. -
-Setting up additional verification methods will give you more ways to log in if -you lose access to your default method. -
+> [!TIP] +> Setting up additional verification methods will give you more ways to log in if +> you lose access to your default method. ## 3. Backup your recovery codes diff --git a/docs/en/userguide/01_User_manual/03_Using_security_keys.md b/docs/en/userguide/01_User_manual/03_Using_security_keys.md index 7ac90461..f50889af 100644 --- a/docs/en/userguide/01_User_manual/03_Using_security_keys.md +++ b/docs/en/userguide/01_User_manual/03_Using_security_keys.md @@ -25,9 +25,8 @@ You must also log in via **HTTPS**. If there is no padlock in the address bar of your browser, try changing `http://` to `https://` at the beginning of the address. -
-Security keys are not recommended for use with Subsites. If you intend to log in to a Subsite over a different website domain to your main site, your security key will not be compatible. This is an intentional security requirement of the WebAuthn standard. -
+> [!CAUTION] +> Security keys are not recommended for use with Subsites. If you intend to log in to a Subsite over a different website domain to your main site, your security key will not be compatible. This is an intentional security requirement of the WebAuthn standard. ## Setting up with a security key @@ -45,7 +44,7 @@ area or button in the centre of the key. ![A screenshot of the Security key setup flow waiting for the security key to be activated](../_images/01-03-3-security-key-progress.png) -Once you see the message _Key verified_, press **Complete registration** to +Once you see the message *Key verified*, press **Complete registration** to finish registering the key with your account. ![A screenshot of a successful security key verification in the Security key setup flow](../_images/01-03-4-security-key-verified.png) diff --git a/docs/en/userguide/01_User_manual/04_Regaining_access.md b/docs/en/userguide/01_User_manual/04_Regaining_access.md index c915093e..9e98ef14 100644 --- a/docs/en/userguide/01_User_manual/04_Regaining_access.md +++ b/docs/en/userguide/01_User_manual/04_Regaining_access.md @@ -23,11 +23,10 @@ be logged into the CMS. ![A screenshot of a user being prompted to enter a backup code during login](../_images/01-04-3-recovery-code.png) -
-If your primary MFA method is permanently lost, make sure you visit your profile -and remove or reset it before logging out. If you are running out of backup -codes, generate a new set to make sure you don't lose access to your account. -
+> [!IMPORTANT] +> If your primary MFA method is permanently lost, make sure you visit your profile +> and remove or reset it before logging out. If you are running out of backup +> codes, generate a new set to make sure you don't lose access to your account. ## Resetting your account diff --git a/docs/en/userguide/01_User_manual/05_Managing_your_MFA_settings.md b/docs/en/userguide/01_User_manual/05_Managing_your_MFA_settings.md index 5eb5690f..a5e30240 100644 --- a/docs/en/userguide/01_User_manual/05_Managing_your_MFA_settings.md +++ b/docs/en/userguide/01_User_manual/05_Managing_your_MFA_settings.md @@ -16,8 +16,8 @@ re-enter your password to access them. 1. Go to your profile page, and find the Multi-factor authentication settings area. If you are prompted for your password, enter it. -2. Press **Add another MFA method**. If prompted, pick which method to set up. -3. Follow the relevant guide for setting up the method: +1. Press **Add another MFA method**. If prompted, pick which method to set up. +1. Follow the relevant guide for setting up the method: - [Authenticator app](using_authenticator_apps) - [Security key](using_security_keys) @@ -27,7 +27,7 @@ re-enter your password to access them. 1. Go to your profile page, and find the Multi-factor authentication settings area. If you are prompted for your password, enter it. -2. Find the method you want to modify, and press the action beneath it that you +1. Find the method you want to modify, and press the action beneath it that you want to take. Actions include: - **Remove:** This will delete the method. This action is only available if you have multiple methods registered, or if MFA is optional for your site. @@ -42,8 +42,8 @@ re-enter your password to access them. 1. Go to your profile page, and find the Multi-factor authentication settings area. If you are prompted for your password, enter it. -2. Find the Backup codes method, and press the **Reset** action. -3. You will be presented with a new set of backup codes, which you should store +1. Find the Backup codes method, and press the **Reset** action. +1. You will be presented with a new set of backup codes, which you should store in a safe place. ![A screenshot of the dialog presented when a user presses the 'Reset' action on their CMS profile](../_images/01-05-4-reset-recovery-codes.png) diff --git a/docs/en/userguide/01_User_manual/06_Remember_trusted_devices.md b/docs/en/userguide/01_User_manual/06_Remember_trusted_devices.md index 2b708f3a..91be24d2 100644 --- a/docs/en/userguide/01_User_manual/06_Remember_trusted_devices.md +++ b/docs/en/userguide/01_User_manual/06_Remember_trusted_devices.md @@ -10,10 +10,8 @@ In order to avoid repeat requests to log in with MFA through a device you know t ## Marking you current device as trusted 1. Visit the login screen. - -2. In addition to entering your Email and Password, choose the option to **Keep me signed in**. - -3. Continue to log in with your registered MFA method. +1. In addition to entering your Email and Password, choose the option to **Keep me signed in**. +1. Continue to log in with your registered MFA method. ![A screenshot of the login screen, highlighting the checkbox to keep the user signed in](../_images/01-06-1-keep_me_signed_in.png) @@ -23,10 +21,9 @@ When logging in with the **Keep me signed in** option selected, a time-based coo In addition to remembering the device, your username and password will also be remembered. -
-It is important that you only choose to use this option on a device you know to be secure. A secure device may be one that requires you to log in before being able to access the Internet, for example one within an office network, or a personal mobile device with a passcode. - -**A device shared between multiple people would not be considered secure and you should not use this option.** -
+> [!IMPORTANT] +> Only choose to use this option on a device you know to be secure. A secure device may be one that requires you to log in before being able to access the Internet, for example one within an office network, or a personal mobile device with a passcode. +> +> A device shared between multiple people would not be considered secure and you should not use this option. This functionality is provided by default with the Silverstripe CMS however it may be disabled by a Developer in some projects. diff --git a/docs/en/userguide/02_Administrator_manual/01_Configuring_MFA_site_settings.md b/docs/en/userguide/02_Administrator_manual/01_Configuring_MFA_site_settings.md index c05fd6a5..a39e16b0 100644 --- a/docs/en/userguide/02_Administrator_manual/01_Configuring_MFA_site_settings.md +++ b/docs/en/userguide/02_Administrator_manual/01_Configuring_MFA_site_settings.md @@ -28,12 +28,11 @@ they will remain logged out. Once a user has set up an MFA method on their account, they will not be able to remove it unless they have added another. -
-**Set a date.** - -As an administrator, you can set the date for when MFA will be become mandatory. -MFA will be optional before this date, however users without MFA configured will -be prompted with the option to set it up on every login, until MFA is set up. - -![A screenshot of the site-wide MFA settings UI with the 'MFA is required for everyone' option selected and a date entered in the 'MFA will be required from' field](../_images/02-01-2-grace-period.png) -
+> [!TIP] +> **Set a date.** +> +> As an administrator, you can set the date for when MFA will be become mandatory. +> MFA will be optional before this date, however users without MFA configured will +> be prompted with the option to set it up on every login, until MFA is set up. +> +> ![A screenshot of the site-wide MFA settings UI with the 'MFA is required for everyone' option selected and a date entered in the 'MFA will be required from' field](../_images/02-01-2-grace-period.png) diff --git a/docs/en/userguide/02_Administrator_manual/02_Resetting_accounts.md b/docs/en/userguide/02_Administrator_manual/02_Resetting_accounts.md index 0c9fd9af..2bfdf3e6 100644 --- a/docs/en/userguide/02_Administrator_manual/02_Resetting_accounts.md +++ b/docs/en/userguide/02_Administrator_manual/02_Resetting_accounts.md @@ -11,9 +11,9 @@ their accounts by sending them an email to reset their account via Silverstripe ![A screenshot of a user's profile from the perspective of an administrator](../_images/02-02-1-account-reset.png) 1. Verify that the person requesting the reset owns the account. -2. Navigate to the **Security** section, and select the member requesting an +1. Navigate to the **Security** section, and select the member requesting an account reset. -3. Locate the **Multi-factor Authentication settings (MFA)** field, and if +1. Locate the **Multi-factor Authentication settings (MFA)** field, and if prompted, re-enter your password to unlock it. Press **Send account reset email**, and confirm the action in the dialog. diff --git a/docs/en/userguide/index.md b/docs/en/userguide/index.md index 714763d5..02e98608 100644 --- a/docs/en/userguide/index.md +++ b/docs/en/userguide/index.md @@ -1,10 +1,10 @@ --- -title: Multi-factor Authentication (MFA) +title: Multi-factor authentication (MFA) --- -# Multi-Factor Authentication (MFA) +# Multi-Factor authentication (MFA) -## What is Multi-factor authentication? +## What is multi-factor authentication? Multi-factor authentication (MFA), often referred to as Two-factor authentication (2FA), is an extra layer of security designed to be used @@ -20,13 +20,13 @@ are - for example your fingerprint or face. For more information, see Two popular verification methods are supported by the MFA feature of Silverstripe CMS: -### Authenticator apps (TOTP) +### Authenticator apps (`TOTP`) An authenticator app is installed on your phone which generates temporary single-use passcodes needed for MFA verification. Each code is usable for only a short period of time before a new one is automatically generated. -### Security keys (WebAuthn) +### Security keys (`WebAuthn`) A security key is a physical device, such as a USB key, that is activated during MFA verification. This may involve plugging the device into your computer or @@ -35,15 +35,14 @@ communications (NFC). To use a security key for MFA within Silverstripe CMS, you in using a supported browser over HTTPS (see [Using security keys](user_manual/using_security_keys) for details). -
-**Recovery codes** - -Recovery codes are a backup verification method. In the event that you lose -access to your other verification methods, a recovery code can be used to -regain access to your account. A set of codes will be provided to you when you -first set up an MFA verification method on your account. You can only use each -of these codes once, and they should be stored somewhere safe. -
+> [!IMPORTANT] +> **Recovery codes** +> +> Recovery codes are a backup verification method. In the event that you lose +> access to your other verification methods, a recovery code can be used to +> regain access to your account. A set of codes will be provided to you when you +> first set up an MFA verification method on your account. You can only use each +> of these codes once, and they should be stored somewhere safe. ## User manual