Skip to content

Commit cc26ba6

Browse files
author
Garion Herman
committed
DOCS Expand Method Creation docs, shift BasicMath components into them
1 parent 2c3a076 commit cc26ba6

10 files changed

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