NestJS Authentication with OAuth2.0: Configuration and Operations
Tags: typescript, nestjs, tutorial, node
Published: 2023-01-14 17:39:25

## Series Intro

This series will cover the full implementation of OAuth2.0 Authentication in [NestJS](https://nestjs.com/) for the following types of APIs:

- [Express](https://expressjs.com/) REST API;
- [Fastify](https://www.fastify.io/) REST API;
- [Apollo GraphQL](https://www.apollographql.com/docs/apollo-server/) API.

And it is divided in 5 parts:

- Configuration and operations;
- Express Local OAuth REST API;
- Fastify Local OAuth REST API;
- Apollo Local OAuth GraphQL API;
- Adding External OAuth Providers to our API;

Lets start the first part of this series.

## Tutorial Intro

In this tutorial I will cover all the common operations necessary for implementing any type of OAuth system:

- User CRUD;
- User versioning for single user token revocation;
- JWT token generation;
- Auth module with token blacklisting.


<small><b>TLDR:</b> if you do not have 45 minutes to read the article, the code can be found on this [repo](https://github.com/tugascript/nestjs-oauth/tree/configuration-and-operations)</small>

## Overview

Local OAuth system, is an authentication system comprised on authentication through JSON Web Tokens (JWTs), where we use an access and refresh token pair:

- **Access Token**: the token we use to authenticate the current user by sending it on the Authorization header as a Bearer token. It has a small lifespan of 5 to 15 minutes;
- **Refresh Token**: this token is normally sent on a signed HTTP only cookie and is used to refresh the access tokens, this is achieved since the refresh token has a higher lifespan from 20 minutes to 7 days.

## Set up

Start by creating a new NestJS app and open it on VSCode:

```bash
$ npm i -g @nestjs/cli
$ nest new nest-local-oauth -s
$ code nest-local-oauth
```

Create a new yarn config file (`.yarnrc.yml`):

```yml
nodeLinker: node-modules
```

Install the latest version of yarn:

```bash
$ yarn set version stable
$ yarn plugin import interactive-tools
```

Before installing the packages add yarn cache to `.gitignore`:

```
### Yarn
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
```

On the `tsconfig.json` add `"esModuleInterop"`:

```json
{
  "compilerOptions": {
    "...": "...",
    "esModuleInterop": true
  }
}
```

Finally install the packages and upgrade to the latest version:

```bash
$ yarn install
$ yarn upgrade-interactive
```

## Technologies

For all adapters we will use the same tech-stack:

- [_MikroORM_](https://mikro-orm.io/): to interact with our database;
- [_Bcrypt_](https://www.npmjs.com/package/bcrypt): for hashing passwords, note that for new projects you should use argon2 as it is more secure, however since bcrypt is still the norm I will explain how to build with it;
- [_JSON Web Tokens_](https://www.npmjs.com/package/jsonwebtoken): the core of this authentication system;
- [_UUID_](https://www.npmjs.com/package/uuid): we will need unique identifiers to be able to blacklist our tokens;
- [_DayJS_](https://day.js.org/): for date manipulations.

So start by installing all packages:

```bash
$ yarn add @mikro-orm/core @mikro-orm/postgresql @mikro-orm/nestjs bcrypt jsonwebtoken uuid dayjs
$ yarn add -D @mikro-orm/cli @types/bcrypt @types/jsonwebtoken @types/nodemailer @types/uuid
```

## Configuration

Before we start we need several things:

- Secrets and lifetimes of the tokens;
- Name and secret of cookie;
- Email configuration;
- Database url.

### Types of token

For a complete authentication system we need 3 types of tokens:

- _Access_: the access token for authorization;
- _Refresh_: the refresh token for refreshing the access token;
- _Reset_: used to reset an user password given an email;
- _Confirmation_: use to confirm the user.

Therefore the access token will be something as follows:

```env
# JWT tokens
JWT_ACCESS_TIME=600
JWT_CONFIRMATION_SECRET='random_string'
JWT_CONFIRMATION_TIME=3600
JWT_RESET_PASSWORD_SECRET='random_string'
JWT_RESET_PASSWORD_TIME=1800
JWT_REFRESH_SECRET='random_string'
JWT_REFRESH_TIME=604800
```

Since access tokens need to be decoded by the Gateway (or other services if you do not have a Gateway), it needs to use a public and private key pair. You can generate a 2048 bits RSA key [here](https://www.csfieldguide.org.nz/en/interactives/rsa-key-generator/), and add them to a `keys` directory on the root of your project.

### Cookie config

Our refresh token will be sent on a http only signed cookie so we need a variable for the refresh cookie name and secret.

```env
# Refresh token
REFRESH_COOKIE='cookie_name'
COOKIE_SECRET='random_string'
```

### Email config

To send emails we will use [Nodemailer](https://nodemailer.com/about/) se we just need to add typical email configuration parameters:

```env
# Email config
EMAIL_HOST='smtp.gmail.com'
EMAIL_PORT=587
EMAIL_SECURE=false
EMAIL_USER='johndoe@gmail.com'
EMAIL_PASSWORD='your_email_password'
```

### Database config

For the database we just need the PostgreSQL URL:

```env
# Database config
DATABASE_URL='postgresql://postgres:postgres@localhost:5432/auth'
```

### General config

Other move geneal variables:

- Node environment, change to production ;
- APP ID, an UUID for the api;
- PORT, the API port on the server (normally 5000);
- Front-end Domain.

```env
APP_ID='00000-00000-00000-00000-00000'
NODE_ENV='development'
PORT=4000
DOMAIN='localhost:3000'
```

### Config Module

For configuration we can use the nestjs `ConfigModule`, so start by creating a `config` folder on the `src` folder. Then inside the `config` folder start adding the interfaces folder with the config interfaces:

_JWT interface_:

```ts
// jwt.interface.ts

export interface ISingleJwt {
  secret: string;
  time: number;
}

export interface IAccessJwt {
  publicKey: string;
  privateKey: string;
  time: number;
}

export interface IJwt {
  access: IAccessJwt;
  confirmation: ISingleJwt;
  resetPassword: ISingleJwt;
  refresh: ISingleJwt;
}
```

_Email config interface_:

```ts
// email-config.interface.ts

interface IEmailAuth {
  user: string;
  pass: string;
}

export interface IEmailConfig {
  host: string;
  port: number;
  secure: boolean;
  auth: IEmailAuth;
}
```

_Config interface_:

```ts
// config.interface.ts

import { MikroOrmModuleOptions } from '@mikro-orm/nestjs';
import { IEmailConfig } from './email-config.interface';
import { IJwt } from './jwt.interface';

export interface IConfig {
  id: string;
  port: number;
  domain: string;
  db: MikroOrmModuleOptions;
  jwt: IJwt;
  emailService: IEmailConfig;
}
```

Create the config function:

```ts
// index.ts

import { LoadStrategy } from '@mikro-orm/core';
import { defineConfig } from '@mikro-orm/postgresql';
import { readFileSync } from 'fs';
import { join } from 'path';
import { IConfig } from './interfaces/config.interface';

export function config(): IConfig {
  const publicKey = readFileSync(
    join(__dirname, '..', '..', 'keys/public.key'),
    'utf-8',
  );
  const privateKey = readFileSync(
    join(__dirname, '..', '..', 'keys/private.key'),
    'utf-8',
  );

  return {
    id: process.env.APP_ID,
    port: parseInt(process.env.PORT, 10),
    domain: process.env.DOMAIN,
    jwt: {
      access: {
        privateKey,
        publicKey,
        time: parseInt(process.env.JWT_ACCESS_TIME, 10),
      },
      confirmation: {
        secret: process.env.JWT_CONFIRMATION_SECRET,
        time: parseInt(process.env.JWT_CONFIRMATION_TIME, 10),
      },
      resetPassword: {
        secret: process.env.JWT_RESET_PASSWORD_SECRET,
        time: parseInt(process.env.JWT_RESET_PASSWORD_TIME, 10),
      },
      refresh: {
        secret: process.env.JWT_REFRESH_SECRET,
        time: parseInt(process.env.JWT_REFRESH_TIME, 10),
      },
    },
    emailService: {
      host: process.env.EMAIL_HOST,
      port: parseInt(process.env.EMAIL_PORT, 10),
      secure: process.env.EMAIL_SECURE === 'true',
      auth: {
        user: process.env.EMAIL_USER,
        pass: process.env.EMAIL_PASSWORD,
      },
    },
    db: defineConfig({
      clientUrl: process.env.DATABASE_URL,
      entities: ['dist/**/*.entity.js', 'dist/**/*.embeddable.js'],
      entitiesTs: ['src/**/*.entity.ts', 'src/**/*.embeddable.ts'],
      loadStrategy: LoadStrategy.JOINED,
      allowGlobalContext: true,
    }),
  };
}
```

Install the config module:

```bash
$ yarn add @nestjs/config joi
```

Create a validation schema:

```ts
// config.schema.ts

import Joi from 'joi';

export const validationSchema = Joi.object({
  APP_ID: Joi.string().uuid({ version: 'uuidv4' }).required(),
  NODE_ENV: Joi.string().required(),
  PORT: Joi.number().required(),
  URL: Joi.string().required(),
  DATABASE_URL: Joi.string().required(),
  JWT_ACCESS_TIME: Joi.number().required(),
  JWT_CONFIRMATION_SECRET: Joi.string().required(),
  JWT_CONFIRMATION_TIME: Joi.number().required(),
  JWT_RESET_PASSWORD_SECRET: Joi.string().required(),
  JWT_RESET_PASSWORD_TIME: Joi.number().required(),
  JWT_REFRESH_SECRET: Joi.string().required(),
  JWT_REFRESH_TIME: Joi.number().required(),
  REFRESH_COOKIE: Joi.string().required(),
  COOKIE_SECRET: Joi.string().required(),
  EMAIL_HOST: Joi.string().required(),
  EMAIL_PORT: Joi.number().required(),
  EMAIL_SECURE: Joi.bool().required(),
  EMAIL_USER: Joi.string().email().required(),
  EMAIL_PASSWORD: Joi.string().required(),
});
```

And finally import it to the `app.module.ts`:

```ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { config } from './config';
import { validationSchema } from './config/config.schema';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema,
      load: [config],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

### Mikro-ORM Config

Mikro-ORM needs a config file added to our `package.json` so on the `src` folder create the file:

```ts
// mikro-orm.config.ts

import { LoadStrategy, Options } from '@mikro-orm/core';
import { defineConfig } from '@mikro-orm/postgresql';

const config: Options = defineConfig({
  clientUrl: process.env.DATABASE_URL,
  entities: ['dist/**/*.entity.js', 'dist/**/*.embeddable.js'],
  entitiesTs: ['src/**/*.entity.ts', 'src/**/*.embeddable.ts'],
  loadStrategy: LoadStrategy.JOINED,
  allowGlobalContext: true,
});

export default config;
```

And on our `package.json` file add the config file:

```json
{
  "...": "...",
  "mikro-orm": {
    "useTsNode": true,
    "configPaths": [
      "./src/mikro-orm.config.ts",
      "./dist/mikro-orm.config.js"
    ]
  }
}
```

To use it on our `config` folder we need to add a class for the `MikroOrmModule`:

```ts
// mikro-orm.config.ts

import {
  MikroOrmModuleOptions,
  MikroOrmOptionsFactory,
} from '@mikro-orm/nestjs';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class MikroOrmConfig implements MikroOrmOptionsFactory {
  constructor(private readonly configService: ConfigService) {}

  public createMikroOrmOptions(): MikroOrmModuleOptions {
    return this.configService.get<MikroOrmModuleOptions>('db');
  }
}
```

And asynchronous register the module on our `app.module.ts` file:

```ts
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
// ...
import { MikroOrmConfig } from './config/mikroorm.config';

@Module({
  imports: [
    // ...
    MikroOrmModule.forRootAsync({
      imports: [ConfigModule],
      useClass: MikroOrmConfig,
    }),
  ],
  // ...
})
export class AppModule {}
```

## Common Module

I like to have a common global module for entity validation, error handling and string manipulation.

For entity and input validation we will use class validator, as explained in the [docs](https://docs.nestjs.com/techniques/validation):

```bash
$ yarn add class-transformer class-validator
```

And add it to the main file:

```ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}

bootstrap();
```

Create the module and service:

```bash
$ nest g mo common
$ nest g s common
```

On the `common` folder add the `Global` decorator to the module and export the service:

```ts
import { Global, Module } from '@nestjs/common';
import { CommonService } from './common.service';

@Global()
@Module({
  providers: [CommonService],
  exports: [CommonService],
})
export class CommonModule {}
```

Personally I like to have all my Regular Expressions inside common, so create a `consts` folder with a `regex.const.ts` file:

```ts
// checks if a password has at least one uppercase letter and a number or special character
export const PASSWORD_REGEX =
  /((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/;

// checks if a string has only letters, numbers, spaces, apostrophes, dots and dashes
export const NAME_REGEX = /(^[\p{L}\d'\.\s\-]*$)/u;

// checks if a string is a valid slug, useful for usernames
export const SLUG_REGEX = /^[a-z\d]+(?:(\.|-|_)[a-z\d]+)*$/;

// validates if passwords are valid bcrypt hashes
export const BCRYPT_HASH = /\$2[abxy]?\$\d{1,2}\$[A-Za-z\d\./]{53}/;
```

Another thing is utility functions for common checks, create a `utils` function with a validation file:

```ts
// validation.util.ts

export const isUndefined = (value: unknown): value is undefined =>
  typeof value === 'undefined';

export const isNull = (value: unknown): value is null => value === null;
```

### Common Service

Start by adding a `LoggerService`:

```ts
import { Dictionary, EntityRepository } from '@mikro-orm/core';
import { Injectable, Logger, LoggerService } from '@nestjs/common';

@Injectable()
export class CommonService {
  private readonly loggerService: LoggerService;

  constructor() {
    this.loggerService = new Logger(CommonService.name);
  }
}
```

We need the following methods:

_Validator for entities_

```ts
import { Dictionary } from '@mikro-orm/core';
import { 
  BadRequestException, 
  NotFoundException,
  // ...
} from '@nestjs/common';
import { validate } from 'class-validator';
    
@Injectable()
export class CommonService {
  // ...

  /**
   * Validate Entity
   *
   * Validates an entities with the class-validator library
   */
  public async validateEntity(entity: Dictionary): Promise<void> {
    const errors = await validate(entity);
    const messages: string[] = [];

    for (const error of errors) {
      messages.push(...Object.values(error.constraints));
    }

    if (errors.length > 0) {
      throw new BadRequestException(messages.join(',\n'));
    }
  }
}
```

_Promise error wrappers_

```ts
import { Dictionary, EntityRepository } from '@mikro-orm/core';
import {
  BadRequestException,
  ConflictException,
  InternalServerErrorException,
  // ...
} from '@nestjs/common';
// ...

@Injectable()
export class CommonService {
  // ...

  /**
   * Throw Duplicate Error
   *
   * Checks is an error is of the code 23505, PostgreSQL's duplicate value error,
   * and throws a conflict exception
   */
  public async throwDuplicateError<T>(promise: Promise<T>, message?: string) {
    try {
      return await promise;
    } catch (error) {
      this.loggerService.error(error);

      if (error.code === '23505') {
        throw new ConflictException(message ?? 'Duplicated value in database');
      }

      throw new BadRequestException(error.message);
    }
  }

  /**
   * Throw Internal Error
   *
   * Function to abstract throwing internal server exception
   */
  public async throwInternalError<T>(promise: Promise<T>): Promise<T> {
    try {
      return await promise;
    } catch (error) {
      this.loggerService.error(error);
      throw new InternalServerErrorException(error);
    }
  }
}
```

_Entity Actions_

```ts
import { Dictionary, EntityRepository } from '@mikro-orm/core';
// ...

@Injectable()
export class CommonService {
  // ...

  /**
   * Check Entity Existence
   *
   * Checks if a findOne query didn't return null or undefined
   */
  public checkEntityExistence<T extends Dictionary>(
    entity: T | null | undefined,
    name: string,
  ): void {
    if (isNull(entity) || isUndefined(entity)) {
      throw new NotFoundException(`${name} not found`);
    }
  }

  /**
   * Save Entity
   *
   * Validates, saves and flushes entities into the DB
   */
  public async saveEntity<T extends Dictionary>(
    repo: EntityRepository<T>,
    entity: T,
    isNew = false,
  ): Promise<void> {
    await this.validateEntity(entity);

    if (isNew) {
      repo.persist(entity);
    }

    await this.throwDuplicateError(repo.flush());
  }

  /**
   * Remove Entity
   *
   * Removes an entities from the DB.
   */
  public async removeEntity<T extends Dictionary>(
    repo: EntityRepository<T>,
    entity: T,
  ): Promise<void> {
    await this.throwInternalError(repo.removeAndFlush(entity));
  }

}
```

_String manipulation_

Start by installing `slugify`:

```bash
$ yarn add slugify
```

Now add the methods:

```ts
// ...
import slugify from 'slugify';
// ...

@Injectable()
export class CommonService {
  // ...

  /**
   * Format Name
   *
   * Takes a string trims it and capitalizes every word
   */
  public formatName(title: string): string {
    return title
      .trim()
      .replace(/\n/g, ' ')
      .replace(/\s\s+/g, ' ')
      .replace(/\w\S*/g, (w) => w.replace(/^\w/, (l) => l.toUpperCase()));
  }

  /**
   * Generate Point Slug
   *
   * Takes a string and generates a slug with dtos as word separators
   */
  public generatePointSlug(str: string): string {
    return slugify(str, { lower: true, replacement: '.', remove: /['_\.\-]/g });
  }
}
```

_Message Generation_

There are endpoints that have to return a single message string, for those type of endpoints I like to make a message interface with an id, so it is easier to filter on the front-end, create an `interfaces` folder and add the following file:

```ts
// message.interface.ts

export interface IMessage {
  id: string;
  message: string;
}
```

And create a method for it:
```ts
// ...
import { v4 } from 'uuid';
// ...

@Injectable()
export class CommonService {
  // ...

  public generateMessage(message: string): IMessage {
    return { id: v4(), message };
  }
} 
```

## Users Module

Before creating the auth module we need a way to do CRUD operations on our users, so create a new users module and service:

```bash
$ nest g mo users
$ nest g s users
```

### User Entity

Before creating the entity we need an interface with what we want in our user, create an `interfaces` folder and the `user.interface.ts` file:

```ts
export interface IUser {
  id: number;
  name: string;
  username: string;
  email: string;
  password: string;
  confirmed: boolean;
  createdAt: Date;
  updatedAt: Date;
}
```

Now implement that on an entity, start by creating an `entities` folder:

```ts
// user.entity.ts

import { Entity, PrimaryKey, Property } from '@mikro-orm/core';
import { IsBoolean, IsEmail, IsString, Length, Matches } from 'class-validator';
import {
  BCRYPT_HASH,
  NAME_REGEX,
  SLUG_REGEX,
} from '../../common/consts/regex.const';
import { IUser } from '../interfaces/users.interface';

@Entity({ tableName: 'users' })
export class UserEntity implements IUser {
  @PrimaryKey()
  public id: number;

  @Property({ columnType: 'varchar', length: 100 })
  @IsString()
  @Length(3, 100)
  @Matches(NAME_REGEX, {
    message: 'Name must not have special characters',
  })
  public name: string;

  @Property({ columnType: 'varchar', length: 106 })
  @IsString()
  @Length(3, 106)
  @Matches(SLUG_REGEX, {
    message: 'Username must be a valid slugs',
  })
  public username: string;

  @Property({ columnType: 'varchar', length: 255 })
  @IsString()
  @IsEmail()
  @Length(5, 255)
  public email: string;

  @Property({ columnType: 'boolean', default: false })
  @IsBoolean()
  public confirmed: true | false = false; // since it is saved on the db as binary

  @Property({ columnType: 'varchar', length: 60 })
  @IsString()
  @Length(59, 60)
  @Matches(BCRYPT_HASH)
  public password: string;

  @Property({ onCreate: () => new Date() })
  public createdAt: Date = new Date();

  @Property({ onUpdate: () => new Date() })
  public updatedAt: Date = new Date();
}
```

Add the entity to `users.module.ts` and export the `UserService`:

```ts
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import { UserEntity } from './entities/user.entity';
import { UsersService } from './users.service';

@Module({
  imports: [MikroOrmModule.forFeature([UserEntity])],
  providers: [UsersService],
  exports: [UserService]
})
export class UsersModule {}
```

#### User versioning

For security purpose we need to be able to version our users credentials (password changes for example), so in case they change any credential we can revoke all refresh tokens.

We do this by creating a `Credentials` JSON parameter on our users. Start by creating its interface:

```ts
// credentials.interface.ts

export interface ICredentials {
  version: number;
  lastPassword: string;
  passwordUpdatedAt: number;
  updatedAt: number;
}
```

On a new `embeddables` folder for our JSON types add a credentials embeddable:

```ts
import { Embeddable, Property } from '@mikro-orm/core';
import dayjs from 'dayjs';
import { ICredentials } from '../interfaces/credentials.interface';

@Embeddable()
export class CredentialsEmbeddable implements ICredentials {
  @Property({ default: 0 })
  public version = 0;

  @Property({ default: '' })
  public lastPassword = '';

  @Property({ default: dayjs().unix() })
  public passwordUpdatedAt: number = dayjs().unix();

  @Property({ default: dayjs().unix() })
  public updatedAt: number = dayjs().unix();

  public updatePassword(password: string): void {
    this.version++;
    this.lastPassword = password;
    const now = dayjs().unix();
    this.passwordUpdatedAt = now;
    this.updatedAt = now;
  }

  public updateVersion(): void {
    this.version++;
    this.updatedAt = dayjs().unix();
  }
}
```

And update our users interface and entity:

```ts
// user.interface.ts

import { ICredentials } from './credentials.interface';

export interface IUser {
  // ...
  credentials: ICredentials;
  // ...
}

// user.entity.ts

import { Embedded, Entity, PrimaryKey, Property } from '@mikro-orm/core';
// ...
import { IUser } from '../interfaces/users.interface';

@Entity({ tableName: 'users' })
export class UserEntity implements IUser {
  // ...

  @Embedded(() => CredentialsEmbeddable)
  public credentials: CredentialsEmbeddable = new CredentialsEmbeddable();

  // ...
}
```

### User Service

User service will mosly cover our User CRUD operations, inject the `usersRepository` with `@InjectRepository` decorator and the `CommonService`:

```ts
import { InjectRepository } from '@mikro-orm/nestjs';
import { EntityRepository } from '@mikro-orm/postgresql';
import { Injectable } from '@nestjs/common';
import { CommonService } from '../common/common.service';
import { UserEntity } from './entities/user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(UserEntity)
    private readonly usersRepository: EntityRepository<UserEntity>,
    private readonly commonService: CommonService,
  ) {}
}
```

#### CRUD Operations

_User Creations_

To create a user we need three params:

- name: the name of the user;
- email: an all lowercased unique email;
- password: the user password (note this field will be optional when we add external providers).

```ts
// ...

@Injectable()
export class UsersService {
  // ...

  public async create(
    email: string,
    name: string,
    password: string,
  ): Promise<UserEntity> {
    const formattedEmail = email.toLowerCase();
    await this.checkEmailUniqueness(formattedEmail);
    const formattedName = this.commonService.formatName(name);
    const user = this.usersRepository.create({
      email: formattedEmail,
      name: formattedName,
      username: await this.generateUsername(formattedName),
      password: await hash(password, 10),
    });
    await this.commonService.saveEntity(this.usersRepository, user, true);
    return user;
  }

  // ...

  private async checkEmailUniqueness(email: string): Promise<void> {
    const count = await this.usersRepository.count({ email });

    if (count > 0) {
      throw new ConflictException('Email already in use');
    }
  }

  /**
   * Generate Username
   *
   * Generates a unique username using a point slug based on the name
   * and if it's already in use, it adds the usernames count to the end
   */
  private async generateUsername(name: string): Promise<string> {
    const pointSlug = this.commonService.generatePointSlug(name);
    const count = await this.usersRepository.count({
      username: {
        $like: `${pointSlug}%`,
      },
    });

    if (count > 0) {
      return `${pointSlug}${count}`;
    }

    return pointSlug;
  }

  // ...
}
```

_User Reads_

We need three read methods:

1. ID: the main read methods that fetches a user by ID
    ```ts
    // ...

    @Injectable()
    export class UsersService {
      // ...

      public async findOneById(id: number): Promise<UserEntity> {
        const user = await this.usersRepository.findOne({ id });
        this.commonService.checkEntityExistence(user, 'User');
        return user;
      }

      // ...
    }
    ```
2. Email: mostly for authentication fetches a user by email
    ```ts
    import {
      // ...
      UnauthorizedException,
    } from '@nestjs/common';
    // ...
    import { isNull, isUndefined } from '../common/utils/validation.util';

    @Injectable()
    export class UsersService {
      // ...

      public async findOneByEmail(email: string): Promise<UserEntity> {
        const user = await this.usersRepository.findOne({
          email: email.toLowerCase(),
        });
        this.throwUnauthorizedException(user);
        return user;
      }

      // necessary for password reset
      public async uncheckedUserByEmail(email: string): Promise<UserEntity> {
        return this.usersRepository.findOne({
          email: email.toLowerCase(),
        });
      }

      // ...

      private throwUnauthorizedException(
        user: undefined | null | UserEntity,
      ): void {
        if (isUndefined(user) || isNull(user)) {
          throw new UnauthorizedException('Invalid credentials');
        }
      }
  
      // ...
    }
    ```
3. Credentials: for token generation and verification
    ```ts
    // ...

    @Injectable()
    export class UsersService {
      // ...

      public async findOneByCredentials(
        id: number,
        version: number,
      ): Promise<UserEntity> {
        const user = await this.usersRepository.findOne({ id });
        this.throwUnauthorizedException(user);

        if (user.credentials.version !== version) {
          throw new UnauthorizedException('Invalid credentials');
        }

        return user;
      }

      // ...
    }
    ```
4. Username: for both fetching the user and for authentication
    ```ts
    // ...

    @Injectable()
    export class UsersService {
      // ...

      public async findOneByUsername(
        username: string,
        forAuth = false,
      ): Promise<UserEntity> {
        const user = await this.usersRepository.findOne({
          username: username.toLowerCase(),
        });

        if (forAuth) {
          this.throwUnauthorizedException(user);
        } else {
          this.commonService.checkEntityExistence(user, 'User');
        }

        return user;
      }

      // ...
    }
    ```

_User Update_

Before creating the updates we need to create some `dtos` (Data Transfer Objects). One for changing the email:

```ts
// change-email.dto.ts

import { IsEmail, IsString, Length, MinLength } from 'class-validator';

export abstract class ChangeEmailDto {
  @IsString()
  @MinLength(1)
  public password!: string;

  @IsString()
  @IsEmail()
  @Length(5, 255)
  public email: string;
}
```

And one for changing the rest of the user:

```ts
// update-user.dto.ts

import { IsString, Length, Matches, ValidateIf } from 'class-validator';
import { NAME_REGEX, SLUG_REGEX } from '../../common/consts/regex.const';
import { isNull, isUndefined } from '../../common/utils/validation.util';

export abstract class UpdateUserDto {
  @IsString()
  @Length(3, 106)
  @Matches(SLUG_REGEX, {
    message: 'Username must be a valid slugs',
  })
  @ValidateIf(
    (o: UpdateUserDto) =>
      !isUndefined(o.username) || isUndefined(o.name) || isNull(o.name),
  )
  public username?: string;

  @IsString()
  @Length(3, 100)
  @Matches(NAME_REGEX, {
    message: 'Name must not have special characters',
  })
  @ValidateIf(
    (o: UpdateUserDto) =>
      !isUndefined(o.name) || isUndefined(o.username) || isNull(o.username),
  )
  public name?: string;
}
```

Since user is a crucial entity on our API, I like to divide the updates in serveral methods, hence endpoints/mutations:

1. User Update:
    ```ts
    // ...
    import { UsernameDto } from './dtos/username.dto';

    @Injectable()
    export class UsersService {
      // ...

      public async update(userId: number, dto: UpdateUserDto): Promise<UserEntity> {
        const user = await this.findOneById(userId);
        const { name, username } = dto;

        if (!isUndefined(name) && !isNull(name)) {
          if (name === user.name) {
            throw new BadRequestException('Name must be different');
          }

          user.name = this.commonService.formatName(name);
        }
        if (!isUndefined(username) && !isNull(username)) {
          const formattedUsername = dto.username.toLowerCase();

          if (user.username === formattedUsername) {
            throw new BadRequestException('Username should be different');
          }

          await this.checkUsernameUniqueness(formattedUsername);
          user.username = formattedUsername;
        }

        await this.commonService.saveEntity(this.usersRepository, user);
        return user;
      }


      // ...

      private async checkUsernameUniqueness(username: string): Promise<void> {
        const count = await this.usersRepository.count({ username });

        if (count > 0) {
          throw new ConflictException('Username already in use');
        }
      }

      // ...
    }
    ```
2. Email Update:
    ```ts
    // ...
    import { ChangeEmailDto } from './dtos/change-email.dto';

    @Injectable()
    export class UsersService {
      // ...

      public async updateEmail(
        userId: number, 
        dto: ChangeEmailDto,
      ): Promise<UserEntity> {
        const user = await this.userById(userId);
        const { email, password } = dto;

        if (!(await compare(password, user.password))) {
          throw new BadRequestException('Invalid password');
        }

        const formattedEmail = email.toLowerCase();
        await this.checkEmailUniqueness(formattedEmail);
        user.credentials.updateVersion();
        user.email = formattedEmail;
        await this.commonService.saveEntity(this.usersRepository, user);
        return user;
      }

      // ...
    }
    ```
3. Password Update and Reset:
    ```ts
    // ...
    import { compare, hash } from 'bcrypt';
    // ...

    @Injectable()
    export class UsersService {
      // ...

      public async updatePassword(
        userId: number,
        password: string,
        newPassword: string,
      ): Promise<UserEntity> {
        const user = await this.userById(userId);

        if (!(await compare(password, user.password))) {
          throw new BadRequestException('Wrong password');
        }
        if (await compare(newPassword, user.password)) {
          throw new BadRequestException('New password must be different');
        }

        user.credentials.updatePassword(user.password);
        user.password = await hash(newPassword, 10);
        await this.commonService.saveEntity(this.usersRepository, user);
        return user;
      }

      public async resetPassword(
        userId: number,
        version: number,
        password: string,
      ): Promise<UserEntity> {
        const user = await this.findOneByCredentials(userId, version);
        user.credentials.updatePassword(user.password);
        user.password = await hash(password, 10);
        await this.commonService.saveEntity(this.usersRepository, user);
        return user;
      }

      // ...
    }
    ```

_User Removal_

Note that I still return the user with the function, in case we ever need to implement a notification system, feel free to return void:

```ts
// ...

@Injectable()
export class UsersService {
  // ...

  public async remove(userId: number): Promise<UserEntity> {
    const user = await this.findOneById(userId);
    await this.commonService.removeEntity(this.usersRepository, user);
    return user;
  }

  // ...
}
```

## JWT module

Although nestjs has its own `JwtService`, we need a custom one for the various types of tokens we have:

```bash
$ nest g mo jwt
$ nest g s jwt
```

And export the service from the module:

```ts
import { Module } from '@nestjs/common';
import { JwtService } from './jwt.service';

@Module({
  providers: [JwtService],
  exports: [JwtService],
})
export class JwtModule {}
```

### Token Types

#### Enum

Create a `enums` folder and add the following enum:

```ts
export enum TokenTypeEnum {
  ACCESS = 'access',
  REFRESH = 'refresh',
  CONFIRMATION = 'confirmation',
  RESET_PASSWORD = 'resetPassword',
}
```

#### Interfaces

Each token extends from the previous, create an `interfaces` folder and add one interface for each type of token.

_Base Token_

All tokens will have an `iat` (issued at), `exp` (expiration), `iss` (issuer), `aud` (audience) and`sub` (subject) field so we need a base for all our tokens:

```ts
// token-base.interface.ts

export interface ITokenBase {
  iat: number;
  exp: number;
  iss: string;
  aud: string;
  sub: string;
}
```

_Access Token_

The access token will only contain the id of an user:

```ts
// access-token.interface.ts

import { ITokenBase } from './token-base.interface';

export interface IAccessPayload {
  id: number;
}

export interface IAccessToken extends IAccessPayload, ITokenBase {}

```

_Email Token_

The email token will contain the id and the version of an user:

```ts
// email-token.interface.ts

import { IAccessPayload } from './access-token.interface';
import { ITokenBase } from './token-base.interface';

export interface IEmailPayload extends IAccessPayload {
  version: number;
}

export interface IEmailToken extends IEmailPayload, ITokenBase {}
```

_Refresh Token_

The refresh token will contain the id and the version of an user, as well as a uuid as the identifier of the token:

```ts
// refresh-token.interface.ts

import { IEmailPayload } from './email-token.interface';
import { ITokenBase } from './token-base.interface';

export interface IRefreshPayload extends IEmailPayload {
  tokenId: string;
}

export interface IRefreshToken extends IRefreshPayload, ITokenBase {}
```

### Service

Start by injecting the `ConfigService` and `CommonService`:

```ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { CommonService } from '../common/common.service';

@Injectable()
export class JwtService {
  private readonly jwtConfig: IJwt;
  private readonly issuer: string;
  private readonly domain: string;

  constructor(
    private readonly configService: ConfigService,
    private readonly commonService: CommonService,
  ) {}
}
```

Since the jsonwebtoken library still uses callback for asynchronous behaviour lets create asynchronous sign and verify functions:

```ts
// ...
import * as jwt from 'jsonwebtoken';
import { IAccessPayload } from './interfaces/access-token.interface';
import { IEmailPayload } from './interfaces/email-token.interface';
import { IRefreshToken } from './interfaces/refresh-token.interface';

@Injectable()
export class JwtService {
  // ...

  private static async generateTokenAsync(
    payload: IAccessPayload | IEmailPayload | IRefreshPayload,
    secret: string,
    options: jwt.SignOptions,
  ): Promise<string> {
    return new Promise((resolve, rejects) => {
      jwt.sign(payload, secret, options, (error, token) => {
        if (error) {
          rejects(error);
          return;
        }
        resolve(token);
      });
    });
  }

  private static async verifyTokenAsync<T>(
    token: string,
    secret: string,
    options: jwt.VerifyOptions,
  ): Promise<T> {
    return new Promise((resolve, rejects) => {
      jwt.verify(token, secret, options, (error, payload: T) => {
        if (error) {
          rejects(error);
          return;
        }
        resolve(payload);
      });
    });
  }
}
```

Start setting up the jwt configuration and domain:

```ts
// ...
import { IJwt } from '../config/interfaces/jwt.interface';
// ...

@Injectable()
export class JwtService {
  private readonly jwtConfig: IJwt;
  private readonly issuer: string;
  private readonly domain: string;

  constructor(
    private readonly configService: ConfigService,
    private readonly commonService: CommonService,
  ) {
    this.jwtConfig = this.configService.get<IJwt>('jwt');
    this.issuer = this.configService.get<string>('id');
    this.domain = this.configService.get<string>('domain');
  }

  // ...
}
```

Create a method for generating tokens using `IUser` and `TokenTypesEnum` as params:

```ts
// ...
import { v4 } from 'uuid';
import { IJwt } from '../config/interfaces/jwt.interface';
import { IUser } from '../users/interfaces/user.interface';
// ...

@Injectable()
export class JwtService {
  // ...

  public async generateToken(
    user: IUser,
    tokenType: TokenTypeEnum,
    domain?: string | null,
    tokenId?: string,
  ): Promise<string> {
    const jwtOptions: jwt.SignOptions = {
      issuer: this.issuer,
      subject: user.email,
      audience: domain ?? this.domain,
      algorithm: 'HS256', // only needs a secret
    };

    switch (tokenType) {
      case TokenTypeEnum.ACCESS:
        const { privateKey, time: accessTime } = this.jwtConfig.access;
        return this.commonService.throwInternalError(
          JwtService.generateTokenAsync({ id: user.id }, privateKey, {
            ...jwtOptions,
            expiresIn: accessTime,
            algorithm: 'RS256', // to use public and private key
          }),
        );
      case TokenTypeEnum.REFRESH:
        const { secret: refreshSecret, time: refreshTime } =
          this.jwtConfig.refresh;
        return this.commonService.throwInternalError(
          JwtService.generateTokenAsync(
            {
              id: user.id,
              version: user.credentials.version,
              tokenId: tokenId ?? v4(),
            },
            refreshSecret,
            {
              ...jwtOptions,
              expiresIn: refreshTime,
            },
          ),
        );
      case TokenTypeEnum.CONFIRMATION:
      case TokenTypeEnum.RESET_PASSWORD:
        const { secret, time } = this.jwtConfig[tokenType];
        return this.commonService.throwInternalError(
          JwtService.generateTokenAsync(
            { id: user.id, version: user.credentials.version },
            secret,
            {
              ...jwtOptions,
              expiresIn: time,
            },
          ),
        );
    }
  }
}
```

And then create a method to verify and decode our tokens:

```ts
import {
  BadRequestException,
  Injectable,
  InternalServerErrorException,
} from '@nestjs/common';
// ...
import {
  // ...
  IAccessToken,
} from './interfaces/access-token.interface';
import { IEmailPayload, IEmailToken } from './interfaces/email-token.interface';
import {
  // ...
  IRefreshToken,
} from './interfaces/refresh-token.interface';
// ...

@Injectable()
export class JwtService {
  // ...

  private static async throwBadRequest<
    T extends IAccessToken | IRefreshToken | IEmailToken,
  >(promise: Promise<T>): Promise<T> {
    try {
      return await promise;
    } catch (error) {
      if (error instanceof jwt.TokenExpiredError) {
        throw new BadRequestException('Token expired');
      }
      if (error instanceof jwt.JsonWebTokenError) {
        throw new BadRequestException('Invalid token');
      }
      throw new InternalServerErrorException(error);
    }
  }

  // ...

  public async verifyToken<
    T extends IAccessToken | IRefreshToken | IEmailToken,
  >(token: string, tokenType: TokenTypeEnum): Promise<T> {
    const jwtOptions: jwt.VerifyOptions = {
      issuer: this.issuer,
      audience: new RegExp(this.domain),
    };

    switch (tokenType) {
      case TokenTypeEnum.ACCESS:
        const { publicKey, time: accessTime } = this.jwtConfig.access;
        return JwtService.throwBadRequest(
          JwtService.verifyTokenAsync(token, publicKey, {
            ...jwtOptions,
            maxAge: accessTime,
            algorithms: ['RS256'],
          }),
        );
      case TokenTypeEnum.REFRESH:
      case TokenTypeEnum.CONFIRMATION:
      case TokenTypeEnum.RESET_PASSWORD:
        const { secret, time } = this.jwtConfig[tokenType];
        return JwtService.throwBadRequest(
          JwtService.verifyTokenAsync(token, secret, {
            ...jwtOptions,
            maxAge: time,
            algorithms: ['HS256'],
          }),
        );
    }
  }
}
```

## Mailer Module

To confirm our users identity, and to be able to reset passwords we need to send emails. Start by installing nodemailer and handlebars:

```bash
$ yarn add nodemailer handlebars
$ yarn add @types/nodemailer @types/handlebars
```

Create a mailer module and service:

```bash
$ nest g mo mailer
$ nest g s mailer
```

And export the service from the module:

```ts
import { Module } from '@nestjs/common';
import { MailerService } from './mailer.service';

@Module({
  providers: [MailerService],
  exports: [MailerService],
})
export class MailerModule {}
```

### Templates

We will use handlebars to create templates for confirmation and password reseting.

#### Interfaces

Create an `interfaces` folder and add an interface for the template data:

```ts
// template-data.interface.ts

export interface ITemplatedData {
  name: string;
  link: string;
}
```

And one for the templates we will have:

```ts
// templates.interface.ts

import { TemplateDelegate } from 'handlebars';
import { ITemplatedData } from './template-data.interface';

export interface ITemplates {
  confirmation: TemplateDelegate<ITemplatedData>;
  resetPassword: TemplateDelegate<ITemplatedData>;
}
```

#### HTML (hbs)

Create an email template for confirmation:

```hbs
<!-- confirmation.hbs -->
<html lang='en'>
  <body>
    <p>Hello {{name}},</p>
    <br />
    <p>Welcome to [Your app],</p>
    <p>
      Click
      <b><a href='{{link}}' target='_blank'>here</a></b>
      to activate your acount or go to this link:
      {{link}}
    </p>
    <p><small>This link will expire in an hour.</small></p>
    <br />
    <p>Best of luck,</p>
    <p>[Your app] Team</p>
  </body>
</html>
```

And one for password reseting:

```hbs
<!-- reset-password.hbs -->
<html lang='en'>
  <body>
    <p>Hello {{name}},</p>
    <br />
    <p>Your password reset link:
      <b><a href='{{link}}' target='_blank'>here</a></b></p>
    <p>Or go to this link: ${{link}}</p>
    <p><small>This link will expire in 30 minutes.</small></p>
    <br />
    <p>Best regards,</p>
    <p>[Your app] Team</p>
  </body>
</html> 
```

To compile the templates you need to add an `assets` on `nest-cli.json`:

```json
{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "assets": [
      "mailer/templates/**/*"
    ],
    "watchAssets": true
  }
}
```

### Service

Start by importing the `ConfigService`:

```ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class MailerService {
  constructor(private readonly configService: ConfigService) {}
}
```

And add the email client configuration, as well as a logger:

```ts
import { Injectable, Logger, LoggerService } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createTransport, Transporter } from 'nodemailer';
import SMTPTransport from 'nodemailer/lib/smtp-transport';

@Injectable()
export class MailerService {
  private readonly loggerService: LoggerService;
  private readonly transport: Transporter<SMTPTransport.SentMessageInfo>;
  private readonly email: string;
  private readonly domain: string;

  constructor(private readonly configService: ConfigService) {
    const emailConfig = this.configService.get<IEmailConfig>('emailService');
    this.transport = createTransport(emailConfig);
    this.email = `"My App" <${emailConfig.auth.user}>`;
    this.domain = this.configService.get<string>('domain');
    this.loggerService = new Logger(MailerService.name);
  }
}
```

As you can see we have not added our templates yet, start by creating a parser method:

```ts
// ...
import { readFileSync } from 'fs';
import Handlebars from 'handlebars';
// ...
import { ITemplatedData } from './interfaces/template-data.interface';

@Injectable()
export class MailerService {
  // ...

  private static parseTemplate(
    templateName: string,
  ): Handlebars.TemplateDelegate<ITemplatedData> {
    const templateText = readFileSync(
      join(__dirname, 'templates', templateName),
      'utf-8',
    );
    return Handlebars.compile<ITemplatedData>(templateText, { strict: true });
  }
}
```

And add our templates to the configuration:

```ts
// ...
import { ITemplates } from './interfaces/templates.interface';

@Injectable()
export class MailerService {
  // ...
  private readonly templates: ITemplates;

  constructor(private readonly configService: ConfigService) {
    this.templates = {
      confirmation: MailerService.parseTemplate('confirmation.hbs'),
      resetPassword: MailerService.parseTemplate('reset-password.hbs'),
    };
  }

  // ...
}
```

Emails should be sent asynchronously so create a public method that uses the `.then` notation:

```ts
// ...

@Injectable()
export class MailerService {
  // ...
  
  public sendEmail(
    to: string,
    subject: string,
    html: string,
    log?: string,
  ): void {
    this.transport
      .sendMail({
        from: this.email,
        to,
        subject,
        html,
      })
      .then(() => this.loggerService.log(log ?? 'A new email was sent.'))
      .catch((error) => this.loggerService.error(error));
  }
}
```

And a method for each of our two templates:

```ts
// ...
import { IUser } from '../users/interfaces/user.interface';


@Injectable()
export class MailerService {
  // ...
  
  public sendConfirmationEmail(user: IUser, token: string): void {
    const { email, name } = user;
    const subject = 'Confirm your email';
    const html = this.templates.confirmation({
      name,
      link: `https://${this.domain}/auth/confirm/${token}`,
    });
    this.sendEmail(email, subject, html, 'A new confirmation email was sent.');
  }

  public sendResetPasswordEmail(user: IUser, token: string): void {
    const { email, name } = user;
    const subject = 'Reset your password';
    const html = this.templates.resetPassword({
      name,
      link: `https://${this.domain}/auth/reset-password/${token}`,
    });
    this.sendEmail(
      email,
      subject,
      html,
      'A new reset password email was sent.',
    );
  }

  // ...
}
```

## Auth Module

Create the auth module:

```bash
$ nest g mo auth
$ nest g s auth
```

### Entities

The auth module will only have one entity, the blacklisted tokens, create its interface on the `interfaces` folder:

```ts
// blacklisted-token.interface.ts

import { IUser } from '../../users/interfaces/user.interface';

export interface IBlacklistedToken {
  tokenId: string;
  user: IUser;
  createdAt: Date;
}
```

Now just implement it on a entity on the `entities` folder:

```ts
// blacklisted-token.entity.ts

import {
  Entity,
  ManyToOne,
  PrimaryKeyType,
  Property,
  Unique,
} from '@mikro-orm/core';
import { UserEntity } from '../../users/entities/user.entity';
import { IBlacklistedToken } from '../interfaces/blacklisted-token.interface';

@Entity({ tableName: 'blacklisted_tokens' })
@Unique({ properties: ['tokenId', 'user'] })
export class BlacklistedTokenEntity implements IBlacklistedToken {
  @Property({
    primary: true,
    columnType: 'uuid',
  })
  public tokenId: string;

  @ManyToOne({
    entity: () => UserEntity,
    onDelete: 'cascade',
    primary: true,
  })
  public user: UserEntity;

  @Property({ onCreate: () => new Date() })
  public createdAt: Date;

  [PrimaryKeyType]: [string, number];
}
```

It has a composite key of the `tokenId` and the `user` ID.

Add the entity to the module, as well as the `UserModel`, `JwtModule` and `MailerModule`:

```ts
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import { JwtModule } from '../jwt/jwt.module';
import { MailerModule } from '../mailer/mailer.module';
import { UsersModule } from '../users/users.module';
import { AuthService } from './auth.service';
import { BlacklistedTokenEntity } from './entity/blacklisted-token.entity';

@Module({
  imports: [
    MikroOrmModule.forFeature([BlacklistedTokenEntity]),
    UsersModule,
    JwtModule,
    MailerModule,
  ],
  providers: [AuthService],
})
export class AuthModule {}
```

### Service

Start by injecting the `blacklistedTokensRepository`, `CommonService`, `UsersService`, `JwtService` and`MailerService`:

```ts
import { InjectRepository } from '@mikro-orm/nestjs';
import { EntityRepository } from '@mikro-orm/postgresql';
import { Injectable } from '@nestjs/common';
import { CommonService } from '../common/common.service';
import { JwtService } from '../jwt/jwt.service';
import { MailerService } from '../mailer/mailer.service';
import { UsersService } from '../users/users.service';
import { BlacklistedTokenEntity } from './entity/blacklisted-token.entity';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(BlacklistedTokenEntity)
    private readonly blacklistedTokensRepository: EntityRepository<BlacklistedTokenEntity>,
    private readonly commonService: CommonService,
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService,
    private readonly mailerService: MailerService,
  ) {}
}
```

#### DTOs

We need several dtos, so create a `dtos` directory, with the following files:

_Passwords DTO_

We need two passwords paramenters one as the main password and a confirmation password for registration and password updates.

```ts
// passwords.dto.ts

import { IsString, Length, Matches, MinLength } from 'class-validator';
import { PASSWORD_REGEX } from '../../common/consts/regex.const';

export abstract class PasswordsDto {
  @IsString()
  @Length(8, 35)
  @Matches(PASSWORD_REGEX, {
    message:
      'Password requires a lowercase letter, an uppercase letter, and a number or symbol',
  })
      public password1!: string;

      @IsString()
      @MinLength(1)
      public password2!: string;
}
```

_Sign-up DTO_

For registration.

```ts
// sign-up.dto.ts

import { IsEmail, IsString, Length, Matches } from 'class-validator';
import { NAME_REGEX } from '../../common/consts/regex.const';
import { PasswordsDto } from './passwords.dto';

export abstract class SignUpDto extends PasswordsDto {
  @IsString()
  @Length(3, 100, {
    message: 'Name has to be between 3 and 50 characters.',
  })
  @Matches(NAME_REGEX, {
    message: 'Name can only contain letters, dtos, numbers and spaces.',
  })
  public name!: string;

  @IsString()
  @IsEmail()
  @Length(5, 255)
  public email!: string;
}
```

_Sign-in DTO_

For login, it can take the email or username.

```ts
// sign-in.dto.ts

import { IsEmail, IsString, Length, Matches } from 'class-validator';
import { NAME_REGEX } from '../../common/consts/regex.const';
import { PasswordsDto } from './passwords.dto';

export abstract class SignUpDto extends PasswordsDto {
  @IsString()
  @Length(3, 100, {
    message: 'Name has to be between 3 and 50 characters.',
  })
  @Matches(NAME_REGEX, {
    message: 'Name can only contain letters, dtos, numbers and spaces.',
  })
  public name!: string;

  @IsString()
  @IsEmail()
  @Length(5, 255)
  public email!: string;
}
```

_Email DTO_

A dto with only the email for sending password reset emails.

```ts
// email.dto.ts

export abstract class EmailDto {
  @IsString()
  @IsEmail()
  @Length(5, 255)
  public email: string;
}
```

_Reset Password DTO_

For reseting the password given a token.

```ts
// reset-password.dto.ts

import { IsJWT, IsString } from 'class-validator';
import { PasswordsDto } from './passwords.dto';

export abstract class ResetPasswordDto extends PasswordsDto {
  @IsString()
  @IsJWT()
  public resetToken!: string;
}
```

_Change Password DTO_ 

For updating the user password.

```ts
// change-password.dto.ts

import { IsString, MinLength } from 'class-validator';
import { PasswordsDto } from './passwords.dto';

export abstract class ChangePasswordDto extends PasswordsDto {
  @IsString()
  @MinLength(1)
  public password!: string;
}
```

#### Interfaces

Most authentication service methods will return the same three fields:

- User;
- Access Token;
- Refresh Token.

So create an interface for that called `IAuthResult`:

```ts
// auth-result.interface.ts

import { IUser } from '../../users/interfaces/user.interface';

export interface IAuthResult {
  user: IUser;
  accessToken: string;
  refreshToken: string;
}
```

#### Methods

We will start by creating a private method for faster generation of the access and refresh token by leveraging `Promise.all`:

```ts
// ...

@Injectable()
export class AuthService {
  // ...

  private async generateAuthTokens(
    user: UserEntity,
    domain?: string,
    tokenId?: string,
  ): Promise<[string, string]> {
    return Promise.all([
      this.jwtService.generateToken(
        user,
        TokenTypeEnum.ACCESS,
        domain,
        tokenId,
      ),
      this.jwtService.generateToken(
        user,
        TokenTypeEnum.REFRESH,
        domain,
        tokenId,
      ),
    ]);
  } 
}
```

_Sign Up Method_

```ts
// ...
import { SignUpDto } from './dtos/sign-up.dto';
// ...

@Injectable()
export class AuthService {
  // ...

  public async signUp(dto: SignUpDto, domain?: string): Promise<IMessage> {
    const { name, email, password1, password2 } = dto;
    this.comparePasswords(password1, password2);
    const user = await this.usersService.create(email, name, password1);
    const confirmationToken = await this.jwtService.generateToken(
      user,
      TokenTypeEnum.CONFIRMATION,
      domain,
    );
    this.mailerService.sendConfirmationEmail(user, confirmationToken);
    return this.commonService.generateMessage('Registration successful');
  }

  private comparePasswords(password1: string, password2: string): void {
    if (password1 !== password2) {
      throw new BadRequestException('Passwords do not match');
    }
  }

  // ...
}
```

_Sign In Method_

```ts
// ...
import {
  // ...
  UnauthorizedException,
} from '@nestjs/common';
import { compare } from 'bcrypt';
import { isEmail } from 'class-validator';
import { SLUG_REGEX } from '../common/consts/regex.const';
// ...
import { IAuthResult } from './interfaces/auth-result.interface';

@Injectable()
export class AuthService {
  // ...

  public async singIn(dto: SignInDto, domain?: string): Promise<IAuthResult> {
    const { emailOrUsername, password } = dto;
    const user = await this.userByEmailOrUsername(emailOrUsername);

    if (!(await compare(password, user.password))) {
      await this.checkLastPassword(user.credentials, password);
    }
    if (!user.confirmed) {
      const confirmationToken = await this.jwtService.generateToken(
        user,
        TokenTypeEnum.CONFIRMATION,
        domain,
      );
      this.mailerService.sendConfirmationEmail(user, confirmationToken);
      throw new UnauthorizedException(
        'Please confirm your email, a new email has been sent',
      );
    }

    const [accessToken, refreshToken] = await this.generateAuthTokens(
      user,
      domain,
    );
    return { user, accessToken, refreshToken };
  }

  // validates the input and fetches the user by email or username
  private async userByEmailOrUsername(
    emailOrUsername: string,
  ): Promise<UserEntity> {
    if (emailOrUsername.includes('@')) {
      if (!isEmail(emailOrUsername)) {
        throw new BadRequestException('Invalid email');
      }

      return this.usersService.userByEmail(emailOrUsername);
    }

    if (
       emailOrUsername.length < 3 ||
       emailOrUsername.length > 106 ||
       !SLUG_REGEX.test(emailOrUsername)
     ) {
      throw new BadRequestException('Invalid username');
    }

    return this.usersService.userByUsername(emailOrUsername, true);
  }

  // checks if your using your last password
  private async checkLastPassword(
    credentials: ICredentials,
    password: string,
  ): Promise<void> {
    const { lastPassword, passwordUpdatedAt } = credentials;

    if (lastPassword.length === 0 || !(await compare(password, lastPassword))) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const now = dayjs();
    const time = dayjs.unix(passwordUpdatedAt);
    const months = now.diff(time, 'month');
    const message = 'You changed your password ';

    if (months > 0) {
      throw new UnauthorizedException(
        message + months + (months > 1 ? ' months ago' : ' month ago'),
      );
    }

    const days = now.diff(time, 'day');

    if (days > 0) {
      throw new UnauthorizedException(
        message + days + (days > 1 ? ' days ago' : ' day ago'),
      );
    }

    const hours = now.diff(time, 'hour');

    if (hours > 0) {
      throw new UnauthorizedException(
        message + hours + (hours > 1 ? ' hours ago' : ' hour ago'),
      );
    }

    throw new UnauthorizedException(message + 'recently');
  }

  // ...
}
```

_Refresh Token Access_

```ts
// ...

@Injectable()
export class AuthService {
  // ...

  public async refreshTokenAccess(
    refreshToken: string,
    domain?: string,
  ): Promise<IAuthResult> {
    const { id, version, tokenId } =
      await this.jwtService.verifyToken<IRefreshToken>(
        refreshToken,
        TokenTypeEnum.REFRESH,
      );
    await this.checkIfTokenIsBlacklisted(id, tokenId);
    const user = await this.usersService.userByCredentials(id, version);
    const [accessToken, newRefreshToken] = await this.generateAuthTokens(
      user,
      domain,
      tokenId,
    );
    return { user, accessToken, refreshToken: newRefreshToken };
  }

  // checks if a token given the ID of the user and ID of token exists on the database
  private async checkIfTokenIsBlacklisted(
    userId: number,
    tokenId: string,
  ): Promise<void> {
    const count = await this.blacklistedTokensRepository.count({
      user: userId,
      tokenId,
    });

    if (count > 0) {
      throw new UnauthorizedException('Token is invalid');
    }
  }

  // ...
}
```

_Logout_

```ts
// ...

@Injectable()
export class AuthService {
  // ...

  public async logout(refreshToken: string): Promise<IMessage> {
    const { id, tokenId } = await this.jwtService.verifyToken<IRefreshToken>(
      refreshToken,
      TokenTypeEnum.REFRESH,
    );
    await this.blacklistToken(id, tokenId);
    return this.commonService.generateMessage('Logout successful');
  }

  // creates a new blacklisted token in the database with the
  // ID of the refresh token that was removed with the logout
  private async blacklistToken(userId: number, tokenId: string): Promise<void> {
    const blacklistedToken = this.blacklistedTokensRepository.create({
      user: userId,
      tokenId,
    });
    await this.commonService.saveEntity(
      this.blacklistedTokensRepository,
      blacklistedToken,
      true,
    );
  }

  // ...
}
```

_Reset Password Email_

```ts
// ...
import { isNull, isUndefined } from '../common/utils/validation.util';

@Injectable()
export class AuthService {
  // ...

  public async resetPasswordEmail(
    dto: EmailDto,
    domain?: string,
  ): Promise<IMessage> {
    const user = await this.usersService.uncheckedUserByEmail(dto.email);

    if (!isUndefined(user) && !isNull(user)) {
      const resetToken = await this.jwtService.generateToken(
        user,
        TokenTypeEnum.RESET_PASSWORD,
        domain,
      );
      this.mailerService.sendResetPasswordEmail(user, resetToken);
    }

    return this.commonService.generateMessage('Reset password email sent');
  }

  // ...
}
```

_Reset Password_

```ts
// ...

@Injectable()
export class AuthService {
  // ...

  public async resetPassword(dto: ResetPasswordDto): Promise<IMessage> {
    const { password1, password2, resetToken } = dto;
    const { id, version } = await this.jwtService.verifyToken<IEmailToken>(
      resetToken,
      TokenTypeEnum.RESET_PASSWORD,
    );
    this.comparePasswords(password1, password2);
    await this.usersService.resetPassword(id, version, password1);
    return this.commonService.generateMessage('Password reset successful');
  }

  // ...
}
```

_Change Password_

```ts
// ...

@Injectable()
export class AuthService {
  // ...

  public async changePassword(
    userId: number,
    dto: ChangePasswordDto,
  ): Promise<IAuthResult> {
    const { password1, password2, password } = dto;
    this.comparePasswords(password1, password2);
    const user = await this.usersService.updatePassword(
      userId,
      password,
      password1,
    );
    const [accessToken, refreshToken] = await this.generateAuthTokens(user);
    return { user, accessToken, refreshToken };
  }

  // ...
}
```

## Optional Section

### Optimizations with Redis Cache

Since tokens expire saving them on disk permanently can be quite wasteful, so it is better to save them on cache. NestJS comes with its own `CacheModule` so start by installing the following packages:

```bash
$ yarn add cache-manager ioredis cache-manager-redis-yet
```

#### Configuration

On the `config.interface.ts` add the ioredis options:

```ts
// ...
import { RedisOptions } from 'ioredis';

export interface IConfig {
  // ...
  redis: RedisOptions;
}
```

Now most managed redis services (AWS ElastiCache, Digital Ocean Redis DB, etc) will actually give you an URL and not the options so we need to build a parser, on a new `utils` folder add:

```ts
// redis-url-parser.util.ts

import { RedisOptions } from 'ioredis';

export const redisUrlParser = (url: string): RedisOptions => {
  if (url.includes('://:')) {
    const arr = url.split('://:')[1].split('@');
    const secondArr = arr[1].split(':');

    return {
      password: arr[0],
      host: secondArr[0],
      port: parseInt(secondArr[1], 10),
    };
  }

  const connectionString = url.split('://')[1];
  const arr = connectionString.split(':');
  return {
    host: arr[0],
    port: parseInt(arr[1], 10),
  };
};
```

Add the redis URL to the `.env` file (and on docker-compose if you are using it), the schema and the `index` file:

```env
REDIS_URL='redis://localhost:6379'
```

```ts
// index.ts

// ...
import { redisUrlParser } from './utils/redis-url-parser.util';

export function config(): IConfig {
  // ...

  return {
    // ...
    redis: redisUrlParser(process.env.REDIS_URL),
  };
}

// config.schema.ts

import Joi from 'joi';

export const validationSchema = Joi.object({
  // ...
  REDIS_URL: Joi.string().required(),
});
```

Finaly to be able to use the `CacheModule` we need to create a config class for the cache:

```ts
// cache.config.ts

import {
  CacheModuleOptions,
  CacheOptionsFactory,
  Injectable,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { redisStore } from 'cache-manager-ioredis-yet';

@Injectable()
export class CacheConfig implements CacheOptionsFactory {
  constructor(private readonly configService: ConfigService) {}

  async createCacheOptions(): Promise<CacheModuleOptions> {
    return {
      store: await redisStore({
        ...this.configService.get('redis'),
        ttl: this.configService.get<number>('jwt.refresh.time') * 1000,
      }),
    };
  }
}
```

And add it to the `app.module.ts`:

```ts
// ...
import { CacheModule, Module } from '@nestjs/common';
// ...

@Module({
  imports: [
    // ...
    CacheModule.registerAsync({
      isGlobal: true,
      imports: [ConfigModule],
      useClass: CacheConfig,
    }),
    // ...
  ],
  // ...
})
export class AppModule {}
```

#### Auth Module

Start by deleting the `entities` directory, the `blacklisted-token.interface.ts` file, and remove it from the module:

```ts
import { Module } from '@nestjs/common';
import { JwtModule } from '../jwt/jwt.module';
import { MailerModule } from '../mailer/mailer.module';
import { UsersModule } from '../users/users.module';
import { AuthService } from './auth.service';

@Module({
  imports: [UsersModule, JwtModule, MailerModule],
  providers: [AuthService],
})
export class AuthModule {}
```

_Auth Service_

Start by changing the `blacklistedTokensRepository` for the cache manager:

```ts
import {
  // ...
  CACHE_MANAGER,
  // ...
} from '@nestjs/common';
import { Cache } from 'cache-manager';
// ...

@Injectable()
export class AuthService {
  constructor(
    @Inject(CACHE_MANAGER)
    private readonly cacheManager: Cache,
    // ...
  ) {}

  // ...
}
```

Now update the `blacklistToken` method and `logout` as it is dependent on it:

```ts
// ...
import dayjs from 'dayjs';
// ...

@Injectable()
export class AuthService {
  // ...

  public async logout(refreshToken: string): Promise<IMessage> {
    const { id, tokenId, exp } =
      await this.jwtService.verifyToken<IRefreshToken>(
        refreshToken,
        TokenTypeEnum.REFRESH,
      );
    await this.blacklistToken(id, tokenId, exp);
    return this.commonService.generateMessage('Logout successful');
  }

  // ...

  // checks if a blacklist token given a redis key exist on cache
  private async blacklistToken(
    userId: number,
    tokenId: string,
    exp: number,
  ): Promise<void> {
    const now = dayjs().unix();
    const ttl = (exp - now) * 1000;

    if (ttl > 0) {
      await this.commonService.throwInternalError(
        this.cacheManager.set(`blacklist:${userId}:${tokenId}`, now, ttl),
      );
    }
  }

  // ...
}
```

As you can see, we use a typical redis key divided in three parts by a colon:

- Title of key: "blacklist"
- User ID: the id of the user that the token belongs;
- Token ID: the id of the token that is blacklisted.

I save the date of when it was created, but you cant just save 0 or 1 (binary) as true or false.

Also, the way we check for if the token exists changes as well:

```ts
// ...

@Injectable()
export class AuthService {
  // ...

  private async checkIfTokenIsBlacklisted(
    userId: number,
    tokenId: string,
  ): Promise<void> {
    const time = await this.cacheManager.get<number>(
      `blacklist:${userId}:${tokenId}`,
    );

    if (!isUndefined(time) && !isNull(time)) {
      throw new UnauthorizedException('Invalid token');
    }
  }

  // ...
}
```

## Conclusion

With this you can create the base for a full local authentication type of API.

The full code of this tutorial can be find on this [repo](https://github.com/tugascript/nestjs-oauth/tree/configuration-and-operations).

## About the Author

Hey there! I am Afonso Barracha, your go-to econometrician who found his way into the world of back-end development with a soft spot for GraphQL. If you enjoyed reading this article, why not show some love by [buying me a coffee](https://www.buymeacoffee.com/barracha)?

Lately, I have been diving deep into more advanced subjects. As a result, I have switched from sharing my thoughts every week to posting once or twice a month. This way, I can make sure to bring you the highest quality content possible.

Do not miss out on any of my latest articles – follow me here on [dev](https://dev.to/tugascript) and [LinkedIn](https://www.linkedin.com/in/afonso-barracha/) to stay updated. I would be thrilled to welcome you to our ever-growing community! See you around!