Skip to content

Commit ecc49a1

Browse files
committed
2fa logic on log in
1 parent 977205f commit ecc49a1

2 files changed

Lines changed: 35 additions & 2 deletions

File tree

api/src/controllers/user.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,16 +176,42 @@ export const register = async (req, res) => {
176176
export const login = async (req, res) => {
177177
if (req.body.username && req.body.password) {
178178
try {
179-
const user = await User.findOne({ username: req.body.username })
179+
const user = await User.findOne({ username: req.body.username }).lean()
180180

181181
if (user) {
182182
if (user.banned) {
183183
res.status(403).send('User is banned')
184184
return
185185
}
186186

187+
if (user.totp.enabled && !req.body.totp) {
188+
res.status(401).send('One-time code required')
189+
return
190+
}
191+
187192
const matches = await bcrypt.compare(req.body.password, user.password)
188193

194+
if (user.totp.enabled) {
195+
const validToken = speakeasy.totp.verify({
196+
secret: user.totp.secret,
197+
encoding: 'base32',
198+
token: req.body.totp,
199+
window: 1,
200+
})
201+
202+
if (!validToken) {
203+
if (!user.totp.backup.includes(req.body.totp)) {
204+
res.status(401).send('Invalid one-time code')
205+
return
206+
} else {
207+
await User.findOneAndUpdate(
208+
{ username: req.body.username },
209+
{ $pull: { 'totp.backup': req.body.totp } }
210+
)
211+
}
212+
}
213+
}
214+
189215
if (matches) {
190216
res.send({
191217
token: jwt.sign(

client/pages/login.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useContext } from 'react'
1+
import React, { useContext, useState } from 'react'
22
import getConfig from 'next/config'
33
import { useRouter } from 'next/router'
44
import Link from 'next/link'
@@ -11,6 +11,8 @@ import { NotificationContext } from '../components/Notifications'
1111
import LoadingContext from '../utils/LoadingContext'
1212

1313
const Login = () => {
14+
const [totpRequired, setTotpRequired] = useState(false)
15+
1416
const [, setCookie] = useCookies()
1517

1618
const { addNotification } = useContext(NotificationContext)
@@ -36,11 +38,13 @@ const Login = () => {
3638
body: JSON.stringify({
3739
username: form.get('username'),
3840
password: form.get('password'),
41+
totp: form.get('totp'),
3942
}),
4043
})
4144

4245
if (res.status !== 200) {
4346
const reason = await res.text()
47+
if (reason === 'One-time code required') setTotpRequired(true)
4448
throw new Error(reason)
4549
}
4650

@@ -78,6 +82,9 @@ const Login = () => {
7882
mb={4}
7983
required
8084
/>
85+
{totpRequired && (
86+
<Input name="totp" label="One-time code" mb={4} required />
87+
)}
8188
<Button>Log in</Button>
8289
</form>
8390
<Link href="/reset-password/initiate" passHref>

0 commit comments

Comments
 (0)