Chapter 7

 Authentication

User Signup

Save the user in the database after account creation in the application. In a Nest.js environment, you'll often use a service with injected repository to handle this data-persistence layer. This follows the software engineering principle of separation of concerns, keeping the data storage logic distinct from the business logic.

Install Dependencies

Save the user password in an encrypted format. Utilize the bcryptjs package for password encryption within the Nest.js ecosystem. While it's generally advisable to also use a salt for added security, this project will focus solely on encryption. Install this dependency to proceed.

Encrypting user passwords is crucial for data security and aligns with the software engineering principle of confidentiality. The choice of bcryptjs is notable for its reliable and secure encryption algorithm.

"bcryptjs": "^2.4.3",



You can add this entry into dependencies in the package.json file and run npm install. Install typescript typing for this package

"@types/bcryptjs": "^2.4.2",



Create User and Auth Module

// app.module.ts

import { UsersModule } from './users/users.module';

import { AuthModule } from './auth/auth.module';




@Module({

imports: [

  CatsModule,

  SongsModule,

  PlayListModule,

  UsersModule,

  AuthModule,

],

})





Create two new modules with nest cli.

//auth.module.ts

import { Module } from "@nestjs/common";

import { AuthController } from "./auth.controller";

import { UsersModule } from "src/users/users.module";

import { AuthService } from "./auth.service";



@Module({

  imports: [UsersModule],

  controllers: [AuthController],

  providers: [AuthService],

  exports: [AuthService],

})

export class AuthModule {}



We are exporting the AuthService from AuthModule which means when we import the AuthModule into another module you can use the AuthService or inject the authservice into your imported module. If you don't have an AuthService you can create it using nest-cli

We are also importing the UsersModule here because we need UserService here.

// users.module.ts

import { Module } from "@nestjs/common";

import { UsersService } from "./users.service";

import { TypeOrmModule } from "@nestjs/typeorm";

import { User } from "./user.entity";



@Module({

  imports: [TypeOrmModule.forFeature([User])],

  providers: [UsersService],

  exports: [UsersService],

})

export class UsersModule {}



Creating AuthController

import { Body, Controller, Post } from "@nestjs/common";

import { CreateUserDTO } from "src/users/dto/create-user.dto";

import { User } from "src/users/user.entity";

import { UsersService } from "src/users/users.service";



@Controller("auth")

export class AuthController {

  constructor(private userService: UsersService) {}

  @Post("signup")

  signup(

    @Body()

    userDTO: CreateUserDTO

  ): Promise<User> {

    return this.userService.create(userDTO);

  }

}



We have created a new route for signup to handle the signup request. We did not create a CreateUserDTO and create method inside the UsersService

Creating UsersService

import { Injectable } from "@nestjs/common";

import { InjectRepository } from "@nestjs/typeorm";

import { Repository } from "typeorm";

import { User } from "./user.entity";

import * as bcrypt from "bcryptjs";



import { CreateUserDTO } from "./dto/create-user.dto";



@Injectable()

export class UsersService {

  constructor(

    @InjectRepository(User)

    private userRepository: Repository<User> // 1.

  ) {}



  async create(userDTO: CreateUserDTO): Promise<User> {

    const salt = await bcrypt.genSalt(); // 2.

    userDTO.password = await bcrypt.hash(userDTO.password, salt); // 3.

    const user = await this.userRepository.save(userDTO); // 4.

    delete user.password; // 5.

    return user; // 6.

  }

}



  1. We have imported the User Entity imports: [TypeOrmModule.forFeature([User])], in the UsersModule now we can inject the UsersRepository inside the UsersService.

  2. We have created the salt number to encrypt the password

  3. We have encrypted the password and set it to userDTO password property

  4. You have to save the user by calling the save method from the repository

  5. You don't need to send the user password in the response. You have to delete the user password from the user object

  6. Finally we need to return the user in the response

Create CreateUserDTO

import { IsEmail, IsNotEmpty, IsString } from "class-validator";



export class CreateUserDTO {

  @IsString()

  @IsNotEmpty()

  firstName: string;



  @IsString()

  @IsNotEmpty()

  lastName: string;



  @IsEmail()

  @IsNotEmpty()

  email: string;



  @IsString()

  @IsNotEmpty()

  password: string;

}



Refactor the User Entity

@Entity("users")

export class User {

  @Column({ unique: true })

  email: string;



  @Column()

  @Exclude()

  password: string;

}



When working with TypeORM, there might be cases where you want to exclude one or multiple columns (fields) from being selected. I don't want to send the password in the response that is why I have added Exclude.

Test the Application

### Signup User




POST http://localhost:3000/auth/signup

Content-Type: application/json




{

"firstName": "john",

"lastName": "doe",

"email": "john@gmail.com",

"password": "123456"

}




User Login

What is JSON Web Token Authentication

JSON Web Token (JWT) authentication is a method of securely transmitting information between parties as a JSON object. It is commonly used for authentication and authorization purposes in web applications and APIs.

The flow of JWT authentication involves the following steps:

User Authentication: The user provides their credentials (e.g., username and password) to the authentication server. The server verifies the credentials and generates a JWT if they are valid.

JWT Generation: Upon successful authentication, the authentication server creates a JWT containing three parts: header, payload, and signature.

Header: It typically consists of two parts: the token type, which is JWT, and the hashing algorithm used to create the signature. Payload: This contains the claims or statements about the user, such as their username, role, and any additional information. The payload is not encrypted but is Base64Url encoded. Signature: The signature is created by combining the encoded header, encoded payload, and a secret key known only to the server. It ensures the integrity of the token and prevents tampering. JWT Issuance: The server responds to the user's authentication request by sending the JWT back as a response.

Token Storage: The client (usually a web browser or a mobile app) stores the received JWT securely. It can be stored in various places, such as local storage, cookies, or session storage, depending on the application's requirements.

Token Usage: For subsequent requests to protected resources, the client includes the JWT in the request headers, typically as the "Authorization" header with the "Bearer" scheme, followed by the JWT.

Token Verification: When the server receives a request with a JWT, it extracts the token from the header, payload, and signature.

Signature Validation: The server recalculates the signature using the same algorithm and the secret key. If the recalculated signature matches the signature in the token, it ensures the token's integrity. Expiration Check: The server checks the expiration time (exp) claim in the payload to ensure the token has not expired. If it has expired, the server rejects the request. Additional Validations: The server may perform additional checks based on the application's requirements, such as verifying the token's audience (aud) or checking for revoked tokens. Access Grant: If the token passes all the validations, the server grants access to the requested resource or performs the requested action on behalf of the user.

Token Renewal: If the token has an expiration time, the client can request a new JWT before the current one expires. This process is typically done using a refresh token or by re-authenticating the user.

The JWT authentication flow allows the client to include a token with each request, eliminating the need for server-side session storage. It enables stateless authentication, making it suitable for distributed systems and APIs.

Install Passport

"@nestjs/passport": "^9.0.3",

"passport": "^0.6.0",



You have to install these two dependencies

Create Login Route and Handler

@Post('login')

login(

@Body()

loginDTO: LoginDTO,

) {

return this.authService.login(loginDTO);

}



We have to create a new login route in AuthService. We have called the login method from Authservice. We have not created the login method in AuthService yet, let's create the login method

import { Injectable, UnauthorizedException } from "@nestjs/common";

import { LoginDTO } from "./dto/login.dto";

import { UsersService } from "src/users/users.service";

import * as bcrypt from "bcryptjs";

import { User } from "src/users/user.entity";



@Injectable()

export class AuthService {

  constructor(private userService: UsersService) {}



  async login(loginDTO: LoginDTO): Promise<User> {

    const user = await this.userService.findOne(loginDTO); // 1.

    const passwordMatched = await bcrypt.compare(

      loginDTO.password,

      user.password

    ); // 2.

    if (passwordMatched) {

      // 3.

      delete user.password; // 4.

      return user;

    } else {

      throw new UnauthorizedException("Password does not match"); // 5.

    }

  }

}



  1. We have to find the user based on email. We need to get the email and password from the request body.

  2. We will compare the user password with an encrypted password that we saved in the last video

  3. If the password matches then delete the user password and send the user back in the response. It means the user has logged in successfully

  4. If the password does not match we have to send the error back in the response

Create LoginDTO

You have to create the LoginDTO file inside the auth/dtos/login.dto.ts

// login.dto.ts

import { IsEmail, IsNotEmpty, IsString } from "class-validator";



export class LoginDTO {

  @IsEmail()

  @IsNotEmpty()

  email: string;



  @IsString()

  @IsNotEmpty()

  password: string;

}



Create findOne method inside UsersService

async findOne(data: Partial<User>): Promise<User> {

 const user = await this.userRepository.findOneBy({ email: data.email });

 if (!user) {

 throw new UnauthorizedException('Could not find user');

 }

 return user;

}



Test the Application

### Login User




POST http://localhost:3001/auth/login

Content-Type: application/json




{

"email": "john@gmail.com",

"password": "123456"

}





We did not send the JSON web token back in the response. In the next lesson, I will teach you how to send the JSOn web token in the response when the user has successfully logged in



Authenticate User



 

Find the user from the database based on their email and encrypt their password. If the user logs in successfully, generate a JSON Web Token (JWT) and include it in the response. This is a key step in stateless authentication, a software engineering principle that improves scalability and security. In the previous lesson, the generation and inclusion of the JWT in the response were omitted, which left the authentication process incomplete.

Let's create the JSON web token:

Install Dependencies

"dependencies" : {

  "@nestjs/jwt": "^10.0.3",

  "passport-jwt": "^4.0.1",

},

"devDependencies": {

  "@types/passport-jwt": "^3.0.8",

}



We have to install these packages to implement complete JSON Web Token authentication and Authorization

Import JWT Module in AuthModule

// auth.module.ts

import { JwtModule } from '@nestjs/jwt';



imports: [UsersModule, JwtModule.register({ secret: 'HAD_12X#@' })],

controllers: [AuthController],

providers: [AuthService],

exports: [AuthService],



You need to register the JwtModule module in the AuthModule by providing the unique secret key. We will use this secret key to decode the token or validate the token

Refactor the AuthService

// auth.service.ts




export class AuthService{

constructor(

  private userService: UsersService,

  private jwtService: JwtService, // 1

) {}




async login(loginDTO: LoginDTO): Promise<{ accessToken: string }> { // 1.




/// ...

if (passwordMatched) {

  delete user.password;

  return user;

  // Sends JWT Token back in the response

  const payload = { email: user.email, sub: user.id };




  return {

    accessToken: this.jwtService.sign(payload),

  };

}





  1. Inject JwtService as a dependency.

  2. If the password matches, generate the JWT token using the jwtService.sign method.

  3. Provide the payload, which should include the user email and userId inside the JWT token. Choose a name for the user ID field; 'sub' is used here but any name can be applied.

Create a new file to save Constant Values

You have to create a new file inside the auth folder.

// auth.constants.ts

export const authConstants = {

  secret: "HAD_12X#@",

};



Refactor Auth Module

// auth.module.ts




imports: [

  UsersModule,

  JwtModule.register({

  secret: authConstants.secret,

  signOptions: { // 1.

  expiresIn: '1d',

},

}),

],



  1. The token will expire after one day

Create JWT Strategy Service

// jwt.strategy.ts



import { Injectable } from "@nestjs/common";

import { PassportStrategy } from "@nestjs/passport";

import { ExtractJwt, Strategy } from "passport-jwt";

import { AuthService } from "./auth.service";

import { authConstants } from "./auth.constants";



@Injectable()

export class JWTStrategy extends PassportStrategy(Strategy) {

  constructor() {

    super({

      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // 1.

      ignoreExpiration: false, // 2.

      secretOrKey: authConstants.secret, // 3.

    });

  }



  async validate(payload: any) {

    // 4.

    return { userId: payload.sub, email: payload.email }; // 5.

  }

}



You have to create new a file inside the auth folder with jwt.strategy.ts

  1. Create the JWTStrategy service, extending it with PassportStrategy. When applying @AuthGuard('jwt'), the validate function will be called, automatically adding the userId and email to the req.user object.

Register the JWTStrategy in AuthModule

// auth.module.ts

import { JWTStrategy } from './jwt.strategy';

import { PassportModule } from '@nestjs/passport';




imports: [

  PassportModule,

]

providers: [AuthService, JWTStrategy],



You have to register the JWTStrategy as a provider in AuthModule. You also have to register the PassportModule. It will allow us to use the PassportStrategy class

Create JWT Guard

A Guard is like a middleware in express.js. You can implement role-based authentication using guards

import { Injectable } from "@nestjs/common";

import { AuthGuard } from "@nestjs/passport";



@Injectable()

export class JwtAuthGuard extends AuthGuard("jwt") {}



Let's create a provider which is JwtAuthGuard. You have to apply the JWTAuthGuard to protect a private route

Protect Private Route in AppController

@Get('profile')

@UseGuards(JwtAuthGuard)

getProfile(

  @Request()

  req,

) {

  return req.user;

}





Create a new protected route inside the AppController. When sending a request to access the profile, the response will include the user ID and email. Apply JwtAuthGuard to any route in any controller to secure the endpoint.

Test the Application

GET http://localhost:3000/profile

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhaWRlcl9hbGkzQGdtYWlsLmNvbSIsInN1YiI6NywiaWF0IjoxNjg0MjM3ODgzLCJleHAiOjE2ODQzMjQyODN9.DztMDAKZOnQjZPFkFPiWiJUmI_VnrNfNvfwPI0yJ8MA



First, send a login request to obtain the token, and then provide that token in the header.



What is Role-based Authentication

Role-based authentication is a method of access control that regulates user permissions and privileges within a system based on their assigned roles. In this approach, users are categorized into roles based on their job responsibilities, functions, or levels of authority within an organization.

Each role is associated with a set of permissions that determine what actions and resources a user with that role can access. These permissions can include read, write, modify, delete, and other operations on various system resources such as files, databases, or functionalities.

We are going to implement this scenario:

  • An artist can upload/create the song

We have to restrict the access of creating songs endpoint. Only artists can access this endpoint and create a song

Create an Artists Module

import { Module } from "@nestjs/common";

import { ArtistsService } from "./artists.service";

import { TypeOrmModule } from "@nestjs/typeorm";

import { Artist } from "./artist.entity";



@Module({

  imports: [TypeOrmModule.forFeature([Artist])],

  providers: [ArtistsService],

  exports: [ArtistsService],

})

export class ArtistsModule {}



Add ArtistsModule into AppModule

import { AuthModule } from './auth/auth.module';




@Module({

imports: [

  PlayListModule,

  UsersModule,

  AuthModule,

  ArtistsModule,

],

})



Create ArtistsService

import { Injectable } from "@nestjs/common";

import { InjectRepository } from "@nestjs/typeorm";

import { Repository } from "typeorm";

import { Artist } from "./artist.entity";



@Injectable()

export class ArtistsService {

  constructor(

    @InjectRepository(Artist)

    private artistRepo: Repository<Artist>

  ) {}



  findArtist(userId: number): Promise<Artist> {

    return this.artistRepo.findOneBy({ user: { id: userId } });

  }

}



We will use the findArtist method to check if the current logged in user is an artist or not.

Refactor login method in AuthService

constructor(

private userService: UsersService,

private jwtService: JwtService,

private artistService: ArtistsService,

) {}



You have to inject the artist's service in the AuthService constructor

const payload: PayloadType = { email: user.email, userId: user.id }; // 1



// find if it is an artist then the add the artist id to payload

const artist = await this.artistService.findArtist(user.id); // 2

if (artist) {

  // 3

  payload.artistId = artist.id;

}



  1. Refactor the payload method in the login function by changing sub: user.id to userId: user.id.

  2. Find the artist based on the logged-in user.

  3. If the user is an artist, save the artist ID in the payload; this artist ID will be used when decoding the token in the ArtistGuard.

Create a PayloadType

You have to create a payload type inside the auth folder. I have created a separate folder for the types to store all my types here

//types/payload.type.ts

export interface PayloadType {

  email: string;

  userId: number;

  artistId?: number;

}



Create a new JwtArtistGuard

In Nest.js, implementing role-based authentication involves the use of guard functions, which act as middleware to authorize requests. Each role can be associated with a distinct guard function that validates whether a user has the appropriate permissions to access a resource. This is an application of the "Single Responsibility Principle," as each guard function focuses solely on authorizing a specific role, making the code easier to manage and extend.

// jwt-artist.guard.ts

import {

  ExecutionContext,

  Injectable,

  UnauthorizedException,

} from "@nestjs/common";

import { AuthGuard } from "@nestjs/passport";

import { Observable } from "rxjs";

import { PayloadType } from "./types/payload.type";



@Injectable()

export class JwtArtistGuard extends AuthGuard("jwt") {

  canActivate(

    context: ExecutionContext

  ): boolean | Promise<boolean> | Observable<boolean> {

    return super.canActivate(context);

  }

  handleRequest<TUser = PayloadType>(err: any, user: any): TUser {

    // 1

    if (err || !user) {

      //2

      throw err || new UnauthorizedException();

    }

    console.log(user);

    if (user.artistId) {

      // 3

      return user;

    }

    throw err || new UnauthorizedException();

  }

}



  1. When you apply the JwtAuthGuard at the controller function it will call the handleRequest function

  2. If there is an error or no user then it will send the unauthorized error

  3. Here we are checking the user role, if it is an artist then we have to return the user. This user will have have email, userId, and artistId property

Refactor the validate method in JwtStrategy

// jwt-strategy.ts

async validate(payload: PayloadType) { //1.

 return {

 userId: payload.userId,

 email: payload.email,

 artistId: payload.artistId, // 2

 };

}



  1. Add the payloadType for the argument.

  2. Include artistId in the response; note that artistId is an optional property and may be null.

Apply JwtArtistGuard on creating songs endpoint

//songs.controller.ts

@Post()

@UseGuards(JwtArtistGuard) // 1

  create(@Body() createSongDTO: CreateSongDto, @Request() req): Promise<Song> {

  console.log(req.user);

  return this.songsService.create(createSongDTO);

}



Now we have protected this endpoint, only artist can access this endpoint and create a new song

Test the Application

  1. First of all you must have an artist record in your DB

  2. If you don't have you can create an artist manually using pgAdmin

  3. You have to send the login request as an artist

  4. It will give you the access token you have to use that token to access to Create Songs endpoint

### Artist Login User




POST http://localhost:3001/auth/login

Content-Type: application/json




{

  "email": "john_doe@gmail.com",

  "password": "123456"

}





It will give you the token, you have to use that token to create a new song

### Create New SONGS REQUEST as An Artist

POST http://localhost:3001/songs

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhaWRlcl9hbGkzQGdtYWlsLmNvbSIsInVzZXJJZCI6NywiaWF0IjoxNjg0MzA3Mjg3LCJleHAiOjE2ODQzOTM2ODd9.A9SEhTH0O0SR5-UELhMhak5MVIyY2ISRwR-o2RKF_dY

Content-Type: application/json




{

  "title": "You for me",

  "artists": [1],

  "releasedDate" : "2022-08-29",

  "duration" :"02:34",

  "lyrics": "by, you're my adrenaline. Brought out this other    side of me You don't even know Controlling my whole anatomy, oh Fingers are holding you right at the edge You're slipping out of my hands Keeping my secrets all up in my head I'm scared that you won't want me back, oh I dance to every song like it's about ya I drink 'til I kiss someone who looks like ya I wish that I was honest when I had you I shoulda told you that I wanted you for me I dance to every song like it's about ya I drink 'til I kiss someone who looks like ya"

}




What is Two Factor Authentication

Two-factor authentication (2FA), also known as multi-factor authentication (MFA), is a security mechanism that requires users to provide two or more separate forms of identification to verify their identity and gain access to a system or account. It adds an extra layer of security beyond traditional username and password authentication.

The two factors typically fall into three categories:

Something you know: This factor involves knowledge-based information that the user possesses, such as a password, PIN, or answers to security questions.

  1. Something you have: This factor requires the user to possess a physical device or object, such as a smartphone, hardware token, or smart card. This device generates or receives a unique one-time code or a cryptographic key.

  2. Something you are: This factor refers to biometric characteristics unique to the individual, such as fingerprints, facial recognition, iris scans, or voice recognition.

  3. The combination of these factors increases the security of authentication because an attacker would need to compromise multiple elements to gain unauthorized access. Even if one factor is compromised, the additional factor(s) provide an extra layer of protection.

Here's a simplified example of how two-factor authentication works:

  1. The user enters their username and password on a login page.

  2. After successful initial authentication, the system prompts the user for a second form of verification.

  3. The user may be required to provide a one-time code generated by an authentication app on their smartphone or received via SMS.

  4. The user enters the one-time code to complete the authentication process.

  5. If both factors are verified successfully, access is granted to the user.

Two-factor authentication is widely used across various systems, including online accounts (email, social media, banking), VPNs, cloud services, and more. It significantly reduces the risk of unauthorized access due to compromised passwords or stolen credentials, enhancing overall security and protecting sensitive information.

Install Dependencies

Speakeasy is a one-time passcode generator, ideal for use in two-factor authentication, that supports Google Authenticator and other two-factor devices.

It is well-tested and includes robust support for custom token lengths, authentication windows, hash algorithms like SHA256 and SHA512, and other features, and includes helpers like a secret key generator.

Speakeasy implements one-time passcode generators as standardized by the Initiative for Open Authentication (OATH). The HMAC-Based One-Time Password (HOTP) algorithm defined in RFC 4226 and the Time-Based One-time Password (TOTP) algorithm defined in RFC 6238 are supported. This project incorporates code from passcode, originally a fork of Speakeasy, and notp.

"speakeasy": "^2.0.0",



You have to add this entry to dependencies in the package.json file and run npm install

"@types/speakeasy": "^2.0.7",



You have to add this entry to devDependencies in the package.json file and run npm install

Update User Entity

// user.entity.ts

@Column({ nullable: true, type: 'text' })

twoFASecret: string;




@Column({ default: false, type: 'boolean' })

enable2FA: boolean;



You have to add two new columns for two-factor authentication. The first column will be used to store the secret key for each user. The second column will be used to enable or disable the two-factor authentication.

Add new Enable2FA type

//types/auth-types.ts

export type Enable2FAType = {

  secret: string;

};



Add a new method in AuthService to enable two-factor auth

//auth.service.ts

import * as speakeasy from 'speakeasy';




async enable2FA(userId: number) : Promise<Enable2FAType> {

const user = await this.userService.findById({ id: userId }); //1

if (user.enable2FA) { //2

return { secret: user.twoFASecret };

}

const secret = speakeasy.generateSecret(); //3

console.log(secret);

user.twoFASecret = secret.base32; //4

await this.userService.updateSecretKey(user.id, user.twoFASecret); //5

return { secret: user.twoFASecret }; //6

}





  1. You have to create a new method to find the user on the based on user id

  2. If user has already enabled the 2 factor authentication we have to return the secret key.

  3. If the user did not enable the 2-factor authentication then we have to generate the secret key. This speakeasy.generateSecret() will return an object with secret.ascii, secret.hex, and secret.base32.

  4. We are going to use the base32 secret key

  5. Finally, we have to update the twoFAsecret key for the specific user

  6. You have to return the secret key in the response.

Use base32 secret key in QR code App

You have to install the chrome extension or Google authenticator app on your phone

You have received the secret key now you have to create a new app in your QR code application. It will ask you to add a manual secret key and the name of your app



Refactor UsersService

import { Repository, UpdateResult } from 'typeorm';




//users.service.ts

async updateSecretKey(userId, secret: string): Promise<UpdateResult> {

return this.userRepository.update(

{ id: userId },

{

twoFASecret: secret,

enable2FA: true,

},

);

}



You have to the secret key and enable the 2-factor authentication for a user

//users.service.ts

async findById(id: number): Promise<User> {

return this.userRepository.findOneBy({ id: id });

}



Create Endpoint in AuthController to enable 2FA

//auth.controller.ts

@Post('enable-2fa')

@UseGuards(JwtAuthGuard)

enable2FA(

@Request()

req,

): Promise<Enable2FAType> {

console.log(req.user);

return this.authService.enable2FA(req.user.userId);

}



Test the Enable Authentication Endpoint




### Enable 2FA Authentication

POST http://localhost:3000/auth/enable-2fa

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhaWRlcl9hbGkzQGdtYWlsLmNvbSIsInN1YiI6NywiaWF0IjoxNjg0NDEyMjk2LCJleHAiOjE2ODQ0OTg2OTZ9.Fg0K4gJABBP3nqt8PMK72MzSnFVK0xRaEeC_aDxnfeo



You have to provide your own user token

Implement Disable 2-Factor Authentication

You have to new method inside the auth.service.ts to disable the authentication

//auth.service.ts

async disable2FA(userId: number): Promise<UpdateResult> {

return this.userService.disable2FA(userId);

}



//users.service.ts

async disable2FA(userId: number): Promise<UpdateResult> {

return this.userRepository.update(

{ id: userId },

{

enable2FA: false,

twoFASecret: null,

},

);

}



You have to create a new route to disable authentication

//auth.controller.ts

@Get('disable-2fa')

@UseGuards(JwtAuthGuard)

disable2FA(

@Request()

req,

): Promise<UpdateResult> {

return this.authService.disable2FA(req.user.userId);

}



Now you can test the disabled authentication endpoint

GET http://localhost:3000/auth/disable-2fa

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhaWRlcl9hbGkzQGdtYWlsLmNvbSIsInN1YiI6NywiaWF0IjoxNjg0NDkyOTk1LCJleHAiOjE2ODQ1NzkzOTV9.vhAHpdyuQHWvsET2sSLvQpr33vpk8K089NLiENgh7pM



Verify One-time password/token

// auth.controller.ts

@Post('validate-2fa')

@UseGuards(JwtAuthGuard)

validate2FA(

@Request()

req,

@Body()

ValidateTokenDTO: ValidateTokenDTO,

): Promise<{ verified: boolean }> {

return this.authService.validate2FAToken(

req.user.userId,

ValidateTokenDTO.token,

);

}



You have to create an endpoint to validate the one-time password/token

// auth/dto/validate-token.dto.ts

import { IsNotEmpty, IsString } from "class-validator";



export class ValidateTokenDTO {

  @IsNotEmpty()

  @IsString()

  token: string;

}



Now you have to create a new method inside the auth.service.ts to verify the token




// validate the 2fa secret with provided token

async validate2FAToken(

userId: number,

token: string,

): Promise<{ verified: boolean }> {

try {

// find the user on the based on id

const user = await this.userService.findById(userId);




// extract his 2FA secret




// verify the secret with a token by calling the speakeasy verify method

const verified = speakeasy.totp.verify({

secret: user.twoFASecret,

token: token,

encoding: 'base32',

});




// if validated then sends the json web token in the response

if (verified) {

return { verified: true };

} else {

return { verified: false };

}

} catch (err) {

throw new UnauthorizedException('Error verifying token');

}

}



Let's test the validate token endpoint.

### Validate 2FA Token




POST http://localhost:3000/auth/validate-2fa

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InNhbUBnbWFpbC5jb20iLCJzdWIiOjksImlhdCI6MTY4NDQ5ODg0MSwiZXhwIjoxNjg0NTg1MjQxfQ.dKgmLSsGctbWR9HKz3ByfS2ZpUGiNR234uqEgs0pgtQ

Content-Type: application/json




{

"token": "054603"

}



The token I have provided is one-time password/token from the authenticator app. You have to provide your unique token from your authenticator app

If user enabled the 2FA refactor the login method




async login(

loginDTO: LoginDTO,

): Promise<{ accessToken: string } | { validate2FA: string; message: string }>



You have to refactor the return type. If the user has enabled the 2FA it will use the second return type with a custom message




const user = await this.userService.findOne(loginDTO);

const passwordMatched = await bcrypt.compare(

loginDTO.password,

// Sends JWT Token back in the response

const payload = { email: user.email, sub: user.id };




// If user has enabled 2FA and have the secret key then

if (user.enable2FA && user.twoFASecret) { //1.

// sends the validateToken request link

// else otherwise sends the json web token in the response

return { //2.

validate2FA: 'http://localhost:3000/auth/validate-2fa',

message:

'Please send the one-time password/token from your Google Authenticator App',

};

}

return {

accessToken: this.jwtService.sign(payload),

};



  1. You have to add a new code here to check user enabled the 2FA.

  2. If the user enabled the 2FA then we have to send the link to validate the token from your QR code app




What is an API Key Authentication

API key authentication is a method of authenticating and securing access to an application programming interface (API). It involves the use of an API key, which is a unique identifier that grants access to specific API resources and operations.

The purpose of API key authentication is to control and monitor access to an API by assigning and managing unique keys for individual users or applications.

API keys serve as a form of credential, allowing the API provider to track and control API usage, enforce rate limits, and monitor and identify unauthorized access attempts.

When do you need API Keys

  • You want to control the number of calls made to your API.

  • You want to identify usage patterns in your API's traffic

API key authentication is typically used in scenarios where multiple users or applications need to access an API.

It enables the API provider to identify and track the usage of different clients, which can be beneficial for managing access levels, implementing usage quotas, and identifying potential abuse or security breaches.

Where can we use API Keys

API key authentication can be used in a variety of contexts, including web and mobile applications, microservices, and third-party integrations.

It is commonly employed by API providers to ensure secure and controlled access to their services. By requiring API keys, providers can track usage, enforce restrictions, and have the flexibility to revoke access for specific keys if necessary.

Complete Flow of API Key Authentication

  1. The client initiates a request to access an API resource or perform an operation.

  2. The client includes the API key as part of the request. This can be done in various ways, such as including the key in the request header, query parameters, or as part of the request body.

  3. The API server receives the request and extracts the API key.

  4. The API server verifies the API key's validity and authenticity. This verification process may involve checking against a database or performing cryptographic operations.

  5. If the API key is valid, the server grants access to the requested resources or operations based on the permissions associated with that key. If the key is invalid or expired, the server denies access and returns an appropriate error response.

  6. The API server processes the client's request and returns the requested data or performs the requested operation.

  7. The client receives the response from the API server and can continue interacting with the API if the authentication was successful.

Steps

  1. Step 1: Generate API Keys

  2. Step 2: Create and Store API key

  3. Step 3: Create an API Key strategy

  4. Step 4: Register API key strategy in Auth Module

  5. Step 5: Validate the User by API key

  6. Step 6: Apply API key Authentication on protected Route

Step 1: Generate API Keys

First of all, we have to generate API keys. I am going to use a third third-party package uuid to create unique api keys.

You have to install it

npm install uuid



Step 2: Create and Store the API key

Now we need to create an API Key and store it in the database. Each user will have its own API key. We need to add api key logic in the signup function. When the user is registered we have to assign the unique api key

// user.entity.ts




@Column()

apiKey: string;



You have to add the apiKey column in the user entity. Let's create the api key inside the signup function

// users.service.ts



import { v4 as uuid4 } from "uuid";



const user = new User();

user.firstName = userDTO.firstName;

user.lastName = userDTO.lastName;

user.email = userDTO.email;

user.apiKey = uuid4();

user.password = userDTO.password;



const savedUser = await this.userRepository.save(user);

delete savedUser.password;

return savedUser;



I have called the uuid4() to create the Api key.

Let's test the application by sending a signup api request

POST http://localhost:3001/auth/signup

Content-Type: application/json




{

"firstName": "sam",

"lastName": "oven",

"email": "sam@gmail.com",

"password": "123456"

}



{

  "firstName": "sam",

  "lastName": "oven",

  "email": "sam@gmail.com",

  "apiKey": "853d94a2-f760-43e3-b384-a9ba94542bf0",

  "twoFASecret": null,

  "id": 1,

  "enable2FA": false

}



Step 3: Create an API Key strategy

You have to create a new file ApiKeyStrategy.ts in the auth folder

import { Injectable, UnauthorizedException } from "@nestjs/common";

import { PassportStrategy } from "@nestjs/passport";

import { Strategy } from "passport-http-bearer";

import { AuthService } from "./auth.service";



@Injectable()

export class ApiKeyStrategy extends PassportStrategy(Strategy) {

  constructor(private authService: AuthService) {

    super();

  }

  async validate(apiKey: string) {

    const user = await this.authService.validateUserByApiKey(apiKey);

    if (!user) {

      throw new UnauthorizedException();

    } else {

      return user;

    }

  }

}



We have installed the passport-http-bearer package and applied this strategy to validate the API keys. It means you need to provide the Api key in the authorization header:

Authorization: Bearer 853d94a2-f760-43e3-b384-a9ba94542bf0



When you will apply the AuthGuard @UseGuards(AuthGuard('bearer')) to the protected route. It will call the validate method from the ApiKeyStrategy

Step 4: Register API key strategy in Auth Module

import { ApiKeyStrategy } from './api-key.strategy';




providers: [AuthService, JWTStrategy, ApiKeyStrategy],





Step 5: Validate the User by API key

//auth.service.ts




async validateUserByApiKey(apiKey: string): Promise<User> {

return this.userService.findByApiKey(apiKey);

}



I have created a new function inside the auth service and validated the user by api key

// user.service.ts

async findByApiKey(apiKey: string): Promise<User> {

return this.userRepository.findOneBy({ apiKey });

}



Let's create a new function inside the user.service.ts to fetch the user from the DB based on API Key

Step 6: Apply API key Authentication on protected Route

//auth.controller.ts

@Get('profile')

@UseGuards(AuthGuard('bearer'))

getProfile(

@Request()

req,

) {

 delete req.user.password;

 return {

  msg: 'authenticated with api key',

  user: req.user,

  };

}



A user can access his/her profile. He has to provide the API key to access the protected route. You can protect any route by applying the AuthGuard('bearer') to the route

GET http://localhost:3001/auth/profile

Authorization: Bearer 853d94a2-f760-43e3-b384-a9ba94542bf0



You have to provide the API key in the authorization header to execute the request successfully.



Create a launch.json file in the .vscode folder

Create a launch.json file in the .vscode folder to configure debugging settings specifically for your Nest.js application. This file allows you to set breakpoints and inspect variables directly in the Visual Studio Code editor, enhancing your development experience. Utilizing a launch.json aligns with the software engineering principle of "Configuration as Code," making your development environment easily reproducible and version-controllable.




{

  "version": "0.2.0",

  "configurations": [{

    "name": "Attach",

    "port": 9229,

    "request": "attach",

    "skipFiles": ["<node_internals>/**"],

    "type": "node"

}

]

}





Start the Application using the Debug script

Nestjs provides the debug command to start debugging the application. Find the debug command in the package.json file

"start:debug": "nest start --debug --watch",



Run the application using npm run start:debug



In TypeORM, migrations are a way to manage and apply changes to your database schema over time. A migration is a file that contains a set of instructions for creating, modifying, or deleting database tables, columns, constraints, and other schema elements. Migrations help you keep your database schema in sync with your application's models or entities.

When you develop an application using TypeORM, your database schema evolves as you add new features, modify existing ones, or fix issues. Migrations provide a structured and controlled approach to apply these changes to your database without losing existing data.

Here's a general workflow of how migrations work in TypeORM:

  1. Creating a Migration: When you make changes to your entities or models, you generate a migration file using TypeORM's CLI command or programmatically using the provided API. The migration file contains both the "up" and "down" methods. The "up" method specifies how to apply the changes to the database, while the "down" method defines how to revert those changes.

  2. Applying Migrations: To apply a migration, you execute the migration runner provided by TypeORM, either through the CLI or programmatically in your code. The runner reads the migration files and executes the "up" method, which performs the necessary changes to the database schema.

  3. Reverting Migrations: If you encounter issues or need to roll back changes, you can use the migration runner to revert migrations. The runner executes the "down" method of the migration file, which undoes the changes made by the corresponding "up" method.

  4. Managing Migration History: TypeORM keeps track of the executed migrations in a table within your database. This table records which migrations have been applied, allowing the runner to determine which migrations are pending or need to be reverted.

By using migrations, you can version control your database schema, collaborate with other developers effectively, and easily deploy schema changes across different environments. Migrations provide a structured and reliable approach to evolving your database schema while preserving data integrity.

$7 bundle of my best NestJS + backend developer Courses.
More details coming soon.


Get the latest insights from the marketing world.

A blog that focuses on providing practical tips and strategies for businesses to improve their marketing and sales efforts.

Solutions

Helping you help your customers.

Sell smarter, better, and faster.

The insights you need to make smarter business decisions.