Skip to content
This repository has been archived by the owner on Sep 20, 2023. It is now read-only.

Commit

Permalink
Implement 2FA reset for T3 (#4528)
Browse files Browse the repository at this point in the history
* 2fa reset implementation

* Layout tweaks

* Add tests

* Remove payload from submitOtpUpdateSuccess action

* Check otpKey not empty and fix typo

* Add discord webhook on 2fa reset

* Fix import
  • Loading branch information
Tom Linton authored Feb 11, 2021
1 parent 723484d commit 3981e54
Show file tree
Hide file tree
Showing 12 changed files with 7,513 additions and 7,225 deletions.
2 changes: 1 addition & 1 deletion infra/token-transfer-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"numeral": "2.0.6",
"optimize-css-assets-webpack-plugin": "5.0.3",
"prettier": "1.19.1",
"react": "16.13.1",
"react": "16.14.0",
"react-app-polyfill": "1.0.6",
"react-bootstrap": "1.0.0",
"react-chartjs-2": "2.9.0",
Expand Down
78 changes: 78 additions & 0 deletions infra/token-transfer-client/src/actions/otp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import agent from '@/utils/agent'
import { apiUrl } from '@/constants'

export const VERIFY_OTP_PENDING = 'VERIFY_OTP_PENDING'
export const VERIFY_OTP_SUCCESS = 'VERIFY_OTP_SUCCESS'
export const VERIFY_OTP_ERROR = 'VERIFY_OTP_ERROR'
export const SUBMIT_OTP_UPDATE_PENDING = 'SUBMIT_OTP_UPDATE_PENDING'
export const SUBMIT_OTP_UPDATE_SUCCESS = 'SUBMIT_OTP_UPDATE_SUCCESS'
export const SUBMIT_OTP_UPDATE_ERROR = 'SUBMIT_OTP_UPDATE_ERROR'

function verifyOtpPending() {
return {
type: VERIFY_OTP_PENDING
}
}

function verifyOtpSuccess(payload) {
return {
type: VERIFY_OTP_SUCCESS,
payload
}
}

function verifyOtpError(error) {
return {
type: VERIFY_OTP_ERROR,
error
}
}

function submitOtpUpdatePending() {
return {
type: SUBMIT_OTP_UPDATE_PENDING
}
}

function submitOtpUpdateSuccess() {
return {
type: SUBMIT_OTP_UPDATE_SUCCESS
}
}

function submitOtpUpdateError(error) {
return {
type: SUBMIT_OTP_UPDATE_ERROR,
error
}
}

export function verifyOtp(data) {
return dispatch => {
dispatch(verifyOtpPending())

return agent
.post(`${apiUrl}/api/user/otp`)
.send(data)
.then(response => dispatch(verifyOtpSuccess(response.body)))
.catch(error => {
dispatch(verifyOtpError(error))
throw error
})
}
}

export function submitOtpUpdate(data) {
return dispatch => {
dispatch(submitOtpUpdatePending())

return agent
.post(`${apiUrl}/api/user/otp`)
.send(data)
.then(response => dispatch(submitOtpUpdateSuccess()))
.catch(error => {
dispatch(submitOtpUpdateError(error))
throw error
})
}
}
300 changes: 300 additions & 0 deletions infra/token-transfer-client/src/components/OtpModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import ReactGA from 'react-ga'
import get from 'lodash.get'

import { formInput, formFeedback } from '@/utils/formHelpers'
import { verifyOtp, submitOtpUpdate } from '@/actions/otp'
import {
getVerifyError as getOtpVerifyError,
getIsVerifying as getOtpIsVerifying,
getUpdateError as getOtpUpdateError,
getIsUpdating as getOtpIsUpdating,
getOtp
} from '@/reducers/otp'
import Modal from '@/components/Modal'
import SuccessIcon from '@/assets/success-icon.svg'
import GoogleAuthenticatorIcon from '@/assets/google-authenticator.svg'

class OtpUpdateModal extends Component {
constructor(props) {
super(props)
this.state = this.getInitialState()
}

getInitialState = () => {
const initialState = {
oldCode: '',
oldCodeError: null,
newCode: '',
newCodeError: null,
modalState: 'OTP'
}
return initialState
}

componentDidMount() {
ReactGA.modalview(`/otpUpdate/${this.state.modalState.toLowerCase()}`)
}

componentDidUpdate(prevProps, prevState) {
if (get(prevProps, 'otpVerifyError') !== this.props.otpVerifyError) {
this.handleServerError(this.props.otpVerifyError)
}

if (get(prevProps, 'otpUpdateError') !== this.props.otpUpdateError) {
this.handleServerError(this.props.otpUpdateError)
}

if (prevState.modalState !== this.state.modalState) {
ReactGA.modalview(`/otpUpdate/${this.state.modalState.toLowerCase()}`)
}
}

handleServerError(error) {
if (error && error.status === 422) {
// Parse validation errors from API
if (error.response.body && error.response.body.errors) {
error.response.body.errors.forEach(e => {
this.setState({ [`${e.param}Error`]: e.msg })
})
} else {
console.error(error.response.body)
}
}
}

handleVerifySubmit = async event => {
event.preventDefault()

try {
await this.props.verifyOtp({ oldCode: this.state.oldCode })
} catch (error) {
// Error will be displayed in form, don't continue to two factor input
return
}

this.setState({
modalState: 'Form'
})
}

handleFormSubmit = async event => {
event.preventDefault()

try {
await this.props.submitOtpUpdate({
otpKey: this.props.otp.otpKey,
oldCode: this.state.oldCode,
newCode: this.state.newCode
})
} catch (error) {
// Error will be displayed in form, don't continue to two factor input
return
}

this.setState({
modalState: 'Thanks'
})
}

handleModalClose = () => {
// Reset the state of the modal back to defaults
this.setState(this.getInitialState())
if (this.props.onModalClose) {
this.props.onModalClose()
}
}

render() {
return (
<Modal appendToId="main" onClose={this.handleModalClose} closeBtn={true}>
{this.state.modalState === 'OTP' && this.renderOtp()}
{this.state.modalState === 'Form' && this.renderForm()}
{this.state.modalState === 'Thanks' && this.renderThanks()}
</Modal>
)
}

// First step, verify existing code
renderOtp() {
const input = formInput(this.state, state => this.setState(state))
const Feedback = formFeedback(this.state)

return (
<>
<div className="row align-items-center mb-3 text-center text-sm-left">
<div className="d-none d-sm-block col-sm-2">
<GoogleAuthenticatorIcon width="80%" height="80%" />
</div>
<div className="col">
<h1 className="my-2">Update 2FA</h1>
</div>
</div>

<hr />

<form onSubmit={this.handleVerifySubmit}>
<div className="col-12 col-sm-8 offset-sm-2">
<p>Enter the code generated by Google Authenticator.</p>
<div className="form-group">
<label htmlFor="oldCode">Code</label>
<div
className={`input-group ${
this.state.oldCodeError ? 'is-invalid' : ''
}`}
>
<input {...input('oldCode')} type="number" />
</div>
<div className={this.state.oldCodeError ? 'input-group-fix' : ''}>
{Feedback('oldCode')}
</div>
</div>
</div>
<div className="actions mt-5">
<div className="row">
<div className="col text-left d-none d-sm-block">
<button
className="btn btn-outline-primary btn-lg"
onClick={this.handleModalClose}
>
Cancel
</button>
</div>
<div className="col text-sm-right mb-3 mb-sm-0">
<button
type="submit"
className="btn btn-primary btn-lg"
disabled={this.props.otpIsUpdating}
>
{this.props.otpIsUpdating ? 'Loading...' : 'Continue'}
</button>
</div>
</div>
</div>
</form>
</>
)
}

// Second step, update 2FA app and verify new code
renderForm() {
const input = formInput(this.state, state => this.setState(state))
const Feedback = formFeedback(this.state)

return (
<>
<div className="row align-items-center mb-3 text-center text-sm-left">
<div className="d-none d-sm-block col-sm-2">
<GoogleAuthenticatorIcon width="80%" height="80%" />
</div>
<div className="col">
<h1 className="my-2">Verify 2FA</h1>
</div>
</div>

<hr />

<form onSubmit={this.handleFormSubmit}>
<div>
<p>
Scan the QR code or use the secret key and enter the code
generated by Google Authenticator to verify your new 2FA.
</p>
<img
src={get(this.props.otp, 'otpQrUrl')}
style={{ margin: '20px 0' }}
/>
</div>
<p>
<strong>Secret Key</strong>
<br />
{get(this.props.otp, 'encodedKey')}{' '}
</p>
<div className="col-12 col-sm-8 offset-sm-2">
<div className="form-group">
<label htmlFor="newCode">Code</label>
<div
className={`input-group ${
this.state.newCodeError ? 'is-invalid' : ''
}`}
>
<input {...input('newCode')} type="number" />
</div>
<div className={this.state.newCodeError ? 'input-group-fix' : ''}>
{Feedback('newCode')}
</div>
</div>
</div>
<div className="actions mt-5">
<div className="row">
<div className="col text-left d-none d-sm-block">
<button
className="btn btn-outline-primary btn-lg"
onClick={this.handleModalClose}
>
Cancel
</button>
</div>
<div className="col text-sm-right mb-3 mb-sm-0">
<button
type="submit"
className="btn btn-primary btn-lg"
disabled={this.props.otpIsUpdating}
>
{this.props.otpIsUpdating ? 'Loading...' : 'Continue'}
</button>
</div>
</div>
</div>
</form>
</>
)
}

renderThanks() {
return (
<>
<div className="my-3">
<SuccessIcon style={{ marginRight: '-46px' }} />
</div>
<h1 className="mb-2">Success</h1>
<p>Your settings have been updated.</p>
<div className="actions mt-5">
<div className="row">
<div className="col text-right">
<button
className="btn btn-primary btn-lg"
onClick={this.props.onModalClose}
>
Done
</button>
</div>
</div>
</div>
</>
)
}
}

const mapStateToProps = ({ otp }) => {
return {
otpVerifyError: getOtpVerifyError(otp),
otpIsVerifying: getOtpIsVerifying(otp),
otpUpdateError: getOtpUpdateError(otp),
otpIsUpdating: getOtpIsUpdating(otp),
otp: getOtp(otp)
}
}

const mapDispatchToProps = dispatch =>
bindActionCreators(
{
verifyOtp,
submitOtpUpdate
},
dispatch
)

export default connect(mapStateToProps, mapDispatchToProps)(OtpUpdateModal)
Loading

0 comments on commit 3981e54

Please sign in to comment.