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.createandthis.user.findOneis a MongoDB document. The actual data is represented inuser._docanduser.passwordis just a getter that returns the data fromuser._doc.password. To prevent sending the password back with a response you could also dodelete user._doc.password, but setting theuser.passwordto 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);
}
}