DEV Community

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

Posted on • Updated on

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

Protecting the endpoint

For authentication, we will be using the JWT strategy. We will require the use of a bearer token generated by Cognito for accessing the protected endpoint resources.

Let's first import the PassportModule and give the JWT strategy as default.

auth.module.ts

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

@Module({
 imports: [PassportModule.register({ defaultStrategy: 'jwt' })],
    ...
})
export class AuthModule {}

Enter fullscreen mode Exit fullscreen mode

Now we create a file called jwt.strategy.ts in the same directory as the auth.controller.ts.

This class will extend from PassportStrategy and pass the chosen strategy. Also, it will verify if the request is valid through the validate() callback. We also use a new environment var AWS_COGNITO_AUTHORITY that should be https://cognito-idp.YOUR_POOL_REGION.amazonaws.com/AWS_COGNITO_USER_POOL_ID, my pool region is North Virginia, so isus-east-1.
Don't forget to list it in the auth.module.ts providers since it is an @Injectable

jwt.strategy.ts


import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { passportJwtSecret } from 'jwks-rsa';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      audience: process.env.AWS_COGNITO_COGNITO_CLIENT_ID,
      issuer: process.env.AWS_COGNITO_AUTHORITY,
      algorithms: ['RS256'],
      secretOrKeyProvider: passportJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: process.env.AWS_COGNITO_AUTHORITY + '/.well-known/jwks.json',
      }),
    });
  }

  async validate(payload: any) {
    return { idUser: payload.sub, email: payload.email };
  }
}

Enter fullscreen mode Exit fullscreen mode

Finally, we protect the pokemon's list endpoint using a nest guard. Let's head to our pokemon.controller.ts and add the @UseGuards() decorator passing AuthGuard('jwt') as a parameter.
Something like this:

pokemon.controller.ts

import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
...

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

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


Enter fullscreen mode Exit fullscreen mode

Now, If we don't pass the bearer token produced when we log in, we will get a 401 Unauthorized from the endpoint:

Unauthorized request

Authorized request

And our endpoint is now protected from unwanted access.

Changing the password

To change the password, we will need to do another method called changeUserPassword in our aws-cognito.service.ts. This method will receive another DTO to get our current and new passwords. In that method, Cognito will need to authenticate the user first for changing the password later using the changePassword method:

auth-change-password-user.dto.ts

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

export class AuthChangePasswordUserDto {
  @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' },
  )
  currentPassword: string;

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


Enter fullscreen mode Exit fullscreen mode

aws-cognito.service.ts

...

import { AuthChangePasswordUserDto } from './dtos/auth-change-password-user.dto';

...

async changeUserPassword(
    authChangePasswordUserDto: AuthChangePasswordUserDto,
  ) {
    const { email, currentPassword, newPassword } = authChangePasswordUserDto;

    const userData = {
      Username: email,
      Pool: this.userPool,
    };

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

    const userCognito = new CognitoUser(userData);

    return new Promise((resolve, reject) => {
      userCognito.authenticateUser(authenticationDetails, {
        onSuccess: () => {
          userCognito.changePassword(
            currentPassword,
            newPassword,
            (err, result) => {
              if (err) {
                reject(err);
                return;
              }
              resolve(result);
            },
          );
        },
        onFailure: (err) => {
          reject(err);
        },
      });
    });
  }

  ...

Enter fullscreen mode Exit fullscreen mode

After we only need to do a new route in the auth.controller passing the AuthChangePasswordUserDto and calling the changeUserPassword from the aws.cognito.service.ts:

auth.controller.ts

...

@Post('/change-password')
  @UsePipes(ValidationPipe)
  async changePassword(
    @Body() authChangePasswordUserDto: AuthChangePasswordUserDto,
  ) {
    await this.awsCognitoService.changeUserPassword(authChangePasswordUserDto);
  }

 ...

Enter fullscreen mode Exit fullscreen mode

After the tests, all seems to be working correctly:
Change password with error
Change Password
Login with changed password

Reset forgotten password

For resetting forgotten passwords, we will need two new endpoints, one for asking for a unique code and the other for switching the password.
We start by defining two new DTO's:

auth-forgot-password-user.dto.ts

...

import { IsEmail } from 'class-validator';

export class AuthForgotPasswordUserDto {
  @IsEmail()
  email: string;
}

 ...

Enter fullscreen mode Exit fullscreen mode

auth-confirm-password-user.dto.ts

...

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

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

  @IsString()
  confirmationCode: string;

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

Enter fullscreen mode Exit fullscreen mode

Also, we will need to make two new methods in the aws.service.ts, to ask Cognito for a new reset code and to change the password:

aws-cognito.service.ts

...

import { AuthConfirmPasswordUserDto } from './dtos/auth-confirm-password-user.dto';
import { AuthForgotPasswordUserDto } from './dtos/auth-forgot-password-user.dto';

...

  async forgotUserPassword(
    authForgotPasswordUserDto: AuthForgotPasswordUserDto,
  ) {
    const { email } = authForgotPasswordUserDto;

    const userData = {
      Username: email,
      Pool: this.userPool,
    };

    const userCognito = new CognitoUser(userData);

    return new Promise((resolve, reject) => {
      userCognito.forgotPassword({
        onSuccess: (result) => {
          resolve(result);
        },
        onFailure: (err) => {
          reject(err);
        },
      });
    });
  }

  async confirmUserPassword(
    authConfirmPasswordUserDto: AuthConfirmPasswordUserDto,
  ) {
    const { email, confirmationCode, newPassword } = authConfirmPasswordUserDto;

    const userData = {
      Username: email,
      Pool: this.userPool,
    };

    const userCognito = new CognitoUser(userData);

    return new Promise((resolve, reject) => {
      userCognito.confirmPassword(confirmationCode, newPassword, {
        onSuccess: () => {
          resolve({ status: 'success' });
        },
        onFailure: (err) => {
          reject(err);
        },
      });
    });
  }

  ...

Enter fullscreen mode Exit fullscreen mode

Pretty much the same situation on the controller where we add two new routes for the API, calling the respective service methods:

auth.controller.ts

...

  @Post('/forgot-password')
  @UsePipes(ValidationPipe)
  async forgotPassword(
    @Body() authForgotPasswordUserDto: AuthForgotPasswordUserDto,
  ) {
    return await this.awsCognitoService.forgotUserPassword(
      authForgotPasswordUserDto,
    );
  }

  @Post('/confirm-password')
  @UsePipes(ValidationPipe)
  async confirmPassword(
    @Body() authConfirmPasswordUserDto: AuthConfirmPasswordUserDto,
  ) {
    return await this.awsCognitoService.confirmUserPassword(
      authConfirmPasswordUserDto,
    );
  }
 ...

Enter fullscreen mode Exit fullscreen mode

Now let's test the new endpoints:
Forgot Password

Password reset email

Email confirmation

Login

Boom!! Everything seems to be working flawlessly.
That's all, folks.
I hope that this series helps you to understand better the Cognito and javascript integration. And you can find the repo here.

Follow me on Dev, Medium, Linkedin or Twitter to read more about my tech journey.

Top comments (6)

Collapse
 
crazyoptimist profile image
crazyoptimist • Edited

Thanks for sharing!

Could you let me know why it's _audience instead of audience in jwt.strategy.ts?

From reading the docs of passport-jwt, it's audience and I couldn't find _audience anywhere. But only _audience works and not audience. That's weird and I feel blind here :(

Collapse
 
fstbraz profile image
Fausto Braz

Is indeed audience, sorry for the typo, corrected

Collapse
 
vic3nnt profile image
Vicent Pérez

Very nice guide Fausto. Thanks very much!

Collapse
 
manikantyml profile image
Manikant Upadhyay

Hi, I was trying to access the Protected API via Postman with Login Access token, But getting Unauthorized!

Collapse
 
vic3nnt profile image
Vicent Pérez

Have you add the access_token to the Authorization header? Other possibility is that your env variable for the AWS_COGNITO_AUTHORITY is not correct thus the guard can not validate your token.

Collapse
 
manikantyml profile image
Manikant Upadhyay

Yeah that was the case! Thank you