Last active
January 4, 2021 01:45
-
-
Save kirasiris/4326bf85d06b78c5e1de9f93c6e05d2a to your computer and use it in GitHub Desktop.
TFAuthenticationWithNodeJSandMongoDB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// | |
/// | |
/// AS THE FILE TITLE SUGGEST, THIS IS WHERE YOU SEND YOUR USERNAME AND/OR PASSWORD DATA | |
/// | |
/// | |
import React, { useEffect, useState } from 'react'; | |
import { Link, withRouter } from 'react-router-dom'; | |
import { connect } from 'react-redux'; | |
import PropTypes from 'prop-types'; | |
import Helmet from 'react-helmet'; | |
// ACTIONS | |
import { login } from '../../actions/auth'; | |
// HELPERS | |
import { URL } from '../../config'; | |
import Alert from '../layout/Alert'; | |
// REACTBOOTSTRAP | |
import Container from 'react-bootstrap/Container'; | |
import Row from 'react-bootstrap/Row'; | |
import Col from 'react-bootstrap/Col'; | |
import Form from 'react-bootstrap/Form'; | |
import InputGroup from 'react-bootstrap/InputGroup'; | |
import Button from 'react-bootstrap/Button'; | |
const Login = ({ login, history }) => { | |
const [loginData, setLoginData] = useState({ | |
email: '', | |
password: '', | |
rememberMe: false | |
}); | |
const { email, password, rememberMe } = loginData; | |
// Remember me | |
useEffect(() => { | |
const email = localStorage.getItem('email'); | |
const password = localStorage.getItem('password'); | |
email && | |
password && | |
setLoginData((loginData) => ({ | |
...loginData, | |
email, | |
password, | |
rememberMe: true | |
})); | |
}, []); | |
const handleChange = (name) => (e) => { | |
setLoginData({ ...loginData, [name]: e.target.value }); | |
}; | |
const loginAccount = async (e) => { | |
e.preventDefault(); | |
if (rememberMe) { | |
localStorage.setItem('email', email); | |
localStorage.setItem('password', password); | |
} else { | |
localStorage.removeItem('email'); | |
localStorage.removeItem('password'); | |
} | |
login(loginData, history); | |
}; | |
const [passwordShown, setPasswordShown] = useState(false); | |
const handlePasswordVisibility = () => { | |
setPasswordShown(passwordShown ? false : true); | |
}; | |
const resetForm = () => { | |
setLoginData({ | |
email: '', | |
password: '' | |
}); | |
}; | |
return ( | |
<> | |
<Helmet> | |
<title>beFree | Login</title> | |
<meta | |
name="description" | |
content="Access to the best open minded social network" | |
></meta> | |
<meta property="og:title" content="beFree | Login" /> | |
<meta | |
property="og:description" | |
content="Access to the best open minded social network" | |
/> | |
<meta property="og:url" content={`${URL}/login`} /> | |
<meta property="og:site_name" content="beFree | Login" /> | |
</Helmet> | |
<section> | |
<Container> | |
<Row> | |
<Col | |
xl={`6`} | |
lg={`6`} | |
md={`12`} | |
sm={`12`} | |
xs={`12`} | |
className="ml-auto mr-auto mt-5 mb-5" | |
> | |
<Link to={`/`} className="btn btn-dark btn-sm float-left"> | |
Go back home | |
</Link> | |
<div className="clearfix"></div> | |
<h1 className="text-center">Log In</h1> | |
<Alert /> | |
<div className="pb-5 pt-5 form-wrapper"> | |
<Form className="form form-signing p-5" onSubmit={loginAccount}> | |
{/* Email */} | |
<Form.Group> | |
<Form.Label htmlFor="email">Email</Form.Label> | |
<InputGroup> | |
<InputGroup.Prepend> | |
<InputGroup.Text id="email-text"> | |
<i className="fas fa-envelope" /> | |
</InputGroup.Text> | |
</InputGroup.Prepend> | |
<Form.Control | |
type="email" | |
placeholder="Email Address" | |
aria-label="email" | |
aria-describedby="email-text" | |
autoComplete="email" | |
name="email" | |
id="email" | |
minLength="6" | |
required | |
onChange={handleChange('email')} | |
value={email} | |
/> | |
</InputGroup> | |
</Form.Group> | |
{/* Password */} | |
<Form.Group> | |
<Form.Label htmlFor="password">Password</Form.Label> | |
<InputGroup> | |
<InputGroup.Prepend> | |
<InputGroup.Text id="password-text"> | |
<i className="fas fa-lock" /> | |
</InputGroup.Text> | |
</InputGroup.Prepend> | |
<Form.Control | |
type={passwordShown ? 'text' : 'password'} | |
placeholder="Password" | |
aria-label="Password" | |
aria-describedby="password-text" | |
autoComplete="password" | |
name="password" | |
id="password" | |
minLength="6" | |
required | |
onChange={handleChange('password')} | |
value={password} | |
/> | |
</InputGroup> | |
</Form.Group> | |
{/* View Password */} | |
<Form.Group | |
controlId="rememberAccount" | |
className={`float-left`} | |
name={`rememberMe`} | |
onChange={handleChange('rememberMe')} | |
checked={rememberMe} | |
readOnly | |
> | |
<Form.Check type="checkbox" label="Remember" /> | |
</Form.Group> | |
{/* View Password */} | |
<Form.Group | |
controlId="viewPassword" | |
className={`float-right`} | |
> | |
<Form.Check | |
type="checkbox" | |
label="View Password" | |
onClick={handlePasswordVisibility} | |
/> | |
</Form.Group> | |
<div className={`clearfix`}></div> | |
{/* Buttons */} | |
<Button | |
variant={`dark`} | |
className={`float-left`} | |
type={`submit`} | |
size={`sm`} | |
disabled={ | |
email.length > 0 && password.length > 0 ? !true : !false | |
} | |
> | |
Login | |
</Button> | |
<Button | |
variant={`secondary`} | |
className={`float-right`} | |
type={`reset`} | |
size={`sm`} | |
onClick={resetForm} | |
> | |
Reset | |
</Button> | |
</Form> | |
</div> | |
<div className="links"> | |
<Link to="/auth/register">Register</Link> |{' '} | |
<Link to="/auth/recover">Forgot Password</Link> | |
</div> | |
</Col> | |
</Row> | |
</Container> | |
</section> | |
</> | |
); | |
}; | |
Login.propTypes = { | |
login: PropTypes.func.isRequired | |
}; | |
export default connect(null, { login })(withRouter(Login)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @desc Get current loggedIn user | |
// @route POST /api/v1/auth/me | |
// @access Private | |
// @status DONE | |
exports.getMe = asyncHandler(async (req, res, next) => { | |
const user = await User.findById(req.user.id) | |
res.status(200).json({ success: true, data: user }) | |
}) | |
// @desc Login user | |
// @route POST /api/v1/auth/login | |
// @access Public | |
// @status DONE | |
exports.login = asyncHandler(async (req, res, next) => { | |
const { email, password } = req.body | |
// Validate email & password | |
if (!email || !password) { | |
return next(new ErrorResponse('Please provide an email and password', 400)) | |
} | |
// Check for user to login and to register activity | |
const user = await User.findOne({ email }).select([ | |
'username', | |
'password', | |
'isEmailConfirmed', | |
'twoFactorTokenEnabled', | |
]) | |
if (!user) { | |
return next(new ErrorResponse('Invalid credentials', 401)) | |
} | |
// Check if user email is active | |
if (!user.isEmailConfirmed) { | |
return next(new ErrorResponse('Not authorized to access this route', 401)) | |
} | |
// Check if password matches | |
const isMatch = await user.matchPassword(password) | |
if (!isMatch) { | |
return next(new ErrorResponse('Invalid credentials', 401)) | |
} | |
// Create activity | |
await Activity.create({ | |
resourceId: user._id, | |
type: `user`, | |
action: `logged_in`, | |
user: user._id, | |
message: `has Logged In`, | |
onModel: `User`, | |
}) | |
// Verify if user has enabled his 2FA authentication method, otherwise let him log in as | |
// usual without requiring a Google Authenticar Token | |
user.twoFactorTokenEnabled | |
? res.status(200).json({ success: true, data: user }) | |
: sendTokenResponse(user, 200, req, res) | |
}) | |
// @desc Enable 2FA | |
// @route PUT /api/v1/auth/2fa/enable | |
// @access Private | |
// @status DONE | |
exports.enableFA = asyncHandler(async (req, res, next) => { | |
// Generate token | |
const temp_secret = speakeasy.generateSecret() | |
const user = await User.findByIdAndUpdate( | |
{ | |
_id: req.user._id, | |
}, | |
{ | |
$set: { | |
twoFactorTokenEnabled: true, | |
twoFactorToken: { | |
ascii: temp_secret.ascii, | |
hex: temp_secret.hex, | |
base32: temp_secret.base32, | |
otpauth_url: temp_secret.otpauth_url, | |
}, | |
}, | |
}, | |
{ | |
new: true, | |
runValidators: true, | |
setDefaultsOnInsert: true, | |
} | |
) | |
// Create URL | |
const url = `${req.get('origin')}/two-fa-verify/${req.user._id}` | |
// Send email | |
await new Email(req.user, url).sendTFAEnabled() | |
// Create activity | |
await Activity.create({ | |
resourceId: req.user._id, | |
type: `user`, | |
action: `enabled_2FA`, | |
user: req.user._id, | |
message: `has enabled 2FA`, | |
onModel: `User`, | |
}) | |
res.status(201).json({ success: true, data: user }) | |
}) | |
// @desc Disable 2FA | |
// @route PUT /api/v1/auth/2fa/disable | |
// @access Private | |
// @status DONE | |
exports.disableFA = asyncHandler(async (req, res, next) => { | |
const user = await User.findByIdAndUpdate( | |
{ | |
_id: req.user._id, | |
}, | |
{ | |
$set: { | |
twoFactorTokenEnabled: false, | |
twoFactorToken: { | |
ascii: null, | |
hex: null, | |
base32: null, | |
otpauth_url: null, | |
}, | |
twoFactorRecoveryToken: null, | |
}, | |
}, | |
{ | |
new: true, | |
runValidators: true, | |
setDefaultsOnInsert: true, | |
} | |
) | |
// Create URL | |
const url = `${req.get('origin')}/edit-two-fa` | |
// Send email | |
await new Email(req.user, url).sendTFADisabled() | |
// Create activity | |
await Activity.create({ | |
resourceId: req.user._id, | |
type: `user`, | |
action: `disabled_2FA`, | |
user: req.user._id, | |
message: `has disabled 2FA`, | |
onModel: `User`, | |
}) | |
res.status(201).json({ | |
success: true, | |
data: user, | |
}) | |
}) | |
// @desc Verify 2FA | |
// @route PUT /api/v1/auth/2fa/verify/:id | |
// @access Private | |
// @status DONE | |
exports.verifyFA = asyncHandler(async (req, res, next) => { | |
const { token } = req.body | |
// Validate email & password | |
if (!req.params.id || !token) { | |
return next(new ErrorResponse('Please provide an userId and token', 400)) | |
} | |
// Get user twoFactorToken | |
const user = await User.findById(req.params.id) | |
// User not found | |
if (!user) { | |
return next(new ErrorResponse('User not found', 404)) | |
} | |
// If user has no twoFactorToken enabled...then..: | |
if (!user.twoFactorTokenEnabled) { | |
return next(new ErrorResponse('User has not requested a 2FA code')) | |
} | |
// Verify | |
const verified = speakeasy.totp.verify({ | |
secret: user.twoFactorToken.base32, | |
encoding: 'base32', | |
token, | |
}) | |
if (!verified) { | |
return next(new ErrorResponse('Token do not match', 400)) | |
} | |
// Set recovery token and save it | |
user.twoFactorRecoveryToken = `${user._id}-${user.username}-${user.twoFactorToken.base32}` | |
user.save() | |
// Create URL | |
const url = `${req.get('origin')}` | |
// Send email | |
await new Email(user, url).sendTFAVerifyToken() | |
// Create activity | |
await Activity.create({ | |
resourceId: req.params._id, | |
type: `user`, | |
action: `verified_2FA`, | |
user: req.params._id, | |
message: `has verified 2FA`, | |
onModel: `User`, | |
}) | |
res.status(201).json({ success: true, data: user.twoFactorRecoveryToken }) | |
}) | |
// @desc Validate 2FA | |
// @route PUT /api/v1/auth/2fa/validate/:id | |
// @access Private | |
// @status DONE | |
exports.validateFA = asyncHandler(async (req, res, next) => { | |
const { token } = req.body | |
// Validate email & password | |
if (!req.params.id || !token) { | |
return next(new ErrorResponse('Please provide a token', 400)) | |
} | |
// Get user twoFactorToken | |
const user = await User.findById(req.params.id) | |
if (!user) { | |
return next(new ErrorResponse('User not found', 404)) | |
} | |
// Check if user email is active | |
if (!user.isEmailConfirmed) { | |
return next(new ErrorResponse('Not authorized to access this route', 401)) | |
} | |
// Check if 2FA is enabled | |
if (!user.twoFactorTokenEnabled) { | |
return next(new ErrorResponse('User has not authorized to use 2FA', 401)) | |
} | |
// Verify | |
const tokenValidates = speakeasy.totp.verify({ | |
secret: user.twoFactorToken.base32, | |
encoding: 'base32', | |
token, | |
window: 0, | |
}) | |
if (!tokenValidates) { | |
return next(new ErrorResponse('Invalid 2FA Token', 500)) | |
} | |
sendTokenResponse(user, 200, req, res) | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
router.route('/2fa/enable').put(protect, enableFA); | |
router.route('/2fa/disable').put(protect, disableFA); | |
router.route('/2fa/verify/:id').put(verifyFA); | |
router.route('/2fa/validate/:id').post(validateFA); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// | |
/// HEADS UP. I USUALLY DO HOOK, TO CONVERT THEM TO A BASE CLASS COMPONENT INSTEAD OF USING USEEFFECT, USESTATE,ETC | |
// YOU WILL NEED THE LIFECYCLE METHODS | |
// I'M ALSO USING REACT-BOOTSTRAP | |
// | |
// THIS IS THE COMPONENT IN WHICH YOU CAN TOGGLE ON AND OFF YOUR TFA FUNCTIONALITY | |
/// | |
/// | |
import React, { useEffect, useState } from 'react'; | |
import { withRouter } from 'react-router-dom'; | |
import PropTypes from 'prop-types'; | |
import { connect } from 'react-redux'; | |
// ACTIONS | |
import { | |
getCurrentProfile, | |
enableTwoFactorAuth, | |
disableTwoFactorAuth | |
} from '../../actions/auth'; | |
// HELPERS | |
import Alert from '../layout/Alert'; | |
import Layout from '../layout/Layout'; | |
import { URL } from '../../config'; | |
import Spinner from '../layout/Spinner'; | |
import LeftSidebar from './LeftSidebar'; | |
import NotFound from '../layout/NotFound'; | |
import Content from '../layout/Container'; | |
import Sidebar from '../layout/Sidebar'; | |
import UseImage from '../../helpers/UseImage'; | |
// REACTBOOTSTRAP | |
import Row from 'react-bootstrap/Row'; | |
import Button from 'react-bootstrap/Button'; | |
import Card from 'react-bootstrap/Card'; | |
// QR CODE | |
import QRCode from 'qrcode'; | |
const EditTwoFactorAuthentication = ({ | |
getCurrentProfile, | |
enableTwoFactorAuth, | |
disableTwoFactorAuth, | |
profile: { loading } | |
}) => { | |
const [twoFactorAuthData, setTwoFactorAuthData] = useState({ | |
base32: null, | |
otpauth_url: null | |
}); | |
const { base32, otpauth_url } = twoFactorAuthData; | |
const [profile, setProfile] = useState(null); | |
const [enabled, setEnabled] = useState(false); | |
const [enabledStr, setEnabledStr] = useState(`Enable`); | |
const [recoveryToken, setRecoveryToken] = useState(null); | |
const [qrCodeImage, setQRCodeImage] = useState(null); | |
const [error, setError] = useState(false); | |
const QRImage = (url) => { | |
QRCode.toDataURL(url) | |
.then((url) => { | |
setQRCodeImage(url); | |
}) | |
.catch((err) => { | |
setError(true); | |
}); | |
}; | |
useEffect(() => { | |
getCurrentProfile() | |
.then((result) => { | |
setProfile(result.payload.data); | |
setEnabled(result.payload.data.twoFactorTokenEnabled); | |
setEnabledStr( | |
result.payload.data.twoFactorTokenEnabled ? `Enabled` : `Disabled` | |
); | |
setRecoveryToken(result.payload.data.twoFactorRecoveryToken); | |
setTwoFactorAuthData({ | |
base32: result.payload.data.twoFactorToken.base32, | |
otpauth_url: result.payload.data.twoFactorToken.otpauth_url | |
}); | |
QRImage(result.payload.data.twoFactorToken.otpauth_url); | |
}) | |
.catch((err) => { | |
setProfile(null); | |
setEnabled(false); | |
setEnabledStr(`Enabled`); | |
setRecoveryToken(null); | |
setTwoFactorAuthData({ | |
base32: null, | |
otpauth_url: null | |
}); | |
setError(true); | |
}); | |
}, [getCurrentProfile, loading]); | |
const activate = async (e) => { | |
enableTwoFactorAuth() | |
.then((result) => { | |
setEnabled(result.payload.data.twoFactorTokenEnabled); | |
setEnabledStr(`Enabled`); | |
setRecoveryToken(result.payload.data.twoFactorRecoveryToken); | |
setTwoFactorAuthData({ | |
base32: result.payload.data.twoFactorToken.base32, | |
otpauth_url: result.payload.data.twoFactorToken.otpauth_url | |
}); | |
QRImage(result.payload.data.twoFactorToken.otpauth_url); | |
}) | |
.catch((err) => { | |
setEnabled(false); | |
setEnabledStr(`Disabled`); | |
setRecoveryToken(null); | |
setTwoFactorAuthData({ | |
base32: null, | |
otpauth_url: null | |
}); | |
setError(true); | |
}); | |
}; | |
const disactivate = async (e) => { | |
disableTwoFactorAuth() | |
.then((result) => { | |
setEnabled(result.payload.data.twoFactorTokenEnabled); | |
setEnabledStr(`Disabled`); | |
setRecoveryToken(null); | |
setTwoFactorAuthData({ | |
base32: null, | |
otpauth_url: null | |
}); | |
}) | |
.catch((err) => { | |
setEnabled(true); | |
setEnabledStr(`Enabled`); | |
setRecoveryToken(profile?.twoFactorRecoveryToken); | |
setTwoFactorAuthData({ | |
base32: profile?.twoFactorToken?.base32, | |
otpauth_url: profile?.twoFactorToken?.otpauth_url | |
}); | |
setError(true); | |
}); | |
}; | |
return ( | |
<Layout | |
title={`beFree | Update 2FA Settings`} | |
description={`Update your profile`} | |
author={`Kevin Fonseca`} | |
sectionClass={`mb-3`} | |
containerClass={`container`} | |
canonical={`${URL}`} | |
url={`edit-two-fa`} | |
posType={`page`} | |
> | |
{loading || profile === null || profile === undefined ? ( | |
error ? ( | |
<NotFound /> | |
) : ( | |
<Spinner /> | |
) | |
) : ( | |
<Row> | |
<Sidebar> | |
<LeftSidebar /> | |
</Sidebar> | |
<Content> | |
<Alert /> | |
<Card> | |
<Card.Header> | |
<Card.Title>Update 2FA</Card.Title> | |
</Card.Header> | |
<Card.Body> | |
<h6>Instructions</h6> | |
<ol className={`ml-3`}> | |
<li>On your phone, go to the app store</li> | |
<li> | |
Search for <i>Google Authenticator</i> | |
</li> | |
<li> | |
Download and Install the{' '} | |
<a | |
href={`https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_US&gl=US`} | |
target={`_blank`} | |
rel={`noopener noreferrer`} | |
> | |
Google Authenticator App | |
</a> | |
</li> | |
</ol> | |
<h6>Open and Configure the Google Authenticator App</h6> | |
<ol className={`ml-3`}> | |
<li> | |
In Google Authenticator, touch the{' '} | |
<i className={`fas fa-plus mr-1`} /> | |
and select any of the following options: | |
<ul className={`ml-4`}> | |
<li> | |
<i className={`fas fa-camera mr-1`} /> | |
Scan a QR Code | |
</li> | |
<li> | |
<i className={`fas fa-keyboard mr-1`} /> | |
Enter a setup key | |
</li> | |
</ul> | |
</li> | |
</ol> | |
{enabled && ( | |
<> | |
<hr /> | |
<UseImage | |
src={`${qrCodeImage}`} | |
alt={`QRCODE`} | |
idGiven={`QRCODE`} | |
/> | |
<hr /> | |
{recoveryToken && ( | |
<> | |
<p> | |
Please keep this code in a safe but accessible area. | |
This is your <b>BACKUP</b> code: | |
<br /> | |
<code>{recoveryToken}</code> | |
<br /> | |
You can now close this window | |
</p> | |
</> | |
)} | |
<p> | |
Copy this to <i>Enter a setup key</i> to your | |
authenticator app: | |
<br /> | |
<code>{base32}</code> | |
</p> | |
<p> | |
This is the string used for the <i>QR Code</i>. | |
<b>You can ignore it!. </b> | |
<br /> | |
<code>{otpauth_url}</code> | |
</p> | |
</> | |
)} | |
<Button | |
onClick={!enabled ? activate : disactivate} | |
variant={!enabled ? `secondary` : `success`} | |
size={`sm`} | |
type={`button`} | |
> | |
{enabledStr} | |
</Button> | |
</Card.Body> | |
</Card> | |
</Content> | |
</Row> | |
)} | |
</Layout> | |
); | |
}; | |
EditTwoFactorAuthentication.propTypes = { | |
getCurrentProfile: PropTypes.func.isRequired, | |
enableTwoFactorAuth: PropTypes.func.isRequired, | |
disableTwoFactorAuth: PropTypes.func.isRequired, | |
profile: PropTypes.object.isRequired | |
}; | |
const mapStateToProps = (state) => ({ | |
profile: state.profile | |
}); | |
export default connect(mapStateToProps, { | |
getCurrentProfile, | |
enableTwoFactorAuth, | |
disableTwoFactorAuth | |
})(withRouter(EditTwoFactorAuthentication)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// | |
/// | |
/// I'M USING AXIOS BUT YOU'RE FREE TO USE THE FETCH API or ANY OTHER HTTP REQUESTS API | |
/// | |
/// | |
// @route PUT api/v1/auth/2fa/enable | |
// @description Enable 2FA | |
// @access Private | |
// @task DONE | |
export const enableTwoFactorAuth = () => async (dispatch) => { | |
try { | |
const res = await api.put(`/auth/2fa/enable`); | |
dispatch( | |
setAlert( | |
'2FA has been enabled. Please check your email to verify', | |
'success' | |
) | |
); | |
// RETURN BECAUSE WE NEED THE DATA TO HANDLE THE STATES | |
return dispatch({ | |
type: GET_PROFILE, | |
payload: res.data | |
}); | |
} catch (err) { | |
const error = err.response.data.message; | |
const errors = err?.response?.data?.errors; | |
if (error) { | |
dispatch(setAlert(error, 'danger')); | |
} | |
if (errors) { | |
errors.forEach((error) => dispatch(setAlert(error.msg, 'danger'))); | |
} | |
dispatch({ | |
type: PROFILE_ERROR, | |
payload: { msg: err.response?.statusText, status: err.response?.status } | |
}); | |
} | |
}; | |
// @route PUT api/v1/auth/2fa/disable | |
// @description Disable 2FA | |
// @access Private | |
// @task DONE | |
export const disableTwoFactorAuth = () => async (dispatch) => { | |
try { | |
const res = await api.put(`/auth/2fa/disable`); | |
dispatch(setAlert('Profile Updated. 2FA has been disabled.', 'success')); | |
// RETURN BECAUSE WE NEED THE DATA TO HANDLE THE STATES | |
return dispatch({ | |
type: GET_PROFILE, | |
payload: res.data | |
}); | |
} catch (err) { | |
const error = err.response.data.message; | |
const errors = err?.response?.data?.errors; | |
if (error) { | |
dispatch(setAlert(error, 'danger')); | |
} | |
if (errors) { | |
errors.forEach((error) => dispatch(setAlert(error.msg, 'danger'))); | |
} | |
dispatch({ | |
type: PROFILE_ERROR, | |
payload: { msg: err.response?.statusText, status: err.response?.status } | |
}); | |
} | |
}; | |
// @route PUT api/v1/auth/2fa/verify/:id | |
// @description Verify 2FA | |
// @access Private | |
// @task DONE | |
export const verifyTwoFactorAuthentication = (id, formData) => async ( | |
dispatch | |
) => { | |
try { | |
const res = await api.put(`/auth/2fa/verify/${id}`, formData); | |
dispatch( | |
setAlert( | |
'Profile Updated. You will also receive an email containing your recovery token.', | |
'success' | |
) | |
); | |
// RETURN BECAUSE WE NEED THE DATA TO HANDLE THE STATES. THE RECOVERY TOKEN WILL BE SHOWN AFTER SUCCESS | |
return dispatch({ | |
type: GET_PROFILE, | |
payload: res.data | |
}); | |
} catch (err) { | |
const error = err.response.data.message; | |
const errors = err?.response?.data?.errors; | |
if (error) { | |
dispatch(setAlert(error, 'danger')); | |
} | |
if (errors) { | |
errors.forEach((error) => dispatch(setAlert(error.msg, 'danger'))); | |
} | |
dispatch({ | |
type: PROFILE_ERROR, | |
payload: { msg: err.response?.statusText, status: err.response?.status } | |
}); | |
} | |
}; | |
// @route PUT api/v1/auth/2fa/validate | |
// @description Validate 2FA | |
// @access Private | |
// @task DONE | |
export const validateFactorAuth = (id, formData, history) => async ( | |
dispatch | |
) => { | |
try { | |
const res = await api.post(`/auth/2fa/validate/${id}`, formData); | |
// FUCK IT AND JUST LET THE USER LOG IN AFTER TOKEN IS FOUND TO BE VALID | |
dispatch({ | |
type: LOGIN_SUCCESS, | |
payload: res.data | |
}); | |
dispatch(loadUser()); | |
history.push('/timeline'); | |
} catch (err) { | |
const error = err.response.data.message; | |
const errors = err?.response?.data?.errors; | |
if (error) { | |
dispatch(setAlert(error, 'danger')); | |
} | |
if (errors) { | |
errors.forEach((error) => dispatch(setAlert(error.msg, 'danger'))); | |
} | |
dispatch({ | |
type: PROFILE_ERROR, | |
payload: { msg: err.response?.statusText, status: err.response?.status } | |
}); | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{/* REMEMBER THESE TWO ARE PUBLIC ROUTES AND DO NOT REQUIRED AUTHENTICATION */} | |
<Route exact path="/two-fa-verify/:id" component={VerifyTwoFactorAuth} /> | |
<Route exact path="/two-fa-validate/:id" component={ValidateTwoFactorAuth} /> | |
{/* REMEMBER THIS IS THE ONLY ROUTE WHICH REQUIRES USER AUTHENTICATION, HWO YOU HANDLE THE AUTHENTICATION IS UP TO YOU */} | |
<PrivateRoute exact path="/edit-two-fa" component={EditTwoFactorAuthentication} /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
twoFactorTokenEnabled: { | |
type: Boolean, | |
required: false, | |
default: false | |
}, | |
twoFactorToken: { | |
ascii: { | |
type: String, | |
required: false, | |
default: null | |
}, | |
hex: { | |
type: String, | |
required: false, | |
default: null | |
}, | |
base32: { | |
type: String, | |
required: false, | |
default: null | |
}, | |
otpauth_url: { | |
type: String, | |
required: false, | |
default: null | |
} | |
}, | |
twoFactorRecoveryToken: { | |
type: String, | |
required: false, | |
default: null | |
}, |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// | |
/// | |
/// THIS COMPONENT WILL BE RETURNED DEPENDING ON THE LOGIN DATA SENT | |
/// IF USER IS FOUND WITH 2FA DISABLED, THIS WILL NOT BE SHOWN. | |
/// | |
/// YES, THIS IS THE COMPONENT IN WHICH YOU WILL TYPE YOUR GOOGLE AUTHENTICATOR TOKEN. ONCE THE TOKEN IS FOUND TO BE VALID YOU BE | |
/// REDIRECTED TO YOUR APP | |
/// | |
/// | |
import React, { useState } from 'react'; | |
import { withRouter } from 'react-router-dom'; | |
import PropTypes from 'prop-types'; | |
import { connect } from 'react-redux'; | |
// ACTIONS | |
import { validateFactorAuth } from '../../actions/auth'; | |
// HELPERS | |
import Alert from '../layout/Alert'; | |
import Layout from '../layout/Layout'; | |
import { URL } from '../../config'; | |
import Content from '../layout/Container'; | |
// REACTBOOTSTRAP | |
import Row from 'react-bootstrap/Row'; | |
import Card from 'react-bootstrap/Card'; | |
import Form from 'react-bootstrap/Form'; | |
import InputGroup from 'react-bootstrap/InputGroup'; | |
import ButtonGroup from 'react-bootstrap/ButtonGroup'; | |
import Button from 'react-bootstrap/Button'; | |
const ValidateTwoFactorAuth = ({ validateFactorAuth, match, history }) => { | |
const userId = match.params.id; | |
const [tokenData, setTokenData] = useState({ | |
token: `` | |
}); | |
const { token } = tokenData; | |
const [validated, setValidated] = useState(false); | |
const handleChange = (name) => (e) => { | |
setTokenData({ ...tokenData, [name]: e.target.value }); | |
}; | |
const validateTFA = async (e) => { | |
const form = e.currentTarget; | |
if (form.checkValidity() === false) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
} | |
e.preventDefault(); | |
setValidated(true); | |
validateFactorAuth(userId, tokenData, history); | |
}; | |
return ( | |
<Layout | |
title={`beFree | Validate 2FA`} | |
description={`Validate your 2FA to access your account`} | |
author={`Kevin Fonseca`} | |
sectionClass={`mb-3`} | |
containerClass={`container`} | |
canonical={`${URL}`} | |
url={`two-fa-validate`} | |
posType={`page`} | |
> | |
<Row> | |
<Content fullWidth> | |
<Alert /> | |
<Card> | |
<Card.Header> | |
<Card.Title> | |
Please enter the 2FA token given to you by your Authenticator | |
app | |
</Card.Title> | |
</Card.Header> | |
<Card.Body> | |
<Form | |
className={`form`} | |
noValidate | |
validated={validated} | |
onSubmit={validateTFA} | |
> | |
<Form.Group> | |
<Form.Label htmlFor={`token`}>Token</Form.Label> | |
<InputGroup> | |
<InputGroup.Prepend> | |
<InputGroup.Text id={`token-text`}> | |
<i className={`fas fa-key`} /> | |
</InputGroup.Text> | |
</InputGroup.Prepend> | |
<Form.Control | |
type={`text`} | |
placeholder={`012 345`} | |
aria-label={`token`} | |
aria-describedby={`token-text`} | |
autoComplete={`token`} | |
name={`token`} | |
id={`token`} | |
onChange={handleChange('token')} | |
value={token} | |
/> | |
</InputGroup> | |
</Form.Group> | |
<ButtonGroup> | |
<Button | |
variant={`primary`} | |
size={`sm`} | |
className={`my-1`} | |
type={`submit`} | |
> | |
Submit | |
</Button> | |
</ButtonGroup> | |
</Form> | |
</Card.Body> | |
</Card> | |
</Content> | |
</Row> | |
</Layout> | |
); | |
}; | |
ValidateTwoFactorAuth.propTypes = { | |
validateFactorAuth: PropTypes.func.isRequired | |
}; | |
export default connect(null, { validateFactorAuth })( | |
withRouter(ValidateTwoFactorAuth) | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// | |
/// HEADS UP. I USUALLY DO HOOK, TO CONVERT THEM TO A BASE CLASS COMPONENT INSTEAD OF USING USEEFFECT, USESTATE,ETC | |
// YOU WILL NEED THE LIFECYCLE METHODS | |
// I'M ALSO USING REACT-BOOTSTRAP | |
// | |
// THIS IS THE COMPONENT IN WHICH YOU SEND A FIRST TIME TOKEN TO THE DB IN ORDER TO GENERATE A RECOVERY TOKEN | |
// FOR CASES IN WHICH YOU FORGET YOUR PASSWORD AND NO SINGLE GOOGLE AUTHENTICATOR TOKEN IS WORKING. | |
// YOU CAN NOT RESET YOUR PASSWORD WHEN YOUR TFA IS ENABLED.... | |
/// | |
/// | |
import React, { useState } from 'react'; | |
import { withRouter } from 'react-router-dom'; | |
import PropTypes from 'prop-types'; | |
import { connect } from 'react-redux'; | |
// ACTIONS | |
import { verifyTwoFactorAuthentication } from '../../actions/auth'; | |
// HELPERS | |
import Alert from '../layout/Alert'; | |
import Layout from '../layout/Layout'; | |
import { URL } from '../../config'; | |
import Content from '../layout/Container'; | |
// REACTBOOTSTRAP | |
import Row from 'react-bootstrap/Row'; | |
import Card from 'react-bootstrap/Card'; | |
import Form from 'react-bootstrap/Form'; | |
import InputGroup from 'react-bootstrap/InputGroup'; | |
import ButtonGroup from 'react-bootstrap/ButtonGroup'; | |
import Button from 'react-bootstrap/Button'; | |
const VerifyTwoFactorAuth = ({ verifyTwoFactorAuthentication, match }) => { | |
const userId = match.params.id; | |
const [tokenData, setTokenData] = useState({ | |
token: `` | |
}); | |
const { token } = tokenData; | |
const [verified, setVerified] = useState(false); | |
const [recoveryToken, setRecoveryToken] = useState(``); | |
const [validated, setValidated] = useState(false); | |
const [, setError] = useState(false); | |
const handleChange = (name) => (e) => { | |
setTokenData({ ...tokenData, [name]: e.target.value }); | |
}; | |
const verifyTFA = async (e) => { | |
const form = e.currentTarget; | |
if (form.checkValidity() === false) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
} | |
e.preventDefault(); | |
setValidated(true); | |
verifyTwoFactorAuthentication(userId, tokenData) | |
.then((result) => { | |
setVerified(true); | |
setRecoveryToken(result.payload.data); | |
}) | |
.catch((err) => { | |
setVerified(false); | |
setError(true); | |
}); | |
}; | |
return ( | |
<Layout | |
title={`beFree | Verify 2FA`} | |
description={`Verify your 2FA`} | |
author={`Kevin Fonseca`} | |
sectionClass={`mb-3`} | |
containerClass={`container`} | |
canonical={`${URL}`} | |
url={`two-fa-verify`} | |
posType={`page`} | |
> | |
<Row> | |
<Content fullWidth> | |
<Alert /> | |
<Card> | |
<Card.Header> | |
<Card.Title>Verify your first 2FA token</Card.Title> | |
</Card.Header> | |
<Card.Body> | |
<Form | |
className={`form`} | |
noValidate | |
validated={validated} | |
onSubmit={verifyTFA} | |
> | |
<Form.Group> | |
<Form.Label htmlFor={`token`}>Token</Form.Label> | |
<InputGroup> | |
<InputGroup.Prepend> | |
<InputGroup.Text id={`token-text`}> | |
<i className={`fas fa-key`} /> | |
</InputGroup.Text> | |
</InputGroup.Prepend> | |
<Form.Control | |
type={`text`} | |
placeholder={`012 345`} | |
aria-label={`token`} | |
aria-describedby={`token-text`} | |
autoComplete={`token`} | |
name={`token`} | |
id={`token`} | |
onChange={handleChange('token')} | |
value={token} | |
/> | |
</InputGroup> | |
</Form.Group> | |
{verified && ( | |
<> | |
<p> | |
Please keep this code in a safe but accessible area. This | |
is your <b>BACKUP</b> code: | |
<br /> | |
<code>{recoveryToken}</code> | |
<br /> | |
You can now close this window | |
</p> | |
</> | |
)} | |
<ButtonGroup> | |
<Button | |
variant={`primary`} | |
size={`sm`} | |
className={`my-1`} | |
type={`submit`} | |
> | |
Submit | |
</Button> | |
</ButtonGroup> | |
</Form> | |
</Card.Body> | |
</Card> | |
</Content> | |
</Row> | |
</Layout> | |
); | |
}; | |
VerifyTwoFactorAuth.propTypes = { | |
verifyTwoFactorAuthentication: PropTypes.func.isRequired | |
}; | |
export default connect(null, { verifyTwoFactorAuthentication })( | |
withRouter(VerifyTwoFactorAuth) | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment