DEV Community

Cover image for Authentication with Aws Cognito, Passport and NestJs (Part II)
Fausto Braz
Fausto Braz

Posted on

Authentication with Aws Cognito, Passport and NestJs (Part II)

Setting up our Nest Js project

Let's start by booting a new instance of a nest js project with nest new pokemon-app.

We also need to install some extra libraries:

package.json:

...

 "dependencies": {
    "@nestjs/common": "^9.0.0",
    "@nestjs/config": "^2.2.0",
    "@nestjs/core": "^9.0.0",
    "@nestjs/platform-express": "^9.0.0",
    "amazon-cognito-identity-js": "^5.2.10",
    "@nestjs/passport": "^9.0.0",
    "@types/passport-jwt": "^3.0.7",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.13.2",
    "jwks-rsa": "^2.1.5",
    "passport": "^0.6.0",
    "passport-jwt": "^4.0.0",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.2.0"
  }

Enter fullscreen mode Exit fullscreen mode

Also, to manage environment files in nestjs we need to install the nest config module via npm i @nestjs/config.
For this guide, I will keep it simple and reference them as global, and instead of dependency injection, I will use the process.env directly. You can find more documentation here.

Don't forget to create the .development.env file in your root folder.

app-module.ts:

...
@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: '.development.env',
      isGlobal: true,
    }),
    PokemonModule,
    AuthModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

Enter fullscreen mode Exit fullscreen mode

Registration and authentication

After, let's navigate to the root directory and boot a module with nest g module pokemon, and create a controller and one service with nest g controller pokemon and nest g service pokemon

Now let's create a pokemon interface under the pokemon directory and export it:

pokemon.interface.ts:


export interface Pokemon {
  readonly name: string;
  readonly type: string;
} 

Enter fullscreen mode Exit fullscreen mode

After let's create the method in the service to list the pokemons. (It will be static to have an example of an endpoint to fetch)

pokemon.service.ts:


@Injectable()
export class PokemonService {
  listAllPokemons(): Array<Pokemon> {
    return [
      { name: 'Pikachu', type: 'Electric' },
      { name: 'Volpix', type: 'Fire' },
      { name: 'Flabébé', type: 'Fairy' },
    ];
  }
}

Enter fullscreen mode Exit fullscreen mode

Let's reference the endpoint path in the pokemon controller and inject the PokemonService to be consumed:

pokemon.controller.ts:

...

import { PokemonService } from './pokemon.service';

@Controller('api/v1/pokemons')
export class PokemonController {
  constructor(private readonly pokemonService: PokemonService) {}

  @Get()
  listAllPokemons(): Array<Pokemon> {
    return this.pokemonService.listAllPokemons();
  }
}

Enter fullscreen mode Exit fullscreen mode

We can run npm run start:dev. If everything builds fine, we should list the pokemon's at http://localhost:3000/api/v1/pokemons

Listing pokemons

Now we will build our auth module only to allow authenticated users to fetch the info from that endpoint. Let's generate another module with nest g module auth and another controller with nest g controller auth.

Next, let's create our DTOs, under the auth folder, for the user registration and login:

auth-login-user:

...

import { IsEmail, Matches } from 'class-validator';

export class AuthLoginUserDto {
  @IsEmail()
  email: string;

  /* Minimum eight characters, at least one uppercase letter, one lowercase letter, one number, and one special character */

  @Matches(
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$&+,:;=?@#|'<>.^*()%!-])[A-Za-z\d@$&+,:;=?@#|'<>.^*()%!-]{8,}$/,
    { message: 'invalid password' },
  )
  password: string;
}

Enter fullscreen mode Exit fullscreen mode

auth-register-user:

...

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

export class AuthRegisterUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;

  /* Minimum eight characters, at least one uppercase letter, one lowercase letter, one number, and one special character */

  @Matches(
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$&+,:;=?@#|'<>.^*()%!-])[A-Za-z\d@$&+,:;=?@#|'<>.^*()%!-]{8,}$/,
    { message: 'invalid password' },
  )
  password: string;

}

Enter fullscreen mode Exit fullscreen mode

After let's write the Cognito service with the register and authentication methods, generate a new service inside the auth module, and don't forget to reference it in that provider's module:

aws-cognito.service.ts:


import { Injectable } from '@nestjs/common';
import {
  AuthenticationDetails,
  CognitoUser,
  CognitoUserAttribute,
  CognitoUserPool,
} from 'amazon-cognito-identity-js';
import { AuthLoginUserDto } from './dtos/auth-login-user.dto';
import { AuthRegisterUserDto } from './dtos/auth-register-user.dto';

@Injectable()
export class AwsCognitoService {
  private userPool: CognitoUserPool;

  constructor() {
    this.userPool = new CognitoUserPool({
      UserPoolId: process.env.AWS_COGNITO_USER_POOL_ID,
      ClientId: process.env.AWS_COGNITO_CLIENT_ID,
    });
  }

  async registerUser(authRegisterUserDto: AuthRegisterUserDto) {
    const { name, email, password } = authRegisterUserDto;

    return new Promise((resolve, reject) => {
      this.userPool.signUp(
        email,
        password,
        [
          new CognitoUserAttribute({
            Name: 'name',
            Value: name,
          }),
        ],
        null,
        (err, result) => {
          if (!result) {
            reject(err);
          } else {
            resolve(result.user);
          }
        },
      );
    });
  }

  async authenticateUser(authLoginUserDto: AuthLoginUserDto) {
    const { email, password } = authLoginUserDto;
    const userData = {
      Username: email,
      Pool: this.userPool,
    };

    const authenticationDetails = new AuthenticationDetails({
      Username: email,
      Password: password,
    });

    const userCognito = new CognitoUser(userData);

    return new Promise((resolve, reject) => {
      userCognito.authenticateUser(authenticationDetails, {
        onSuccess: (result) => {
         resolve({
            accessToken: result.getAccessToken().getJwtToken(),
            refreshToken: result.getRefreshToken().getToken(),
          });
        },
        onFailure: (err) => {
          reject(err);
        },
      });
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

Now we should save in the development.env the values for AWS_COGNITO_USER_POOL_ID and AWS_COGNITO_CLIENT_ID that we held in the first part of the guide.

In our auth.controller.ts, we can now inject the AWS Cognito service and register and authenticate the user:

auth.controller.ts


import {
  Body,
  Controller,
  Post,
  UsePipes,
  ValidationPipe,
} from '@nestjs/common';
import { AwsCognitoService } from './aws-cognito.service';
import { AuthLoginUserDto } from './dtos/auth-login-user.dto';
import { AuthRegisterUserDto } from './dtos/auth-register-user.dto';

@Controller('api/v1/auth')
export class AuthController {
  constructor(private awsCognitoService: AwsCognitoService) {}

  @Post('/register')
  async register(@Body() authRegisterUserDto: AuthRegisterUserDto) {
    return await this.awsCognitoService.registerUser(authRegisterUserDto);
  }

  @Post('/login')
  @UsePipes(ValidationPipe)
  async login(@Body() authLoginUserDto: AuthLoginUserDto) {
    return await this.awsCognitoService.authenticateUser(authLoginUserDto);
  }
}

Enter fullscreen mode Exit fullscreen mode

Start your application with npm run start:dev and if everything starts up without errors, we can give our registry endpoint a go to see if it's working:

Register endpoint

User in pool

Verification email

Seem's to be working, the user is created and verified. Let's test the login:

Login Endpoint

That's all for the second part; in the third part, we will protect the endpoint and the resources using Passport and Jwt Authentication.
Stay tuned 😊

Latest comments (1)

Collapse
 
ajankowy25 profile image
Agnieszka Jankowy • Edited

That's a great guide to understanding how Cognito works! It helps me a lot :) I just wanna point out one thing: You don't need to use await within return: such as return await... It uses slightly more memory to work. Actually, the client which uses our controller will await the result, so there is no need to use _ return await _ inside the controller :)