Skip to content

Commit

Permalink
Merge branch '5.2' into 5
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Apr 15, 2024
2 parents 23fc087 + 062eb95 commit 3502678
Show file tree
Hide file tree
Showing 26 changed files with 322 additions and 218 deletions.
1 change: 1 addition & 0 deletions .doclintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docs/en/
50 changes: 3 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,21 @@
[![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
[here](https://github.com/firesphere/silverstripe-bootstrapmfa).

## 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

Expand Down
6 changes: 6 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -52,6 +53,11 @@
"client/lang"
]
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"autoload": {
"psr-4": {
"SilverStripe\\MFA\\": "src/",
Expand Down
7 changes: 7 additions & 0 deletions docs/en/01_authenticators/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Authenticators
---

# Authenticators

[CHILDREN includeFolders]
20 changes: 16 additions & 4 deletions docs/en/debugging.md → docs/en/01_debugging.md
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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;
Expand All @@ -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());
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
});
});
```
Expand All @@ -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 ? {} : {
Expand All @@ -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';

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 7 additions & 0 deletions docs/en/02_creating-new-mfa-methods/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Creating new MFA methods
---

# Creating new MFA methods

[CHILDREN includeFolders]
10 changes: 7 additions & 3 deletions docs/en/local-development.md → docs/en/03_local-development.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
13 changes: 9 additions & 4 deletions docs/en/encryption.md → docs/en/04_encryption.md
Original file line number Diff line number Diff line change
@@ -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.
45 changes: 45 additions & 0 deletions docs/en/05_datastores.md
Original file line number Diff line number Diff line change
@@ -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');
}
}
```
24 changes: 24 additions & 0 deletions docs/en/06_security.md
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 3502678

Please sign in to comment.