Skip to content

Commit f550f88

Browse files
author
Guy Marriott
authored
DOCS Expand Method Creation docs, shift BasicMath components int… (#227)
DOCS Expand Method Creation docs, shift BasicMath components into them
2 parents 2c3a076 + 0bdfac4 commit f550f88

10 files changed

Lines changed: 254 additions & 205 deletions

File tree

client/dist/js/bundle-cms.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/dist/js/bundle.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import Register from 'components/BackupCodes/Register';
22
import Verify from 'components/BackupCodes/Verify';
3-
import BasicMathRegister from 'components/BasicMath/Register';
4-
import BasicMathLogin from 'components/BasicMath/Login';
53
import Injector from 'lib/Injector'; // eslint-disable-line
64

75
export default () => {
86
Injector.component.registerMany({
97
BackupCodeRegister: Register,
108
BackupCodeVerify: Verify,
11-
BasicMathRegister,
12-
BasicMathLogin,
139
});
1410
};

client/src/components/BasicMath/Login.js

Lines changed: 0 additions & 64 deletions
This file was deleted.

client/src/components/BasicMath/Register.js

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Creating a new MFA method: Backend
2+
3+
<!-- TODO: Documentation covering Method interfaces, etc. -->
4+
5+
## Method availability
6+
7+
If your method isn't available in some situations, and you can determine this via server-side state, you can provide
8+
this information to the frontend via `MethodInterface::isAvailable()`, for example:
9+
10+
```php
11+
class MyMethod implements MethodInterface
12+
{
13+
public function isAvailable(): bool
14+
{
15+
return Injector::inst()->get(HTTPRequest::class)->getHeader('something') === 'example';
16+
}
17+
18+
public function getUnavailableMessage(): string
19+
{
20+
return 'My silly example criteria was not fulfilled, so you cannot use me.';
21+
}
22+
}
23+
```
24+
25+
The results of both of these methods are automatically exposed to the MFA application schema when the registration /
26+
verification UI is loaded, so no extra work is required to incorporate them.
27+
28+
If you need to determine the availability of your method via the frontend, see [Creating a new MFA method: Frontend](creating-mfa-method-frontend.md#method-availability)
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# Creating a new MFA method: Front-end
2+
3+
## Introduction
4+
5+
The MFA module provides a clear path for creating additional authentication methods. In this document we'll cover how to
6+
implement the front-end portion of the required code, using the Basic Math method as an example. Some prior experience
7+
with React / Redux is recommended.
8+
9+
The front-end components of MFA make use of [`react-injector`](https://github.com/silverstripe/react-injector/)
10+
(Injector) to allow sharing of React components and Redux reducers between separate JS bundles. You can find more
11+
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).
12+
13+
You'll find it easiest to get up and running by matching the NPM dependencies and Webpack configuration used in the TOTP
14+
and WebAuthn modules, with a single entry point that handles registering your components with Injector. We also suggest
15+
making use of the i18n library, exposed to components as `window.ss.i18n`, and shown in the examples below.
16+
17+
## Create components
18+
19+
In order to handle both registration of your method, and authentication via it, you'll need to provide a component for
20+
each. The Register and Verify components in the core MFA module are designed to fetch and render your component when the
21+
user selects your method, either in the registration flow or when authenticating.
22+
23+
### Register
24+
25+
Your component for registration will need to accept a couple of key props:
26+
27+
- `onCompleteRegistration`: A callback that should be invoked when your registration process is complete. Pass in an
28+
object with any data that needs to be passed to your `RegisterHandlerInterface::register()` implementation to complete
29+
the registration process.
30+
- `onBack`: A callback that should be invoked if the user wants to pick another method. We recommend rendering a 'Back'
31+
button in the same fashion as the TOTP / WebAuthn methods do.
32+
- Any data you return from your `RegisterHandlerInterface::start()` implementation will also be provided to the
33+
component as props. For example, the TOTP module sends a code to expose in the UI for the user to scan as a QR code or
34+
enter manually into their authenticator app.
35+
36+
A Register component for Basic Math might look like this:
37+
38+
```jsx
39+
import React, { Component } from 'react';
40+
41+
class BasicMathRegister extends Component {
42+
constructor(props) {
43+
super(props);
44+
45+
this.state = {
46+
secret: '',
47+
};
48+
49+
this.handleChange = this.handleChange.bind(this);
50+
}
51+
52+
handleChange(event) {
53+
this.setState({ secret: event.target.value });
54+
}
55+
56+
render() {
57+
const { onCompleteRegistration, onBack } = this.props;
58+
const { ss: { i18n } } = window;
59+
60+
return (
61+
<div className="mfa-register-backup-codes__container">
62+
<label htmlFor="secret">Enter a secret number:</label>
63+
<input id="secret" type="text" value={this.state.secret} onChange={this.handleChange} />
64+
65+
<button
66+
className="btn btn-primary"
67+
onClick={() => onCompleteRegistration({ number: this.state.secret })}
68+
>
69+
{i18n._t('MFABackupCodesRegister.FINISH', 'Finish')}
70+
</button>
71+
<button
72+
className="btn btn-secondary"
73+
onClick={() => onBack()}
74+
>
75+
{i18n._t('MFABackupCodesRegister.BACK', 'Back')}
76+
</button>
77+
</div>
78+
);
79+
}
80+
}
81+
82+
export default Register;
83+
```
84+
85+
### Verify
86+
87+
Your verification component will look similar to your registration one - it should accept the following props:
88+
89+
- `onCompleteVerification`: A callback that should be invoked when the user has completed the challenge presented, with
90+
any data that your `VerifyHandlerInterface::verify()` implementation needs to confirm the user's identity. **NOTE:**
91+
It is _imperative_ that your backend code is involved in the verification process, as providing secrets to the browser
92+
or otherwise relying solely on it to approve the authentication can result in significant security flaws.
93+
- `moreOptionsControl`: A React component to render in your UI, which presents a button for users to pick a different
94+
method to authenticate with. We recommend referencing the layout of the TOTP / WebAuthn implementations.
95+
- Any data you return from your `VerifyHandlerInterface::start()` implementation will also be provided to the
96+
component as props. For example, the WebAuthn module sends a challenge for the security key to sign.
97+
98+
A Verify component for Basic Math might look like this:
99+
100+
```jsx
101+
import React, { Component } from 'react';
102+
103+
class BasicMathVerify extends Component {
104+
constructor(props) {
105+
super(props);
106+
107+
this.state = {
108+
answer: '',
109+
};
110+
111+
this.handleChange = this.handleChange.bind(this);
112+
}
113+
114+
handleChange(event) {
115+
this.setState({
116+
answer: event.target.value,
117+
});
118+
}
119+
120+
renderQuestion() {
121+
const { numbers } = this.props;
122+
123+
return `What's the sum of ${numbers.join(', ')} and your secret number?`;
124+
}
125+
126+
render() {
127+
const { onCompleteVerification, moreOptionsControl, numbers } = this.props;
128+
const { ss: { i18n } } = window;
129+
130+
if (!numbers) {
131+
return (
132+
<div>
133+
<h3>{i18n._t('BasicMathLogin.LOADING', 'Loading...')}</h3>
134+
{ moreOptionsControl }
135+
</div>
136+
);
137+
}
138+
139+
return (
140+
<div className="mfa-register-backup-codes__container">
141+
<label style={{ display: 'block' }} htmlFor="answer">{this.renderQuestion()}</label>
142+
<input id="answer" type="text" value={this.state.answer} onChange={this.handleChange} />
143+
<div>
144+
<button
145+
className="btn btn-primary"
146+
onClick={() => onCompleteVerification({ answer: this.state.answer })}
147+
>
148+
{i18n._t('BasicMathLogin.FINISH', 'Finish')}
149+
</button>
150+
{ moreOptionsControl }
151+
</div>
152+
</div>
153+
);
154+
}
155+
}
156+
157+
export default BasicMathVerify;
158+
```
159+
160+
## Register components with Injector
161+
162+
In order for your components to be found and rendered by the MFA module, you'll need to register them with Injector.
163+
Your JS entrypoint (the file Webpack is pointed at) should contain the following:
164+
165+
```js
166+
import BasicMathRegister from './components/BasicMathRegister';
167+
import BasicMathVerify from './components/BasicMathVerify';
168+
import Injector from 'lib/Injector'; // available via expose-loader
169+
170+
// Injector expects dependencies to be registered during this event, and initialises itself afterwards
171+
window.document.addEventListener('DOMContentLoaded', () => {
172+
Injector.component.registerMany({
173+
BasicMathRegister,
174+
BasicMathVerify,
175+
});
176+
});
177+
```
178+
179+
You can then specify the component names via `VerifyHandlerInterface::getComponent()` and
180+
`RegisterHandlerInterface::getComponent()`, and MFA will render them when your method is selected.
181+
182+
## Method availability
183+
184+
If your method needs to rely on frontend environment state to determine whether it's available (such as the browser
185+
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)
186+
that will initialise some "availability" information in the Redux store, which the MFA module will look for when it
187+
determines whether a method is available to be used or not. For example:
188+
189+
```jsx
190+
// File: webauthn-module/client/src/state/availability/reducer.js
191+
export default (state = {}) => {
192+
const isAvailable = typeof window.AuthenticatorResponse !== 'undefined';
193+
const availability = isAvailable ? {} : {
194+
isAvailable,
195+
unavailableMessage: 'Not supported by your browser.',
196+
};
197+
198+
return { ...state, ...availability };
199+
};
200+
```
201+
202+
You must register this reducer with Injector with a name that matches the pattern `[urlSegment]Availability`. This is
203+
required for the MFA module to find this part of the redux state. For example:
204+
205+
```jsx
206+
// File: webauthn-module/client/src/boot/index.js
207+
import Injector from 'lib/Injector';
208+
import reducer from 'state/availability/reducer';
209+
210+
export default () => {
211+
Injector.reducer.register('web-authnAvailability', reducer);
212+
};
213+
```
214+
215+
Any part of the MFA React application that has the `withMethodAvailability` [HOC](https://reactjs.org/docs/higher-order-components.html)
216+
applied to it will now have access to use `this.props.isAvailable(method)` and `this.props.getUnavailableMessage(method)`
217+
in order to get a compiled set of this information, giving priority to frontend methods defined via Redux, and falling
218+
back to backend definitions that come from the method's schema during the app mount. For this reason, it is important
219+
that any Redux reducers you define only contribute information when they need to, since information provided will
220+
take priority over the backend method definitions if it exists.
221+
222+
If you need to determine the availability of your method via the backend, see [Creating a new MFA method: Backend](mfa-method-backend.md#method-availability)

0 commit comments

Comments
 (0)