TypeScript Express tutorial #4. Registering users and authenticating with JWT

119 阅读1分钟

Registration

pnpm i bcrypt jsonwebtoken cookie-parser
pnpm i -D @types/bcrypt @types/jsonwebtoken @types/cookie-parser
// src/models/users/user.interface.ts

interface User {
	_id: string;
	firstName: string;
	lastName: string;
	fullName: string;
	email: string;
	password: string;
	address?: {
		street: string;
		city: string;
	};
}

export default User;

// src/models/users/user.model.ts

import * as mongoose from 'mongoose';
import User from './user.interface';
 
const userSchema = new mongoose.Schema<User>({
  name: String,
  email: String,
  password: String,
});
 
const userModel = mongoose.model<User & mongoose.Document>('User', userSchema);
 
export default userModel;
// src/dto/users/user.dto.ts

import { IsString } from 'class-validator';
 
class CreateUserDto {
  @IsString()
  public name?: string;
 
  @IsString()
  public email?: string;
 
  @IsString()
  public password?: string;
  
  constructor(email: string, password: string, name: string) {
    this.email = email;
    this.password = password;
    this.name = name;
  }
}
 
export default CreateUserDto;
// src/dto/users/login.dto.ts

import { IsString } from 'class-validator';
 
class LogInDto {
  @IsString()
  public email?: string;
 
  @IsString()
  public password?: string;
}
 
export default LogInDto;
// src/exceptions/UserWithThatEmailAlreadyExistsException.ts

import HttpException from "./HttpException";
 
class UserWithThatEmailAlreadyExistsException extends HttpException {
  constructor(email: string) {
    super(400, `User With That Email ${email} Already Exists`);
  }
}
 
export default UserWithThatEmailAlreadyExistsException;
// src/exceptions/WrongCredentialsException.ts

import HttpException from "./HttpException";
 
class WrongCredentialsException extends HttpException {
  constructor() {
    super(400, `Wrong Credentials Exception`);
  }
}
 
export default WrongCredentialsException;

Hashing with Bcrypt

test code


import * as bcrypt from 'bcrypt';

const test = async () => {

    const passwordInPlainText = '12345678';
    const hashedPassword = await bcrypt.hash(passwordInPlainText, 10);

    console.log('hashedPassword', hashedPassword);
    
     
    const doPasswordsMatch = await bcrypt.compare(passwordInPlainText, hashedPassword);
    console.log(doPasswordsMatch); // true
}

test();

Registration and logging in implementation

// src/controller/authentication/authentication.controller.ts

import * as bcrypt from 'bcrypt';
import express from 'express';
import userModel from '../../models/users/user.model';
import validationMiddleware from '../../middleware/validation.middleware';
import CreateUserDto from '../../dto/users/user.dto';
import LogInDto from '../../dto/users/login.dto';
import UserWithThatEmailAlreadyExistsException from '../../exceptions/UserWithThatEmailAlreadyExistsException';
import Controller from '../../interfaces/controller.interface';
import WrongCredentialsException from '../../exceptions/WrongCredentialsException';
 
class AuthenticationController implements Controller {
  public path = '/auth';
  public router = express.Router();
  private user = userModel;
 
  constructor() {
    this.initializeRoutes();
  }
 
  private initializeRoutes() {
    this.router.post(`${this.path}/register`, validationMiddleware(CreateUserDto), this.registration);
    this.router.post(`${this.path}/login`, validationMiddleware(LogInDto), this.loggingIn);
  }
 
  private registration = async (request: express.Request, response: express.Response, next: express.NextFunction) => {
    const userData: CreateUserDto = request.body;
    if (
      await this.user.findOne({ email: userData.email })
    ) {
      next(new UserWithThatEmailAlreadyExistsException(userData.email));
    } else {
      const hashedPassword = await bcrypt.hash((userData.password as string), 10);
      const user = await this.user.create({
        ...userData,
        password: hashedPassword,
      });
      user.password = undefined as any;
      response.send(user);
    }
  }
 
  private loggingIn = async (request: express.Request, response: express.Response, next: express.NextFunction) => {
    const logInData: LogInDto = request.body;
    const user = await this.user.findOne({ email: logInData.email });
    if (user) {
      const isPasswordMatching = await bcrypt.compare((logInData.password as any), user.password);
      if (isPasswordMatching) {
        user.password = undefined as any;
        response.send(user);
      } else {
        next(new WrongCredentialsException());
      }
    } else {
      next(new WrongCredentialsException());
    }
  }
}
 
export default AuthenticationController;

In this example the return of  this.user.create and  this.user.findOne is a MongoDB document. The actual data is represented in  user._doc and user.password  is just a getter that returns the data from  user._doc.password. To prevent sending the password back with a response you could also do  delete user._doc.password, but setting the   user.password to undefined also does the trick and there is no trace of the password in the response.

Authentication with JWT tokens

Signing tokens

// src/controller/authentication/interfaces/index.ts

export interface TokenData {
	token: string;
	expiresIn: number;
}

export interface DataStoredInToken {
	_id: string;
}

env

# set any char you want
JWT_SECRET=jwt_secret_you_set_not_leak
// src/utils/validateEnv.ts

import {
    cleanEnv, str, port
  } from 'envalid';
   
export default function validateEnv() {
    cleanEnv(process.env, {
      MONGO_PASSWORD: str(),
      MONGO_PATH: str(),
      MONGO_USER: str(),
      PORT: port(),
      JWT_SECRET: str(),
    });
  }
...
import * as jwt from 'jsonwebtoken';


class AuthenticationController implements Controller {
    ...
 private registration = async (request: express.Request, response: express.Response, next: express.NextFunction) => {
    const userData: CreateUserDto = request.body;
    if (
      await this.user.findOne({ email: userData.email })
    ) {
      next(new UserWithThatEmailAlreadyExistsException(userData.email));
    } else {
      const hashedPassword = await bcrypt.hash((userData.password as string), 10);
      const user = await this.user.create({
        ...userData,
        password: hashedPassword,
      });
      user.password = undefined as any;
      const tokenData = this.createToken(user);
      response.setHeader('Set-Cookie', [this.createCookie(tokenData)]);
      response.send(user);
    }
  }
 
  private loggingIn = async (request: express.Request, response: express.Response, next: express.NextFunction) => {
    const logInData: LogInDto = request.body;
    const user = await this.user.findOne({ email: logInData.email });
    if (user) {
      const isPasswordMatching = await bcrypt.compare((logInData.password as any), user.password);
      if (isPasswordMatching) {
        user.password = undefined as any;
        const tokenData = this.createToken(user);
        response.setHeader('Set-Cookie', [this.createCookie(tokenData)]);
        response.send(user);
      } else {
        next(new WrongCredentialsException());
      }
    } else {
      next(new WrongCredentialsException());
    }
  }

  private createCookie(tokenData: TokenData) {
    return `Authorization=${tokenData.token}; HttpOnly; Max-Age=${tokenData.expiresIn}`;
  }    
    
  private createToken(user: { _id: string }): TokenData {
    const expiresIn = 60 * 60; // an hour
    const secret = process.env.JWT_SECRET as string;
    const dataStoredInToken: DataStoredInToken = {
      _id: user._id,
    };
    return {
      expiresIn,
      token: jwt.sign(dataStoredInToken, secret, { expiresIn }),
    };
  }
}
// src/app.ts

import cookieParser from 'cookie-parser';  
 
class App {
  public app: express.Application;
 
  // (...)
 
  private initializeMiddlewares() {
    this.app.use(bodyParser.json());
    this.app.use(cookieParser());
  }
}
// src/exceptions/WrongAuthenticationTokenException.ts
import HttpException from './HttpException';

class WrongAuthenticationTokenException extends HttpException {
  constructor() {
    super(401, 'Wrong authentication token');
  }
}

export default WrongAuthenticationTokenException;
// src/exceptions/AuthenticationTokenMissingException.ts
import HttpException from './HttpException';

class AuthenticationTokenMissingException extends HttpException {
  constructor() {
    super(401, 'Authentication token missing');
  }
}

export default AuthenticationTokenMissingException;
// 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 userModel from '../models/users/user.model';
 
async function authMiddleware(request: RequestWithUser, response: Response, next: NextFunction) {
  const cookies = request.cookies;
  if (cookies && cookies.Authorization) {
    const secret = process.env.JWT_SECRET as string;
    try {
      const verificationResponse = jwt.verify(cookies.Authorization, secret) as DataStoredInToken;
      const id = verificationResponse._id;
      const user = await userModel.findById(id);
      if (user) {
        request.user = user;
        next();
      } else {
        next(new WrongAuthenticationTokenException());
      }
    } catch (error) {
      next(new WrongAuthenticationTokenException());
    }
  } else {
    next(new AuthenticationTokenMissingException());
  }
}
 
export default authMiddleware;
// src/controller/posts/post.controller.ts

	private initializeRoutes() {
		// this.router.get(this.path, this.getAllPosts);
		// this.router.get(`${this.path}/:id`, this.getPostById);
		// this.router.patch(`${this.path}/:id`, this.modifyPost);
		// this.router.delete(`${this.path}/:id`, this.deletePost);
		// this.router.post(
		// 	this.path,
		// 	validationMiddleware(CreatePostDto),
		// 	this.createPost
		// );

		this.router.get(this.path, this.getAllPosts);
		this.router.get(`${this.path}/:id`, this.getPostById);
		this.router
			.all(`${this.path}/*`, (authMiddleware as any))
			.patch(
				`${this.path}/:id`,
				validationMiddleware(CreatePostDto, true),
				this.modifyPost
			)
			.delete(`${this.path}/:id`, this.deletePost)
			.post(
				this.path,
				(authMiddleware as any),
				validationMiddleware(CreatePostDto),
				this.createPost
			);
	}

Logging out


class AuthenticationController implements Controller {
    // ...
      private initializeRoutes() {
    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);
  }
    // ...

  private loggingOut = (request: express.Request, response: express.Response) => {
    response.setHeader('Set-Cookie', ['Authorization=;Max-age=0']);
    response.send(200);
  }
}

ref: jwt bcrypt sources