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 && (
+
+ )}