Skip to content
This repository has been archived by the owner on Apr 19, 2023. It is now read-only.

Commit

Permalink
✨ Login with 2FA
Browse files Browse the repository at this point in the history
  • Loading branch information
AnandChowdhary committed May 30, 2019
1 parent d40db7f commit a93af38
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 17 deletions.
19 changes: 18 additions & 1 deletion src/controllers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
impersonate,
approveLocation,
verifyEmail,
register
register,
login2FA
} from "../rest/auth";
import { verifyToken } from "../helpers/jwt";
import {
Expand Down Expand Up @@ -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) {
Expand Down
26 changes: 25 additions & 1 deletion src/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
updateNotificationForUser,
enable2FAForUser,
disable2FAForUser,
verify2FAForUser
verify2FAForUser,
getBackupCodesForUser,
regenerateBackupCodesForUser
} from "../rest/user";
import { ErrorCode } from "../interfaces/enum";
import {
Expand Down Expand Up @@ -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));
}
}
25 changes: 17 additions & 8 deletions src/crud/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
};

/**
Expand All @@ -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 (<BackupCode[]>(
await query(
"SELECT * FROM `backup-codes` WHERE userId = ? AND code = ? LIMIT 1",
[userId, backupCode]
)
))[0];
};
23 changes: 19 additions & 4 deletions src/helpers/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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
*/
Expand Down Expand Up @@ -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);
};
1 change: 1 addition & 0 deletions src/interfaces/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
24 changes: 22 additions & 2 deletions src/rest/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
updateUser,
getUserByEmail,
getUser,
addApprovedLocation
addApprovedLocation,
getUserBackupCode,
updateBackupCode
} from "../crud/user";
import { InsertResult } from "../interfaces/mysql";
import {
Expand All @@ -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";
Expand All @@ -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 = <User>await verifyToken(token, Tokens.REFRESH);
Expand All @@ -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,
Expand Down
23 changes: 22 additions & 1 deletion src/rest/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
updateApiKey,
deleteApiKey,
createBackupCodes,
deleteUserBackupCodes
deleteUserBackupCodes,
getUserBackupCodes
} from "../crud/user";
import {
deleteAllUserMemberships,
Expand Down Expand Up @@ -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);
};

0 comments on commit a93af38

Please sign in to comment.