Merge branch 'mfa-messaging-fixes' into 'develop'

MFA improvements

See merge request soapbox-pub/soapbox-fe!985
This commit is contained in:
Alex Gleason 2022-01-12 19:02:52 +00:00
commit 7e5fb63deb
9 changed files with 121 additions and 70 deletions

View File

@ -143,6 +143,7 @@ export function otpVerify(code, mfa_token) {
code: code, code: code,
challenge_type: 'totp', challenge_type: 'totp',
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
scope: getScopes(getState()),
}).then(({ data: token }) => dispatch(authLoggedIn(token))); }).then(({ data: token }) => dispatch(authLoggedIn(token)));
}; };
} }

View File

@ -60,10 +60,12 @@ export function confirmMfa(method, code, password) {
return (dispatch, getState) => { return (dispatch, getState) => {
const params = { code, password }; const params = { code, password };
dispatch({ type: MFA_CONFIRM_REQUEST, method, code }); dispatch({ type: MFA_CONFIRM_REQUEST, method, code });
return api(getState).post(`/api/pleroma/accounts/mfa/confirm/${method}`, params).then(() => { return api(getState).post(`/api/pleroma/accounts/mfa/confirm/${method}`, params).then(({ data }) => {
dispatch({ type: MFA_CONFIRM_SUCCESS, method, code }); dispatch({ type: MFA_CONFIRM_SUCCESS, method, code });
return data;
}).catch(error => { }).catch(error => {
dispatch({ type: MFA_CONFIRM_FAIL, method, code, error }); dispatch({ type: MFA_CONFIRM_FAIL, method, code, error, skipAlert: true });
throw error;
}); });
}; };
} }
@ -71,10 +73,12 @@ export function confirmMfa(method, code, password) {
export function disableMfa(method, password) { export function disableMfa(method, password) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ type: MFA_DISABLE_REQUEST, method }); dispatch({ type: MFA_DISABLE_REQUEST, method });
return api(getState).delete(`/api/pleroma/accounts/mfa/${method}`, { data: { password } }).then(response => { return api(getState).delete(`/api/pleroma/accounts/mfa/${method}`, { data: { password } }).then(({ data }) => {
dispatch({ type: MFA_DISABLE_SUCCESS, method }); dispatch({ type: MFA_DISABLE_SUCCESS, method });
return data;
}).catch(error => { }).catch(error => {
dispatch({ type: MFA_DISABLE_FAIL, method }); dispatch({ type: MFA_DISABLE_FAIL, method, skipAlert: true });
throw error;
}); });
}; };
} }

View File

@ -8,6 +8,7 @@ import Icon from './icon';
export default class Button extends React.PureComponent { export default class Button extends React.PureComponent {
static propTypes = { static propTypes = {
type: PropTypes.string,
text: PropTypes.node, text: PropTypes.node,
onClick: PropTypes.func, onClick: PropTypes.func,
to: PropTypes.string, to: PropTypes.string,
@ -54,6 +55,7 @@ export default class Button extends React.PureComponent {
const btn = ( const btn = (
<button <button
type={this.props.type}
className={className} className={className}
disabled={this.props.disabled} disabled={this.props.disabled}
onClick={this.handleClick} onClick={this.handleClick}

View File

@ -45,10 +45,7 @@ class OtpAuthForm extends ImmutablePureComponent {
this.setState({ shouldRedirect: true }); this.setState({ shouldRedirect: true });
return dispatch(switchAccount(account.id)); return dispatch(switchAccount(account.id));
}).catch(error => { }).catch(error => {
this.setState({ isLoading: false }); this.setState({ isLoading: false, code_error: true });
if (error.response.data.error === 'Invalid code') {
this.setState({ code_error: true });
}
}); });
this.setState({ isLoading: true }); this.setState({ isLoading: true });
event.preventDefault(); event.preventDefault();
@ -82,11 +79,11 @@ class OtpAuthForm extends ImmutablePureComponent {
</div> </div>
</div> </div>
</fieldset> </fieldset>
{ code_error && {code_error && (
<div className='error-box'> <div className='error-box'>
<FormattedMessage id='login.otp_log_in.fail' defaultMessage='Invalid code, please try again.' /> <FormattedMessage id='login.otp_log_in.fail' defaultMessage='Invalid code, please try again.' />
</div> </div>
} )}
<div className='actions'> <div className='actions'>
<button name='button' type='submit' className='btn button button-primary'> <button name='button' type='submit' className='btn button button-primary'>
<FormattedMessage id='login.log_in' defaultMessage='Log in' /> <FormattedMessage id='login.log_in' defaultMessage='Log in' />

View File

@ -47,6 +47,8 @@ const messages = defineMessages({
disableFail: { id: 'security.disable.fail', defaultMessage: 'Incorrect password. Try again.' }, disableFail: { id: 'security.disable.fail', defaultMessage: 'Incorrect password. Try again.' },
mfaDisableSuccess: { id: 'mfa.disable.success_message', defaultMessage: 'MFA disabled' }, mfaDisableSuccess: { id: 'mfa.disable.success_message', defaultMessage: 'MFA disabled' },
mfaConfirmSuccess: { id: 'mfa.confirm.success_message', defaultMessage: 'MFA confirmed' }, mfaConfirmSuccess: { id: 'mfa.confirm.success_message', defaultMessage: 'MFA confirmed' },
codePlaceholder: { id: 'mfa.mfa_setup.code_placeholder', defaultMessage: 'Code' },
passwordPlaceholder: { id: 'mfa.mfa_setup.password_placeholder', defaultMessage: 'Password' },
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
@ -118,44 +120,62 @@ class DisableOtpForm extends ImmutablePureComponent {
state = { state = {
password: '', password: '',
isLoading: false,
} }
handleInputChange = e => { handleInputChange = e => {
this.setState({ [e.target.name]: e.target.value }); this.setState({ [e.target.name]: e.target.value });
} }
handleOtpDisableClick = e => { handleSubmit = e => {
const { password } = this.state; const { password } = this.state;
const { dispatch, intl } = this.props; const { dispatch, intl } = this.props;
this.setState({ isLoading: true });
dispatch(disableMfa('totp', password)).then(() => { dispatch(disableMfa('totp', password)).then(() => {
dispatch(snackbar.success(intl.formatMessage(messages.mfaDisableSuccess))); dispatch(snackbar.success(intl.formatMessage(messages.mfaDisableSuccess)));
this.context.router.history.push('../auth/edit');
}).catch(error => { }).catch(error => {
dispatch(snackbar.error(intl.formatMessage(messages.disableFail))); dispatch(snackbar.error(intl.formatMessage(messages.disableFail)));
this.setState({ isLoading: false });
}); });
this.context.router.history.push('../auth/edit');
e.preventDefault(); e.preventDefault();
} }
render() { render() {
const { intl } = this.props; const { intl } = this.props;
const { isLoading, password } = this.state;
return ( return (
<SimpleForm>
<div className='security-settings-panel'> <div className='security-settings-panel'>
<SimpleForm onSubmit={this.handleSubmit} disabled={isLoading}>
<h1 className='security-settings-panel__setup-otp'> <h1 className='security-settings-panel__setup-otp'>
<FormattedMessage id='mfa.otp_enabled_title' defaultMessage='OTP Enabled' /> <FormattedMessage id='mfa.otp_enabled_title' defaultMessage='OTP Enabled' />
</h1> </h1>
<div><FormattedMessage id='mfa.otp_enabled_description' defaultMessage='You have enabled two-factor authentication via OTP.' /></div> <h2 className='security-settings-panel__setup-otp'>
<div><FormattedMessage id='mfa.mfa_disable_enter_password' defaultMessage='Enter your current password to disable two-factor auth:' /></div> <FormattedMessage id='mfa.otp_enabled_description' defaultMessage='You have enabled two-factor authentication via OTP.' />
</h2>
<ShowablePassword <ShowablePassword
label={intl.formatMessage(messages.passwordPlaceholder)}
placeholder={intl.formatMessage(messages.passwordPlaceholder)}
hint={<FormattedMessage id='mfa.mfa_disable_enter_password' defaultMessage='Enter your current password to disable two-factor auth.' />}
disabled={isLoading}
name='password' name='password'
onChange={this.handleInputChange} onChange={this.handleInputChange}
value={password}
required
/>
<div className='security-settings-panel__setup-otp__buttons'>
<Button
disabled={isLoading}
className='button button-primary disable'
text={intl.formatMessage(messages.mfa_setup_disable_button)}
/> />
<Button className='button button-primary disable' text={intl.formatMessage(messages.mfa_setup_disable_button)} onClick={this.handleOtpDisableClick} />
</div> </div>
</SimpleForm> </SimpleForm>
</div>
); );
} }
@ -191,6 +211,7 @@ class EnableOtpForm extends ImmutablePureComponent {
handleCancelClick = e => { handleCancelClick = e => {
this.context.router.history.push('../auth/edit'); this.context.router.history.push('../auth/edit');
e.preventDefault();
} }
render() { render() {
@ -198,8 +219,8 @@ class EnableOtpForm extends ImmutablePureComponent {
const { backupCodes, displayOtpForm } = this.state; const { backupCodes, displayOtpForm } = this.state;
return ( return (
<SimpleForm>
<div className='security-settings-panel'> <div className='security-settings-panel'>
<SimpleForm>
<h1 className='security-settings-panel__setup-otp'> <h1 className='security-settings-panel__setup-otp'>
<FormattedMessage id='mfa.setup_otp_title' defaultMessage='OTP Disabled' /> <FormattedMessage id='mfa.setup_otp_title' defaultMessage='OTP Disabled' />
</h1> </h1>
@ -233,8 +254,8 @@ class EnableOtpForm extends ImmutablePureComponent {
)} )}
</div> </div>
)} )}
</div>
</SimpleForm> </SimpleForm>
</div>
); );
} }
@ -255,7 +276,7 @@ class OtpConfirmForm extends ImmutablePureComponent {
state = { state = {
password: '', password: '',
done: false, isLoading: false,
code: '', code: '',
qrCodeURI: '', qrCodeURI: '',
confirm_key: '', confirm_key: '',
@ -275,66 +296,96 @@ class OtpConfirmForm extends ImmutablePureComponent {
this.setState({ [e.target.name]: e.target.value }); this.setState({ [e.target.name]: e.target.value });
} }
handleOtpConfirmClick = e => { handleCancelClick = e => {
const { code, password } = this.state; this.context.router.history.push('../auth/edit');
e.preventDefault();
}
handleSubmit = e => {
const { dispatch, intl } = this.props; const { dispatch, intl } = this.props;
const { code, password } = this.state;
this.setState({ isLoading: true });
dispatch(confirmMfa('totp', code, password)).then(() => { dispatch(confirmMfa('totp', code, password)).then(() => {
dispatch(snackbar.success(intl.formatMessage(messages.mfaConfirmSuccess))); dispatch(snackbar.success(intl.formatMessage(messages.mfaConfirmSuccess)));
this.context.router.history.push('../auth/edit');
}).catch(error => { }).catch(error => {
dispatch(snackbar.error(intl.formatMessage(messages.confirmFail))); dispatch(snackbar.error(intl.formatMessage(messages.confirmFail)));
this.setState({ isLoading: false });
}); });
this.context.router.history.push('../auth/edit');
e.preventDefault(); e.preventDefault();
} }
render() { render() {
const { intl } = this.props; const { intl } = this.props;
const { qrCodeURI, confirm_key } = this.state; const { isLoading, qrCodeURI, confirm_key, password, code } = this.state;
return ( return (
<SimpleForm>
<div className='security-settings-panel'> <div className='security-settings-panel'>
<SimpleForm onSubmit={this.handleSubmit} disabled={isLoading}>
<fieldset disabled={false}> <fieldset disabled={false}>
<FieldsGroup> <FieldsGroup>
<div className='security-settings-panel__section-container'> <div className='security-settings-panel__section-container'>
<h2><FormattedMessage id='mfa.mfa_setup_scan_title' defaultMessage='Scan' /></h2> <h2><FormattedMessage id='mfa.mfa_setup_scan_title' defaultMessage='Scan' /></h2>
<div><FormattedMessage id='mfa.mfa_setup_scan_description' defaultMessage='Using your two-factor app, scan this QR code or enter text key:' /></div> <div><FormattedMessage id='mfa.mfa_setup_scan_description' defaultMessage='Using your two-factor app, scan this QR code or enter the text key.' /></div>
<span className='security-settings-panel qr-code'> <div className='security-settings-panel__qr-code'>
<QRCode value={qrCodeURI} /> <QRCode value={qrCodeURI} />
</span> <div className='security-settings-panel__confirm-key'>
{confirm_key}
</div>
</div>
<div className='security-settings-panel confirm-key'><FormattedMessage id='mfa.mfa_setup_scan_key' defaultMessage='Key:' /> {confirm_key}</div>
</div> </div>
<div className='security-settings-panel__section-container'> <div className='security-settings-panel__section-container'>
<h2><FormattedMessage id='mfa.mfa_setup_verify_title' defaultMessage='Verify' /></h2> <h2><FormattedMessage id='mfa.mfa_setup_verify_title' defaultMessage='Verify' /></h2>
<div><FormattedMessage id='mfa.mfa_setup_verify_description' defaultMessage='To enable two-factor authentication, enter the code from your two-factor app:' /></div>
<TextInput <TextInput
name='code' name='code'
label={intl.formatMessage(messages.codePlaceholder)}
hint={<FormattedMessage id='mfa.mfa_setup.code_hint' defaultMessage='Enter the code from your two-factor app.' />}
placeholder={intl.formatMessage(messages.codePlaceholder)}
onChange={this.handleInputChange} onChange={this.handleInputChange}
autoComplete='off' autoComplete='off'
value={code}
disabled={isLoading}
required
/> />
<div><FormattedMessage id='mfa.mfa_setup_enter_password' defaultMessage='Enter your current password to confirm your identity:' /></div>
<ShowablePassword <ShowablePassword
name='password' name='password'
label={intl.formatMessage(messages.passwordPlaceholder)}
hint={<FormattedMessage id='mfa.mfa_setup.password_hint' defaultMessage='Enter your current password to confirm your identity.' />}
placeholder={intl.formatMessage(messages.passwordPlaceholder)}
onChange={this.handleInputChange} onChange={this.handleInputChange}
value={password}
disabled={isLoading}
required
/> />
</div> </div>
</FieldsGroup> </FieldsGroup>
</fieldset> </fieldset>
<div className='security-settings-panel__setup-otp__buttons'> <div className='security-settings-panel__setup-otp__buttons'>
<Button className='button button-secondary cancel' text={intl.formatMessage(messages.mfa_cancel_button)} onClick={this.handleCancelClick} /> <Button
<Button className='button button-primary setup' text={intl.formatMessage(messages.mfa_setup_confirm_button)} onClick={this.handleOtpConfirmClick} /> type='button'
</div> className='button button-secondary cancel'
text={intl.formatMessage(messages.mfa_cancel_button)}
onClick={this.handleCancelClick}
disabled={isLoading}
/>
<Button
type='submit'
className='button button-primary setup'
text={intl.formatMessage(messages.mfa_setup_confirm_button)}
disabled={isLoading}
/>
</div> </div>
</SimpleForm> </SimpleForm>
</div>
); );
} }

View File

@ -580,12 +580,12 @@
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Toggle visibility",
"media_panel.empty_message": "No media found.", "media_panel.empty_message": "No media found.",
"media_panel.title": "Media", "media_panel.title": "Media",
"mfa.mfa_disable_enter_password": "Enter your current password to disable two-factor auth:", "mfa.mfa_disable_enter_password": "Enter your current password to disable two-factor auth.",
"mfa.mfa_setup_enter_password": "Enter your current password to confirm your identity:", "mfa.mfa_setup_enter_password": "Enter your current password to confirm your identity",
"mfa.mfa_setup_scan_description": "Using your two-factor app, scan this QR code or enter text key:", "mfa.mfa_setup_scan_description": "Using your two-factor app, scan this QR code or enter the text key.",
"mfa.mfa_setup_scan_key": "Key:", "mfa.mfa_setup_scan_key": "Key:",
"mfa.mfa_setup_scan_title": "Scan", "mfa.mfa_setup_scan_title": "Scan",
"mfa.mfa_setup_verify_description": "To enable two-factor authentication, enter the code from your two-factor app:", "mfa.mfa_setup_verify_description": "To enable two-factor authentication, enter the code from your two-factor app",
"mfa.mfa_setup_verify_title": "Verify", "mfa.mfa_setup_verify_title": "Verify",
"mfa.otp_enabled_description": "You have enabled two-factor authentication via OTP.", "mfa.otp_enabled_description": "You have enabled two-factor authentication via OTP.",
"mfa.otp_enabled_title": "OTP Enabled", "mfa.otp_enabled_title": "OTP Enabled",

View File

@ -3,9 +3,7 @@ import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { import {
MFA_FETCH_SUCCESS, MFA_FETCH_SUCCESS,
MFA_CONFIRM_SUCCESS, MFA_CONFIRM_SUCCESS,
MFA_DISABLE_REQUEST,
MFA_DISABLE_SUCCESS, MFA_DISABLE_SUCCESS,
MFA_DISABLE_FAIL,
} from '../actions/mfa'; } from '../actions/mfa';
import { import {
FETCH_TOKENS_SUCCESS, FETCH_TOKENS_SUCCESS,
@ -49,11 +47,8 @@ export default function security(state = initialState, action) {
return importMfa(state, fromJS(action.data)); return importMfa(state, fromJS(action.data));
case MFA_CONFIRM_SUCCESS: case MFA_CONFIRM_SUCCESS:
return enableMfa(state, action.method); return enableMfa(state, action.method);
case MFA_DISABLE_REQUEST:
case MFA_DISABLE_SUCCESS: case MFA_DISABLE_SUCCESS:
return disableMfa(state, action.method); return disableMfa(state, action.method);
case MFA_DISABLE_FAIL:
return enableMfa(state, action.method);
default: default:
return state; return state;
} }

View File

@ -1,6 +1,10 @@
.security-settings-panel { .security-settings-panel {
margin: 20px; margin: 20px;
.simple_form {
padding: 0 !important;
}
h1.security-settings-panel__setup-otp { h1.security-settings-panel__setup-otp {
font-size: 20px; font-size: 20px;
line-height: 1.25; line-height: 1.25;
@ -26,7 +30,7 @@
} }
.backup_codes { .backup_codes {
margin: 20px; margin: 10px 0;
font-weight: bold; font-weight: bold;
padding: 15px 20px; padding: 15px 20px;
font-size: 14px; font-size: 14px;
@ -35,6 +39,9 @@
text-align: center; text-align: center;
position: relative; position: relative;
min-height: 125px; min-height: 125px;
display: flex;
justify-content: center;
align-items: center;
.backup_code { .backup_code {
margin: 5px auto; margin: 5px auto;
@ -42,22 +49,30 @@
} }
.security-settings-panel__setup-otp__buttons { .security-settings-panel__setup-otp__buttons {
margin: 20px; margin: 15px 0;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
.button { .button {
min-width: 182px; flex: 1;
} }
} }
div.confirm-key { &__qr-code {
margin: 20px 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
&__confirm-key {
display: block; display: block;
font-size: 16px; font-size: 14px;
line-height: 1.5; line-height: 1.5;
color: var(--primary-text-color--faint); color: var(--primary-text-color--faint);
font-weight: 400; font-weight: 400;
margin: 0 0 20px 20px; margin-top: 10px;
} }
} }

View File

@ -643,20 +643,6 @@ code {
font-size: 24px; font-size: 24px;
} }
.qr-code {
flex: 0 0 auto;
background: var(--foreground-color);
padding: 4px;
margin: 0 10px 20px 0;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
display: inline-block;
svg {
display: block;
margin: 0;
}
}
.simple_form { .simple_form {
.warning { .warning {
box-sizing: border-box; box-sizing: border-box;