Skip to content

Commit 7f4da2d

Browse files
authored
feat(rest-example): add new mfa otp process (#1050)
1 parent b1eb1b2 commit 7f4da2d

File tree

27 files changed

+559
-150
lines changed

27 files changed

+559
-150
lines changed

examples/accounts-boost/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"apollo-server": "2.16.1",
2121
"graphql": "14.7.0",
2222
"graphql-tools": "5.0.0",
23-
"lodash": "4.17.19",
23+
"lodash": "4.17.20",
2424
"node-fetch": "2.6.0",
2525
"tslib": "2.0.1"
2626
},

examples/graphql-server-typescript/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"@graphql-tools/merge": "6.0.16",
2121
"apollo-server": "2.16.1",
2222
"graphql": "14.7.0",
23-
"lodash": "4.17.19",
23+
"lodash": "4.17.20",
2424
"mongoose": "5.9.28",
2525
"tslib": "2.0.1"
2626
},

examples/react-rest-typescript/src/Login.tsx

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import { useFormik, FormikErrors } from 'formik';
1616
import { SnackBarContentError } from './components/SnackBarContentError';
1717
import { useAuth } from './components/AuthContext';
1818
import { UnauthenticatedContainer } from './components/UnauthenticatedContainer';
19+
import { LoginMfa } from './LoginMfa';
1920

20-
const useStyles = makeStyles(theme => ({
21+
const useStyles = makeStyles((theme) => ({
2122
cardContent: {
2223
padding: theme.spacing(3),
2324
},
@@ -41,20 +42,19 @@ const ResetPasswordLink = React.forwardRef<RouterLink, any>((props, ref) => (
4142
interface LoginValues {
4243
email: string;
4344
password: string;
44-
code: string;
4545
}
4646

4747
const Login = ({ history }: RouteComponentProps<{}>) => {
4848
const classes = useStyles();
4949
const { loginWithService } = useAuth();
5050
const [error, setError] = useState<string | undefined>();
51+
const [mfaToken, setMfaToken] = useState<string | undefined>();
5152
const formik = useFormik<LoginValues>({
5253
initialValues: {
5354
email: '',
5455
password: '',
55-
code: '',
5656
},
57-
validate: values => {
57+
validate: (values) => {
5858
const errors: FormikErrors<LoginValues> = {};
5959
if (!values.email) {
6060
errors.email = 'Required';
@@ -66,13 +66,17 @@ const Login = ({ history }: RouteComponentProps<{}>) => {
6666
},
6767
onSubmit: async (values, { setSubmitting }) => {
6868
try {
69-
await loginWithService('password', {
69+
const loginResponse = await loginWithService('password', {
7070
user: {
7171
email: values.email,
7272
},
7373
password: values.password,
74-
code: values.code,
7574
});
75+
if ('mfaToken' in loginResponse) {
76+
setMfaToken(loginResponse.mfaToken);
77+
return;
78+
}
79+
// No MFA is set so we can continue to dashboard
7680
history.push('/');
7781
} catch (error) {
7882
setError(error.message);
@@ -81,6 +85,10 @@ const Login = ({ history }: RouteComponentProps<{}>) => {
8185
},
8286
});
8387

88+
if (mfaToken) {
89+
return <LoginMfa mfaToken={mfaToken} />;
90+
}
91+
8492
return (
8593
<UnauthenticatedContainer>
8694
<Snackbar
@@ -130,18 +138,6 @@ const Login = ({ history }: RouteComponentProps<{}>) => {
130138
helperText={formik.touched.password && formik.errors.password}
131139
/>
132140
</Grid>
133-
<Grid item xs={12}>
134-
<TextField
135-
label="2fa code if enabled"
136-
variant="outlined"
137-
fullWidth={true}
138-
id="code"
139-
value={formik.values.code}
140-
onChange={formik.handleChange}
141-
error={Boolean(formik.errors.code && formik.touched.code)}
142-
helperText={formik.touched.code && formik.errors.code}
143-
/>
144-
</Grid>
145141
<Grid item xs={12} md={6}>
146142
<Button
147143
variant="contained"
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { useHistory } from 'react-router-dom';
3+
import {
4+
Button,
5+
Typography,
6+
makeStyles,
7+
Card,
8+
CardContent,
9+
TextField,
10+
Grid,
11+
Snackbar,
12+
} from '@material-ui/core';
13+
import { useFormik, FormikErrors } from 'formik';
14+
import { SnackBarContentError } from './components/SnackBarContentError';
15+
import { useAuth } from './components/AuthContext';
16+
import { UnauthenticatedContainer } from './components/UnauthenticatedContainer';
17+
import { accountsClient } from './accounts';
18+
19+
const useStyles = makeStyles((theme) => ({
20+
cardContent: {
21+
padding: theme.spacing(3),
22+
},
23+
divider: {
24+
marginTop: theme.spacing(2),
25+
marginBottom: theme.spacing(2),
26+
},
27+
logo: {
28+
maxWidth: '100%',
29+
width: 250,
30+
},
31+
}));
32+
33+
interface LoginMfaProps {
34+
mfaToken: string;
35+
}
36+
37+
interface LoginMfaValues {
38+
code: string;
39+
}
40+
41+
export const LoginMfa = ({ mfaToken }: LoginMfaProps) => {
42+
const classes = useStyles();
43+
const history = useHistory();
44+
const { loginWithService } = useAuth();
45+
const [error, setError] = useState<string | undefined>();
46+
const formik = useFormik<LoginMfaValues>({
47+
initialValues: {
48+
code: '',
49+
},
50+
validate: (values) => {
51+
const errors: FormikErrors<LoginMfaValues> = {};
52+
if (!values.code) {
53+
errors.code = 'Required';
54+
}
55+
return errors;
56+
},
57+
onSubmit: async (values, { setSubmitting }) => {
58+
try {
59+
await loginWithService('mfa', {
60+
mfaToken,
61+
code: values.code,
62+
});
63+
history.push('/');
64+
} catch (error) {
65+
setError(error.message);
66+
setSubmitting(false);
67+
}
68+
},
69+
});
70+
71+
useEffect(() => {
72+
const fetchAuthenticators = async () => {
73+
// TODO try catch
74+
const data = await accountsClient.authenticatorsByMfaToken(mfaToken);
75+
const authenticator = data[0];
76+
await accountsClient.mfaChallenge(mfaToken, authenticator.id);
77+
};
78+
79+
fetchAuthenticators();
80+
}, [mfaToken]);
81+
82+
return (
83+
<UnauthenticatedContainer>
84+
<Snackbar
85+
anchorOrigin={{
86+
vertical: 'top',
87+
horizontal: 'center',
88+
}}
89+
open={!!error}
90+
onClose={() => setError(undefined)}
91+
>
92+
<SnackBarContentError message={error} />
93+
</Snackbar>
94+
95+
<Card>
96+
<CardContent className={classes.cardContent}>
97+
<form onSubmit={formik.handleSubmit}>
98+
<Grid container spacing={3}>
99+
<Grid item xs={12}>
100+
<img src="/logo.png" alt="Logo" className={classes.logo} />
101+
</Grid>
102+
<Grid item xs={12}>
103+
<Typography variant="h5">Two-factor authentication</Typography>
104+
</Grid>
105+
<Grid item xs={12}>
106+
<Typography variant="body2" color="textSecondary">
107+
Your account is protected by two-factor authentication. To verify your identity
108+
you need to provide the access code from your authenticator app.
109+
</Typography>
110+
</Grid>
111+
<Grid item xs={12}>
112+
<TextField
113+
label="Authenticator code"
114+
variant="outlined"
115+
fullWidth={true}
116+
id="code"
117+
value={formik.values.code}
118+
onChange={formik.handleChange}
119+
error={Boolean(formik.errors.code && formik.touched.code)}
120+
helperText={formik.touched.code && formik.errors.code}
121+
/>
122+
</Grid>
123+
<Grid item xs={12} md={6}>
124+
<Button
125+
variant="contained"
126+
color="primary"
127+
type="submit"
128+
disabled={formik.isSubmitting}
129+
>
130+
Sign in
131+
</Button>
132+
</Grid>
133+
</Grid>
134+
</form>
135+
</CardContent>
136+
</Card>
137+
</UnauthenticatedContainer>
138+
);
139+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react';
2+
import { makeStyles, Typography, Divider, Card, CardHeader, CardContent } from '@material-ui/core';
3+
import { AuthenticatedContainer } from './components/AuthenticatedContainer';
4+
5+
const useStyles = makeStyles((theme) => ({
6+
divider: {
7+
marginTop: theme.spacing(2),
8+
},
9+
card: {
10+
marginTop: theme.spacing(3),
11+
},
12+
cardHeader: {
13+
paddingLeft: theme.spacing(3),
14+
paddingRight: theme.spacing(3),
15+
},
16+
cardContent: {
17+
padding: theme.spacing(3),
18+
},
19+
authenticatorDescription: {
20+
marginTop: theme.spacing(1),
21+
},
22+
}));
23+
24+
export const MfaAuthenticator = () => {
25+
const classes = useStyles();
26+
27+
return (
28+
<AuthenticatedContainer>
29+
<Typography variant="h5">Authenticator Details</Typography>
30+
<Divider className={classes.divider} />
31+
<Card className={classes.card}>
32+
{/* TODO title and content based on the type */}
33+
<CardHeader subheader="Authenticator app" className={classes.cardHeader} />
34+
<Divider />
35+
<CardContent className={classes.cardContent}>
36+
TODO
37+
<Typography className={classes.authenticatorDescription}>
38+
An authenticator application that supports TOTP (like Google Authenticator or 1Password)
39+
can be used to conveniently secure your account. A new token is generated every 30
40+
seconds.
41+
</Typography>
42+
</CardContent>
43+
<Divider />
44+
</Card>
45+
</AuthenticatedContainer>
46+
);
47+
};

examples/react-rest-typescript/src/Router.tsx

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { BrowserRouter, Route, Redirect } from 'react-router-dom';
2+
import { BrowserRouter, Route, Redirect, Switch } from 'react-router-dom';
33
import { CssBaseline } from '@material-ui/core';
44
import { AuthProvider, useAuth } from './components/AuthContext';
55
import Signup from './Signup';
@@ -9,6 +9,8 @@ import ResetPassword from './ResetPassword';
99
import VerifyEmail from './VerifyEmail';
1010
import { Email } from './Email';
1111
import { Security } from './Security';
12+
import { TwoFactorOtp } from './TwoFactorOtp';
13+
import { MfaAuthenticator } from './MfaAuthenticator';
1214

1315
// A wrapper for <Route> that redirects to the login
1416
// screen if you're not yet authenticated.
@@ -39,23 +41,31 @@ const Router = () => {
3941
<AuthProvider>
4042
<CssBaseline />
4143

42-
{/* Authenticated routes */}
43-
<PrivateRoute exact path="/">
44-
<Home />
45-
</PrivateRoute>
46-
<PrivateRoute path="/emails">
47-
<Email />
48-
</PrivateRoute>
49-
<PrivateRoute path="/security">
50-
<Security />
51-
</PrivateRoute>
44+
<Switch>
45+
{/* Authenticated routes */}
46+
<PrivateRoute exact path="/">
47+
<Home />
48+
</PrivateRoute>
49+
<PrivateRoute path="/emails">
50+
<Email />
51+
</PrivateRoute>
52+
<PrivateRoute exact path="/security">
53+
<Security />
54+
</PrivateRoute>
55+
<PrivateRoute exact path="/security/mfa/otp">
56+
<TwoFactorOtp />
57+
</PrivateRoute>
58+
<PrivateRoute path="/security/mfa/:authenticatorId">
59+
<MfaAuthenticator />
60+
</PrivateRoute>
5261

53-
{/* Unauthenticated routes */}
54-
<Route path="/signup" component={Signup} />
55-
<Route path="/login" component={Login} />
56-
<Route exact path="/reset-password" component={ResetPassword} />
57-
<Route path="/reset-password/:token" component={ResetPassword} />
58-
<Route path="/verify-email/:token" component={VerifyEmail} />
62+
{/* Unauthenticated routes */}
63+
<Route path="/signup" component={Signup} />
64+
<Route path="/login" component={Login} />
65+
<Route exact path="/reset-password" component={ResetPassword} />
66+
<Route path="/reset-password/:token" component={ResetPassword} />
67+
<Route path="/verify-email/:token" component={VerifyEmail} />
68+
</Switch>
5969
</AuthProvider>
6070
</BrowserRouter>
6171
);

0 commit comments

Comments
 (0)