admin Settings & reset MFA

This commit is contained in:
Aaron 2019-02-21 23:22:16 -06:00
parent 3f7c90a381
commit 35661a0672
No known key found for this signature in database
GPG Key ID: 3B5B7597106F0A0E
8 changed files with 144 additions and 48 deletions

View File

@ -20,6 +20,7 @@ import Contributions from 'components/Contributions';
import ContributionForm from 'components/ContributionForm';
import ContributionDetail from 'components/ContributionDetail';
import Moderation from 'components/Moderation';
import Settings from 'components/Settings';
import 'styles/style.less';
@ -55,6 +56,8 @@ class Routes extends React.Component<Props> {
<Route path="/contributions" component={Contributions} />
<Route path="/emails/:type?" component={Emails} />
<Route path="/moderation" component={Moderation} />
<Route path="/settings/2fa-reset" render={() => <MFAuth isReset={true} />} />
<Route path="/settings" component={Settings} />
</Switch>
)}
</Template>

View File

@ -4,6 +4,14 @@
h1 {
margin-top: 1rem;
font-size: 1.5rem;
}
h2 {
font-size: 1.15rem;
}
& .ant-alert {
margin-bottom: 0.8rem;
}
&-codes,
@ -48,7 +56,7 @@
margin-top: 1rem;
text-align: right;
& > button + button {
& > * + * {
margin-left: 0.5rem;
}
}
@ -61,10 +69,6 @@
}
}
h1 {
font-size: 1.5rem;
}
& > div {
margin-bottom: 0.5rem;
}

View File

@ -1,4 +1,5 @@
import React, { ReactNode } from 'react';
import { Link, Redirect } from 'react-router-dom';
import { view } from 'react-easy-state';
import CopyToClipboard from 'react-copy-to-clipboard';
import QRCode from 'qrcode.react';
@ -14,6 +15,10 @@ import store, {
import { downloadString } from 'src/util/file';
import './index.less';
interface Props {
isReset?: boolean;
}
const STATE = {
// remote
isLoginFresh: false,
@ -29,6 +34,7 @@ const STATE = {
hasReadSetup: false,
initializing: false,
hasSavedCodes: false,
hasVerified: false,
password: '',
verifyCode: '',
isVerifying: false,
@ -36,18 +42,18 @@ const STATE = {
};
type State = typeof STATE;
class MFAuth extends React.Component<{}, State> {
class MFAuth extends React.Component<Props, State> {
state = STATE;
componentDidMount() {
this.update2faStateFromServer();
}
render() {
if (store.is2faAuthed) return 'You should not be here.';
const {
loaded,
hasReadSetup,
password,
hasSavedCodes,
hasVerified,
verifyCode,
isLoginFresh,
has2fa,
@ -61,6 +67,7 @@ class MFAuth extends React.Component<{}, State> {
backupCodeCount,
isEmailVerified,
} = this.state;
const { isReset } = this.props;
const emailNotVerifiedWarning = loaded &&
!isEmailVerified && (
@ -93,6 +100,9 @@ class MFAuth extends React.Component<{}, State> {
<div className="MFAuth">
{emailNotVerifiedWarning || (
<>
<h1>
{isReset ? 'Reset two-factor authentication' : 'Two-factor authentication'}
</h1>
{lowBackupCodesWarning}
{children}
</>
@ -105,34 +115,17 @@ class MFAuth extends React.Component<{}, State> {
return wrap(<Spin tip="Loading security details..." />);
}
// STEP 1 (outline)
if (!has2fa && !hasReadSetup) {
return wrap(
<div>
{!has2fa && (
<Alert type="warning" message="Administration requires 2fa setup." />
)}
<h1>Two-factor Authentication Setup</h1>
<p>Please be prepared to perform the following steps:</p>
<ol>
<li>Save two-factor recovery codes</li>
<li>
Setup up TOTP authentication device, typically a smartphone with Google
Authenticator, Authy, 1Password or other compatible authenticator app.
</li>
</ol>
<Button onClick={this.handleReadSetup} type="primary">
I'm ready
</Button>
</div>,
);
}
// STEP 2 (if login is stale)
if (!has2fa && !isLoginFresh) {
// STEP 0. (if login is stale)
if ((!has2fa || isReset) && !isLoginFresh) {
return wrap(
<>
<h1>Please verify your password</h1>
<h2>
<Icon type="unlock" /> Please verify your password
</h2>
<p>
Too much time has elapsed since you last affirmed your credentials, please
enter your password below.
</p>
<Input.Password
onPressEnter={this.handleSubmitPassword}
onChange={e => this.setState({ password: e.target.value })}
@ -151,14 +144,53 @@ class MFAuth extends React.Component<{}, State> {
);
}
// STEP 3 (recovery codes)
if (!has2fa && !hasSavedCodes) {
// STEP 1 (outline)
if ((!has2fa || isReset) && !hasReadSetup) {
return wrap(
<div>
{!has2fa && <Alert type="info" message="Administration requires 2fa setup." />}
{isReset && (
<Alert
type="warning"
message={
<>
Your current recovery codes and authenticator app setup will be
invalidated when you continue.
</>
}
/>
)}
<h2>1. Two-factor Authentication Setup</h2>
<p>Please be prepared to perform the following steps:</p>
<ol>
<li>Save two-factor recovery codes</li>
<li>
Setup up TOTP authentication device, typically a smartphone with Google
Authenticator, Authy, 1Password or other compatible authenticator app.
</li>
</ol>
<div className="MFAuth-controls">
{isReset && (
<Link to="/settings">
<Button>Cancel</Button>
</Link>
)}
<Button onClick={this.handleReadSetup} type="primary">
I'm ready
</Button>
</div>
</div>,
);
}
// STEP 2 (recovery codes)
if ((!has2fa || isReset) && !hasSavedCodes) {
return wrap(
((initializing || !backupCodes.length) && (
<Spin tip="Loading 2fa setup..." />
)) || (
<div>
<h1>Recovery codes</h1>
<h2>2. Recovery codes</h2>
<p>
Please copy, download or print these codes and keep them safe. Treat them
with the same care as passwords.
@ -207,10 +239,10 @@ class MFAuth extends React.Component<{}, State> {
}
// STEP 4 (totp setup/verify)
if (!has2fa && hasSavedCodes) {
if ((!has2fa || isReset) && !hasVerified) {
return wrap(
<div>
<h1>Set up Authenticator</h1>
<h2>3. Set up Authenticator</h2>
<p>
Please scan the barcode with your athenticator application. If you cannot
scan, please{' '}
@ -256,18 +288,18 @@ class MFAuth extends React.Component<{}, State> {
disabled={verifyCode.length === 0}
loading={isVerifying}
>
Enable
{isReset ? 'Reset' : 'Enable'}
</Button>
</div>
</div>,
);
}
// FINAL & unauthed
// unauthed
if (has2fa && !is2faAuthed) {
return wrap(
<>
<h1>2FAuthentication required</h1>
<h2>2FAuthentication required</h2>
<p>
Enter the current code from your authenticator application. Enter a backup
code if you do not have access to your authenticator application.
@ -278,6 +310,7 @@ class MFAuth extends React.Component<{}, State> {
placeholder="123456"
value={verifyCode}
onChange={e => this.setState({ verifyCode: e.target.value })}
onPressEnter={this.handleVerify}
/>
</div>
<div className="MFAuth-controls">
@ -294,15 +327,15 @@ class MFAuth extends React.Component<{}, State> {
);
}
return 'should not get here';
return isReset ? <Redirect to="/settings" /> : 'should not get here';
}
private update2faStateFromServer = () => {
get2fa()
.then(x => {
.then(state => {
this.setState({
loaded: true,
...x,
...state,
});
})
.catch(handleApiError);
@ -314,8 +347,8 @@ class MFAuth extends React.Component<{}, State> {
};
private handleReadSetup = async () => {
this.setState({ hasReadSetup: true });
if (this.state.isLoginFresh) {
this.setState({ hasReadSetup: true });
this.loadSetup();
}
};
@ -323,6 +356,7 @@ class MFAuth extends React.Component<{}, State> {
private handleSubmitPassword = async () => {
const { password } = this.state;
try {
// refresh the login
await refresh(password);
// will set fresh login
await this.update2faStateFromServer();
@ -351,8 +385,9 @@ class MFAuth extends React.Component<{}, State> {
this.setState({ isVerifying: true });
try {
await post2faEnable({ backupCodes, totpSecret, verifyCode });
message.success('2FA setup complete!');
message.success('Two-factor setup complete!');
store.checkLogin(); // should return authenticated status
this.setState({ hasVerified: true });
// await this.update2faStateFromServer();
} catch (e) {
handleApiError(e);
@ -365,7 +400,7 @@ class MFAuth extends React.Component<{}, State> {
this.setState({ isVerifying: true });
try {
await post2faVerify({ verifyCode });
message.success('2FAuthentication verified!');
message.success('Two-factor authentication verified');
store.checkLogin(); // should return authenticated status
} catch (e) {
handleApiError(e);

View File

@ -0,0 +1,11 @@
.Settings {
max-width: 600px;
h1 {
font-size: 1.5rem;
}
& > div {
margin-bottom: 0.5rem;
}
}

View File

@ -0,0 +1,32 @@
import React from 'react';
import { view } from 'react-easy-state';
import { Link } from 'react-router-dom';
import { Button } from 'antd';
import './index.less';
class Settings extends React.Component {
state = {
username: '',
password: '',
};
render() {
return (
<div className="Settings">
<div>
<h1>Two-Factor Authentication</h1>
<p>
This will require saving new recovery codes and setting up an Authenticator
application.{' '}
<b>Current recovery and Authenticator codes will be invalidated.</b>
</p>
<Link to="/settings/2fa-reset">
<Button>Setup 2FA</Button>
</Link>
</div>
</div>
);
}
}
export default view(Settings);

View File

@ -10,6 +10,10 @@
text-align: center;
font-size: 1.5rem;
}
& .ant-menu-item-divider {
background-color: #3d3d3d;
}
}
&-layout {

View File

@ -75,6 +75,13 @@ class Template extends React.Component<Props> {
<span className="nav-text">Moderation</span>
</Link>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="settings">
<Link to="/settings">
<Icon type="setting" />
<span className="nav-text">Settings</span>
</Link>
</Menu.Item>
<Menu.Item key="logout" onClick={store.logout}>
<Icon type="logout" />
<span className="nav-text">Logout</span>

View File

@ -112,7 +112,7 @@ def admin_auth_required(f):
@wraps(f)
def decorated(*args, **kwargs):
user = get_authed_user()
if admin_is_authed() and admin_is_2fa_authed():
if admin_is_authed() and admin_is_2fa_authed() and is_email_verified():
return f(*args, **kwargs)
else:
return {"message": "Authentication required"}, 401