Implementing Node.js Two-Factor Authentication
Generating a secret key
pnpm i speakeasy @types/speakeasy qrcode @types/qrcode
.env append a new varible TWO_FACTOR_AUTHENTICATION_APP_NAME
TWO_FACTOR_AUTHENTICATION_APP_NAME=express-ts-tutorial
// src/service/auth/auth.service.ts
import * as speakeasy from 'speakeasy';
function getTwoFactorAuthenticationCode() {
const secretCode = speakeasy.generateSecret({
name: process.env.TWO_FACTOR_AUTHENTICATION_APP_NAME,
});
return {
otpauthUrl : secretCode.otpauth_url,
base32: secretCode.base32,
};
}
// src/controller/authentication/authentication.controller.ts
private initializeRoutes() {
this.router.get(`${this.path}/2fa/generate`, authMiddleware as any, this.generateTwoFactorAuthenticationCode as any);
this.router.post(`${this.path}/register`, validationMiddleware(CreateUserDto), this.registration);
this.router.post(`${this.path}/login`, validationMiddleware(LogInDto), this.loggingIn);
this.router.post(`${this.path}/logout`, this.loggingOut);
}
get cookie with http://localhost:5001/auth/login, and in chrome console use it. then visit http://localhost:5001/auth/2fa/generate, you can get the qr code image:
document.cookie='Authorization=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOjQsImlhdCI6MTY4OTc4Mjk4OSwiZXhwIjoxNjg5Nzg2NTg5fQ.0P-uNrSCGtM_Xr6J3HR2uAlPJyiYgixbbj0eAyBKXNc'
with app google authenticator in your phone scan the image, we got the code
Turning on Node.js Two-Factor Authentication
// src/dto/auth/auth.dto.ts
import { IsString } from 'class-validator';
class TwoFactorAuthenticationDto {
@IsString()
public twoFactorAuthenticationCode?: string;
}
export default TwoFactorAuthenticationDto;
// src/controller/authentication/authentication.controller.ts
private initializeRoutes() {
this.router.get(
`${this.path}/2fa/generate`,
authMiddleware as any,
this.generateTwoFactorAuthenticationCode as any
);
this.router.post(
`${this.path}/2fa/turn-on`,
validationMiddleware(TwoFactorAuthenticationDto),
authMiddleware as any,
this.turnOnTwoFactorAuthentication as any,
);
}
private turnOnTwoFactorAuthentication = async (
request: RequestWithUser,
response: express.Response,
next: express.NextFunction,
) => {
const userRepository = await getRepository(UserEntity);
const { twoFactorAuthenticationCode } = request.body;
const user = request.user;
const isCodeValid = await authService.verifyTwoFactorAuthenticationCode(
twoFactorAuthenticationCode, user,
);
if (isCodeValid) {
user.isTwoFactorAuthenticationEnabled = true;
await userRepository.save(user);
response.send(200);
} else {
next(new WrongAuthenticationTokenException());
}
}
Logging in using our Node.js Two-Factor Authentication
// src/controller/authentication/authentication.controller.ts
private initializeRoutes() {
this.router.get(
`${this.path}/2fa/generate`,
authMiddleware as any,
this.generateTwoFactorAuthenticationCode as any
);
this.router.post(
`${this.path}/2fa/turn-on`,
validationMiddleware(TwoFactorAuthenticationDto),
authMiddleware as any,
this.turnOnTwoFactorAuthentication as any,
);
this.router.post(
`${this.path}/2fa/authenticate`,
validationMiddleware(TwoFactorAuthenticationDto),
authMiddleware(true),
this.secondFactorAuthentication as any,
);
}
private secondFactorAuthentication = async (
request: RequestWithUser,
response: express.Response,
next: express.NextFunction,
) => {
const { twoFactorAuthenticationCode } = request.body;
const user = request.user;
const isCodeValid = await authService.verifyTwoFactorAuthenticationCode(
twoFactorAuthenticationCode, user,
);
if (isCodeValid) {
const tokenData = authService.createToken(user, true);
response.setHeader('Set-Cookie', [authService.createCookie(tokenData)]);
response.send({
...user,
password: undefined,
twoFactorAuthenticationCode: undefined
});
} else {
next(new WrongAuthenticationTokenException());
}
}
// src/service/auth/auth.service.ts
export function createToken(user: UserEntity, isSecondFactorAuthenticated = false): TokenData {
const expiresIn = 60 * 60; // an hour
const secret = process.env.JWT_SECRET;
const dataStoredInToken: DataStoredInToken = {
isSecondFactorAuthenticated,
_id: user.id,
};
return {
expiresIn,
token: jwt.sign(dataStoredInToken, secret!, { expiresIn }),
};
}
The authMiddleware
// src/middleware/auth.middleware.ts
import { NextFunction, Response } from 'express';
import * as jwt from 'jsonwebtoken';
import AuthenticationTokenMissingException from '../exceptions/AuthenticationTokenMissingException';
import WrongAuthenticationTokenException from '../exceptions/WrongAuthenticationTokenException';
import RequestWithUser from '../interfaces/requestWithUser.interface';
import { DataStoredInToken } from '../controller/authentication/interfaces/index';
import UserEntity from '../entities/user/user.entity';
import { getRepository } from '../utils/connectedToDatabase';
import express from 'express';
function authMiddleware(omitSecondFactor = false): express.RequestHandler {
return async (request: RequestWithUser, response: Response, next: NextFunction) => {
const cookies = request.cookies;
const userRepository = await getRepository(UserEntity);
if (cookies && cookies.Authorization) {
const secret = process.env.JWT_SECRET;
try {
const verificationResponse = jwt.verify(cookies.Authorization, secret!) as unknown as DataStoredInToken;
const { _id: id, isSecondFactorAuthenticated } = verificationResponse;
const user = await userRepository.findOneBy({id});
if (user) {
if (!omitSecondFactor && user.isTwoFactorAuthenticationEnabled && !isSecondFactorAuthenticated) {
next(new WrongAuthenticationTokenException());
} else {
request.user = user;
next();
}
} else {
next(new WrongAuthenticationTokenException());
}
} catch (error) {
next(new WrongAuthenticationTokenException());
}
} else {
next(new AuthenticationTokenMissingException());
}
};
}
export default authMiddleware;
// src/controller/authentication/authentication.controller.ts
private loggingIn = async (
request: express.Request,
response: express.Response,
next: express.NextFunction
) => {
const logInData: LogInDto = request.body;
const userRepository = await getRepository(UserEntity);
const user = await userRepository.findOneBy({ email: logInData.email });
if (user) {
const isPasswordMatching = await bcrypt.compare(
logInData.password!,
user.password!
);
if (isPasswordMatching) {
user.password = undefined;
user.twoFactorAuthenticationCode = undefined;
const tokenData = authService.createToken(user);
response.setHeader("Set-Cookie", [
authService.createCookie(tokenData),
]);
if (user.isTwoFactorAuthenticationEnabled) {
response.send({
isTwoFactorAuthenticationEnabled: true,
});
} else {
response.send(user);
}
} else {
next(new WrongCredentialsException());
}
} else {
next(new WrongCredentialsException());
}
};