From a93af38143764343eb29a25ef61b3deccc136069 Mon Sep 17 00:00:00 2001 From: Anand Chowdhary Date: Thu, 30 May 2019 18:02:23 +0200 Subject: [PATCH] :sparkles: Login with 2FA --- src/controllers/auth.ts | 19 ++++++++++++++++++- src/controllers/user.ts | 26 +++++++++++++++++++++++++- src/crud/user.ts | 25 +++++++++++++++++-------- src/helpers/jwt.ts | 23 +++++++++++++++++++---- src/interfaces/enum.ts | 1 + src/rest/auth.ts | 24 ++++++++++++++++++++++-- src/rest/user.ts | 23 ++++++++++++++++++++++- 7 files changed, 124 insertions(+), 17 deletions(-) diff --git a/src/controllers/auth.ts b/src/controllers/auth.ts index c60d2c19e..172b53a75 100644 --- a/src/controllers/auth.ts +++ b/src/controllers/auth.ts @@ -10,7 +10,8 @@ import { impersonate, approveLocation, verifyEmail, - register + register, + login2FA } from "../rest/auth"; import { verifyToken } from "../helpers/jwt"; import { @@ -90,6 +91,22 @@ export class AuthController { res.json(await login(email, password, res.locals)); } + @Post("2fa") + async twoFactor(req: Request, res: Response) { + const code = req.body.code; + const token = req.body.token; + joiValidate( + { + token: Joi.string().required(), + code: Joi.number() + .min(5) + .required() + }, + { code, token } + ); + res.json(await login2FA(code, token, res.locals)); + } + @Post("verify-token") @Middleware(authHandler) async postVerifyToken(req: Request, res: Response) { diff --git a/src/controllers/user.ts b/src/controllers/user.ts index e2c2b15a6..43d4eaf43 100644 --- a/src/controllers/user.ts +++ b/src/controllers/user.ts @@ -15,7 +15,9 @@ import { updateNotificationForUser, enable2FAForUser, disable2FAForUser, - verify2FAForUser + verify2FAForUser, + getBackupCodesForUser, + regenerateBackupCodesForUser } from "../rest/user"; import { ErrorCode } from "../interfaces/enum"; import { @@ -344,4 +346,26 @@ export class UserController { ); res.json(await disable2FAForUser(res.locals.token.id, id)); } + + @Get(":id/backup-codes") + async getBackupCodes(req: Request, res: Response) { + let id = req.params.id; + if (id === "me") id = res.locals.token.id; + joiValidate( + { id: [Joi.string().required(), Joi.number().required()] }, + { id } + ); + res.json(await getBackupCodesForUser(res.locals.token.id, id)); + } + + @Get(":id/backup-codes/regenerate") + async getRegenerateBackupCodes(req: Request, res: Response) { + let id = req.params.id; + if (id === "me") id = res.locals.token.id; + joiValidate( + { id: [Joi.string().required(), Joi.number().required()] }, + { id } + ); + res.json(await regenerateBackupCodesForUser(res.locals.token.id, id)); + } } diff --git a/src/crud/user.ts b/src/crud/user.ts index 4320b903c..bd85425bb 100644 --- a/src/crud/user.ts +++ b/src/crud/user.ts @@ -274,22 +274,19 @@ export const createBackupCodes = async (userId: number, count = 1) => { /** * Update a backup code */ -export const updateBackupCode = async ( - backupCodeId: number, - code: KeyValue -) => { +export const updateBackupCode = async (backupCode: number, code: KeyValue) => { code.updatedAt = new Date(); return await query( - `UPDATE \`backup-codes\` SET ${setValues(code)} WHERE id = ?`, - [...Object.values(code), backupCodeId] + `UPDATE \`backup-codes\` SET ${setValues(code)} WHERE code = ?`, + [...Object.values(code), backupCode] ); }; /** * Delete a backup code */ -export const deleteBackupCode = async (backupCodeId: number) => { - return await query("DELETE FROM `backup-codes` WHERE id = ?", [backupCodeId]); +export const deleteBackupCode = async (backupCode: number) => { + return await query("DELETE FROM `backup-codes` WHERE code = ?", [backupCode]); }; /** @@ -305,3 +302,15 @@ export const deleteUserBackupCodes = async (userId: number) => { export const getUserBackupCodes = async (userId: number) => { return await query("SELECT * FROM `backup-codes` WHERE userId = ?", [userId]); }; + +/** + * Get a specific backup code + */ +export const getUserBackupCode = async (userId: number, backupCode: number) => { + return (( + await query( + "SELECT * FROM `backup-codes` WHERE userId = ? AND code = ? LIMIT 1", + [userId, backupCode] + ) + ))[0]; +}; diff --git a/src/helpers/jwt.ts b/src/helpers/jwt.ts index d829e6547..beb66a92f 100644 --- a/src/helpers/jwt.ts +++ b/src/helpers/jwt.ts @@ -71,6 +71,12 @@ export const passwordResetToken = (id: number) => export const loginToken = (user: User) => generateToken(user, TOKEN_EXPIRY_LOGIN, Tokens.LOGIN); +/** + * Generate a new 2FA JWT + */ +export const twoFactorToken = (user: User) => + generateToken({ userId: user.id }, TOKEN_EXPIRY_LOGIN, Tokens.TWO_FACTOR); + /** * Generate a new approve location JWT */ @@ -83,6 +89,14 @@ export const approveLocationToken = (id: number) => export const refreshToken = (id: number) => generateToken({ id }, TOKEN_EXPIRY_REFRESH, Tokens.REFRESH); +export const postLoginTokens = async (user: User) => { + if (!user.id) throw new Error(ErrorCode.USER_NOT_FOUND); + return { + token: await loginToken(deleteSensitiveInfoUser(user)), + refresh: await refreshToken(user.id) + }; +}; + /** * Get the token response after logging in a user */ @@ -117,8 +131,9 @@ export const getLoginResponse = async ( }, locals ); - return { - token: await loginToken(deleteSensitiveInfoUser(user)), - refresh: await refreshToken(user.id) - }; + if (user.twoFactorEnabled) + return { + twoFactorToken: await twoFactorToken(user) + }; + return await postLoginTokens(user); }; diff --git a/src/interfaces/enum.ts b/src/interfaces/enum.ts index 4e19d7560..a43b0ab16 100644 --- a/src/interfaces/enum.ts +++ b/src/interfaces/enum.ts @@ -90,6 +90,7 @@ export enum Templates { export enum Tokens { LOGIN = "auth", + TWO_FACTOR = "2fa", REFRESH = "refresh", PASSWORD_RESET = "password-reset", EMAIL_VERIFY = "email-verify", diff --git a/src/rest/auth.ts b/src/rest/auth.ts index c65b572f9..98b07e274 100644 --- a/src/rest/auth.ts +++ b/src/rest/auth.ts @@ -4,7 +4,9 @@ import { updateUser, getUserByEmail, getUser, - addApprovedLocation + addApprovedLocation, + getUserBackupCode, + updateBackupCode } from "../crud/user"; import { InsertResult } from "../interfaces/mysql"; import { @@ -17,7 +19,8 @@ import { mail } from "../helpers/mail"; import { verifyToken, passwordResetToken, - getLoginResponse + getLoginResponse, + postLoginTokens } from "../helpers/jwt"; import { KeyValue, Locals } from "../interfaces/general"; import { createEvent } from "../crud/event"; @@ -39,6 +42,7 @@ import { } from "../helpers/google"; import { can } from "../helpers/authorization"; import { validate } from "../helpers/utils"; +import { authenticator } from "otplib"; export const validateRefreshToken = async (token: string, locals: Locals) => { const data = await verifyToken(token, Tokens.REFRESH); @@ -61,6 +65,22 @@ export const login = async ( throw new Error(ErrorCode.INVALID_LOGIN); }; +export const login2FA = async (code: number, token: string, locals: Locals) => { + const data = (await verifyToken(token, Tokens.TWO_FACTOR)) as any; + const user = await getUser(data.userId, true); + const secret = user.twoFactorSecret; + if (!secret) throw new Error(ErrorCode.NOT_ENABLED_2FA); + if (!user.id) throw new Error(ErrorCode.USER_NOT_FOUND); + if (authenticator.check(code.toString(), secret)) + return await postLoginTokens(user); + const backupCode = await getUserBackupCode(data.userId, code); + if (!backupCode.used) { + await updateBackupCode(backupCode.code, { used: true }); + return await postLoginTokens(user); + } + throw new Error(ErrorCode.INVALID_2FA_TOKEN); +}; + export const register = async ( user: User, locals: Locals, diff --git a/src/rest/user.ts b/src/rest/user.ts index 98e3c1d29..ad9d32629 100644 --- a/src/rest/user.ts +++ b/src/rest/user.ts @@ -16,7 +16,8 @@ import { updateApiKey, deleteApiKey, createBackupCodes, - deleteUserBackupCodes + deleteUserBackupCodes, + getUserBackupCodes } from "../crud/user"; import { deleteAllUserMemberships, @@ -271,3 +272,23 @@ export const disable2FAForUser = async ( await deleteUserBackupCodes(userId); await updateUser(userId, { twoFactorEnabled: false, twoFactorSecret: "" }); }; + +export const getBackupCodesForUser = async ( + tokenUserId: number, + userId: number +) => { + if (!(await can(tokenUserId, Authorizations.READ_SECURE, "user", userId))) + throw new Error(ErrorCode.INSUFFICIENT_PERMISSION); + return await getUserBackupCodes(userId); +}; + +export const regenerateBackupCodesForUser = async ( + tokenUserId: number, + userId: number +) => { + if (!(await can(tokenUserId, Authorizations.READ_SECURE, "user", userId))) + throw new Error(ErrorCode.INSUFFICIENT_PERMISSION); + await deleteUserBackupCodes(userId); + await createBackupCodes(userId, 10); + return await getUserBackupCodes(userId); +};