TypeScript Express tutorial #11. Node.js Two-Factor Authentication

249 阅读1分钟

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'

image.png

with app google authenticator in your phone scan the image, we got the code

image.png

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());
		}
	};