Skip to content

Commit aeb16be

Browse files
committed
Added 2FA to example
1 parent dba3604 commit aeb16be

File tree

7 files changed

+170
-10
lines changed

7 files changed

+170
-10
lines changed

example/docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ services:
2222
BACKEND_CERT_DIR: '/app/certs/'
2323
BACKEND_CERT_HOSTNAMES: 'proxy'
2424
SMTP_SERVER: 'smtp:1025'
25+
TOTP_ENABLE: '1'
26+
TOTP_ENCRYPT_KEY: 'w66iO0l3Kru7Qgpx'
2527
depends_on:
2628
- mongo
2729
mongo:

example/frontend/src/App.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
nav {
22
margin-bottom: 50px;
3+
}
4+
5+
.btn-margin-right {
6+
margin-right: 5px;
37
}

example/frontend/src/App.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import LoginForm from './pages/LoginForm'
1313
import ConfirmPage from './pages/ConfirmPage'
1414
import DashboardPage from './pages/DashboardPage';
1515
import Ajax from './Ajax';
16+
import TotpEnableForm from './pages/TotpEnableForm';
1617

1718
function RouteWithSubRoutes(route) {
1819
return (
@@ -47,6 +48,7 @@ const NavHeader = props => {
4748
{window.sessionStorage.getItem("accessToken") == null ? <Nav.Link href="/signup.html">Sign Up</Nav.Link> : null}
4849
{window.sessionStorage.getItem("accessToken") == null ? <Nav.Link href="/login.html">Log In</Nav.Link> : null}
4950
{window.sessionStorage.getItem("accessToken") != null ? <Nav.Link href="/dashboard.html">Dashboard</Nav.Link> : null}
51+
{window.sessionStorage.getItem("accessToken") != null ? <Nav.Link href="/totp.html">Security</Nav.Link> : null}
5052
{window.sessionStorage.getItem("accessToken") != null ? <Nav.Link onClick={handleLogout}>Log Out</Nav.Link> : null}
5153
</Nav>
5254
</Navbar>
@@ -74,6 +76,10 @@ const routes = [
7476
{
7577
path: "/dashboard.html",
7678
component: DashboardPage
79+
},
80+
{
81+
path: "/totp.html",
82+
component: TotpEnableForm
7783
}
7884
];
7985

example/frontend/src/pages/HomePage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export default class HomePage extends React.Component {
1010
<Col>
1111
<h1 className="display-4 font-weight-normal">JWT Auth Proxy Example</h1>
1212
<p className="lead font-weight-normal">This web application is meant to demonstrate the usage of the JWT Auth Proxy. You can use it as a template to build your own awesome and secure application.</p>
13-
<Button href="https://github.com/virtualzone/jwt-auth-proxy" className="btn btn-primary">GitHub Page</Button>
14-
<Link to="/signup.html" className="btn btn-outline-secondary">Sign up</Link>
13+
<Button href="https://github.com/virtualzone/jwt-auth-proxy" className="btn btn-primary btn-margin-right">GitHub Page</Button>
14+
<Link to="/signup.html" className="btn btn-outline-secondary btn-margin-right">Sign up</Link>
1515
<Link to="/login.html" className="btn btn-outline-secondary">Log in</Link>
1616
</Col>
1717
</Row>

example/frontend/src/pages/LoginForm.tsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ interface LoginFormState {
66
isLoading: boolean
77
hasDataError: boolean
88
email: string
9+
otp: string
10+
requireOtp: boolean
911
password: string
1012
}
1113

@@ -16,6 +18,8 @@ export default class LoginForm extends React.Component<{}, LoginFormState> {
1618
isLoading: false,
1719
hasDataError: false,
1820
email: '',
21+
otp: '',
22+
requireOtp: false,
1923
password: ''
2024
};
2125
this.handleChange = this.handleChange.bind(this);
@@ -36,13 +40,21 @@ export default class LoginForm extends React.Component<{}, LoginFormState> {
3640
});
3741
let data = {
3842
email: this.state.email,
39-
password: this.state.password
43+
password: this.state.password,
44+
otp: this.state.otp
4045
};
4146
Ajax.postData("/auth/login", data).then(res => {
4247
if (res.status === 200) {
43-
window.sessionStorage.setItem("accessToken", res.json.accessToken);
44-
window.sessionStorage.setItem("refreshToken", res.json.refreshToken);
45-
window.location.href = "/dashboard.html";
48+
if (res.json.otpRequired) {
49+
this.setState({
50+
requireOtp: true,
51+
isLoading: false
52+
});
53+
} else {
54+
window.sessionStorage.setItem("accessToken", res.json.accessToken);
55+
window.sessionStorage.setItem("refreshToken", res.json.refreshToken);
56+
window.location.href = "/dashboard.html";
57+
}
4658
return;
4759
}
4860
this.setState({
@@ -65,13 +77,17 @@ export default class LoginForm extends React.Component<{}, LoginFormState> {
6577
<h1 className="display-4 font-weight-normal">Log In</h1>
6678
<Alert variant="danger" show={this.state.hasDataError}>Please verify the data you have entered.</Alert>
6779
<Form onSubmit={this.handleSubmit}>
68-
<Form.Group controlId="email">
80+
<Form.Group controlId="email" hidden={this.state.requireOtp}>
6981
<Form.Label>Email address</Form.Label>
70-
<Form.Control type="email" name="email" placeholder="[email protected]" value={this.state.email} onChange={this.handleChange} autoFocus={true} />
82+
<Form.Control type="email" name="email" placeholder="[email protected]" value={this.state.email} onChange={this.handleChange} autoFocus={true} required={true} />
7183
</Form.Group>
72-
<Form.Group controlId="password">
84+
<Form.Group controlId="password" hidden={this.state.requireOtp}>
7385
<Form.Label>Password</Form.Label>
74-
<Form.Control type="password" name="password" placeholder="Choose a password" value={this.state.password} onChange={this.handleChange} minLength={8} maxLength={32} />
86+
<Form.Control type="password" name="password" placeholder="Enter your password" value={this.state.password} onChange={this.handleChange} minLength={8} maxLength={32} required={true} />
87+
</Form.Group>
88+
<Form.Group controlId="otp" hidden={!this.state.requireOtp}>
89+
<Form.Label>Six-Digit Code</Form.Label>
90+
<Form.Control type="text" pattern="\d*" name="otp" placeholder="Time-based One-time Password (TOTP)" value={this.state.otp} onChange={this.handleChange} minLength={6} maxLength={6} required={this.state.requireOtp} />
7591
</Form.Group>
7692
<Button variant="primary" type="submit" disabled={this.state.isLoading}>{this.state.isLoading ? 'Loading...' : 'Submit'}</Button>
7793
</Form>
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import React from 'react';
2+
import { Container, Row, Col, Form, Button, Alert } from "react-bootstrap";
3+
import Ajax from '../Ajax';
4+
5+
interface TotpEnableFormState {
6+
isLoading: boolean
7+
hasDataError: boolean
8+
stateInit: boolean
9+
stateConfirm: boolean
10+
stateFinish: boolean
11+
secret: string
12+
qrCode: string
13+
otp: string
14+
}
15+
16+
export default class TotpEnableForm extends React.Component<{}, TotpEnableFormState> {
17+
constructor(props: any) {
18+
super(props);
19+
this.state = {
20+
isLoading: false,
21+
hasDataError: false,
22+
stateInit: true,
23+
stateConfirm: false,
24+
stateFinish: false,
25+
secret: "",
26+
qrCode: "",
27+
otp: ""
28+
};
29+
this.handleChange = this.handleChange.bind(this);
30+
this.handleSubmitInit = this.handleSubmitInit.bind(this);
31+
this.handleSubmitConfirm = this.handleSubmitConfirm.bind(this);
32+
}
33+
34+
handleChange(event: React.FormEvent) {
35+
let target = event.target as HTMLInputElement;
36+
this.setState({[target.name]: target.value} as React.ComponentState );
37+
event.preventDefault();
38+
}
39+
40+
handleSubmitInit(event: React.FormEvent) {
41+
event.preventDefault();
42+
this.setState({
43+
isLoading: true,
44+
hasDataError: false
45+
});
46+
Ajax.postData("/auth/otp/init").then(res => {
47+
if (res.status === 200) {
48+
this.setState({
49+
isLoading: false,
50+
hasDataError: false,
51+
stateInit: false,
52+
stateConfirm: true,
53+
secret: res.json.secret,
54+
qrCode: "data:image/png;base64," + res.json.image
55+
});
56+
return;
57+
}
58+
this.setState({
59+
hasDataError: true,
60+
isLoading: false
61+
});
62+
}).catch(() => {
63+
this.setState({
64+
hasDataError: true,
65+
isLoading: false
66+
});
67+
});
68+
}
69+
70+
handleSubmitConfirm(event: React.FormEvent) {
71+
event.preventDefault();
72+
this.setState({
73+
isLoading: true,
74+
hasDataError: false
75+
});
76+
let payload = {
77+
passcode: this.state.otp
78+
};
79+
Ajax.postData("/auth/otp/confirm", payload).then(res => {
80+
if (res.status === 204) {
81+
this.setState({
82+
isLoading: false,
83+
hasDataError: false,
84+
stateConfirm: false,
85+
stateFinish: true
86+
});
87+
return;
88+
}
89+
this.setState({
90+
hasDataError: true,
91+
isLoading: false
92+
});
93+
}).catch(() => {
94+
this.setState({
95+
hasDataError: true,
96+
isLoading: false
97+
});
98+
});
99+
}
100+
101+
render() {
102+
return (
103+
<Container>
104+
<Row className="justify-content-md-center">
105+
<Col lg="5">
106+
<h1 className="display-4 font-weight-normal">Security</h1>
107+
<Alert variant="danger" show={this.state.hasDataError}>Please verify the data you have entered.</Alert>
108+
<Form onSubmit={this.handleSubmitInit} hidden={!this.state.stateInit}>
109+
<p>Two-Factor Authentication is not enabled yet. Enable it to add an additional layer of security to your account.</p>
110+
<Button variant="primary" type="submit" disabled={this.state.isLoading}>{this.state.isLoading ? 'Loading...' : 'Enable 2FA'}</Button>
111+
</Form>
112+
<Form onSubmit={this.handleSubmitConfirm} hidden={!this.state.stateConfirm}>
113+
<p>Scan the barcode below with your authenticator app, orenter the code below if you can't use the barcode.</p>
114+
<p><img src={this.state.qrCode} alt="" /></p>
115+
<p>{this.state.secret}</p>
116+
<Form.Group controlId="otp">
117+
<Form.Label>Six-Digit Code from authenticator app</Form.Label>
118+
<Form.Control type="text" pattern="\d*" name="otp" placeholder="Time-based One-time Password (TOTP)" value={this.state.otp} onChange={this.handleChange} minLength={6} maxLength={6} required={true} />
119+
</Form.Group>
120+
<Button variant="primary" type="submit" disabled={this.state.isLoading}>{this.state.isLoading ? 'Loading...' : 'Finish 2FA Setup'}</Button>
121+
</Form>
122+
<div hidden={!this.state.stateFinish}>
123+
<p>2FA is now enabled for your account.</p>
124+
</div>
125+
</Col>
126+
</Row>
127+
</Container>
128+
);
129+
}
130+
}

run.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ TEMPLATE_NEW_PASSWORD=../res/newpassword.tpl \
99
PROXY_TARGET=http://localhost:8090 \
1010
CORS_ENABLE=1 \
1111
BACKEND_GENERATE_CERT=1 \
12+
TOTP_ENABLE=1 \
13+
TOTP_ENCRYPT_KEY=w66iO0l3Kru7Qgpx \
1214
PROXY_WHITELIST=/foo/bar \
1315
go run `ls *.go | grep -v _test.go`

0 commit comments

Comments
 (0)