diff --git a/api/src/controllers/user.js b/api/src/controllers/user.js index 4baacb6..5f60704 100644 --- a/api/src/controllers/user.js +++ b/api/src/controllers/user.js @@ -176,7 +176,7 @@ export const register = async (req, res) => { export const login = async (req, res) => { if (req.body.username && req.body.password) { try { - const user = await User.findOne({ username: req.body.username }) + const user = await User.findOne({ username: req.body.username }).lean() if (user) { if (user.banned) { @@ -184,8 +184,34 @@ export const login = async (req, res) => { return } + if (user.totp.enabled && !req.body.totp) { + res.status(401).send('One-time code required') + return + } + const matches = await bcrypt.compare(req.body.password, user.password) + if (user.totp.enabled) { + const validToken = speakeasy.totp.verify({ + secret: user.totp.secret, + encoding: 'base32', + token: req.body.totp, + window: 1, + }) + + if (!validToken) { + if (!user.totp.backup.includes(req.body.totp)) { + res.status(401).send('Invalid one-time code') + return + } else { + await User.findOneAndUpdate( + { username: req.body.username }, + { $pull: { 'totp.backup': req.body.totp } } + ) + } + } + } + if (matches) { res.send({ token: jwt.sign( diff --git a/client/pages/login.js b/client/pages/login.js index 0898b84..4f5e003 100644 --- a/client/pages/login.js +++ b/client/pages/login.js @@ -1,4 +1,4 @@ -import React, { useContext } from 'react' +import React, { useContext, useState } from 'react' import getConfig from 'next/config' import { useRouter } from 'next/router' import Link from 'next/link' @@ -11,6 +11,8 @@ import { NotificationContext } from '../components/Notifications' import LoadingContext from '../utils/LoadingContext' const Login = () => { + const [totpRequired, setTotpRequired] = useState(false) + const [, setCookie] = useCookies() const { addNotification } = useContext(NotificationContext) @@ -36,11 +38,13 @@ const Login = () => { body: JSON.stringify({ username: form.get('username'), password: form.get('password'), + totp: form.get('totp'), }), }) if (res.status !== 200) { const reason = await res.text() + if (reason === 'One-time code required') setTotpRequired(true) throw new Error(reason) } @@ -78,6 +82,9 @@ const Login = () => { mb={4} required /> + {totpRequired && ( + + )}