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:
- To be able to communicate with the Cognito service, Amazon Cognito Identity SDK
- For validating request body parameters with conditions, class-validator
- Enabling serialize/deserialize objects based on some criteria, class-transformer
- To get the signing keys for the JWT tokens jwks-rsa
- Passport to act as auth middleware to authenticate requests
- Passport strategy for JWT passport-jwt
- Passport utilities for nestjs @nestjs/passport
- Passport jwt types @types/passport-jwt
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"
}
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 {}
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;
}
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' },
];
}
}
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();
}
}
We can run npm run start:dev
. If everything builds fine, we should list the pokemon's at http://localhost:3000/api/v1/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;
}
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;
}
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);
},
});
});
}
}
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 this.awsCognitoService.registerUser(authRegisterUserDto);
}
@Post('/login')
@UsePipes(ValidationPipe)
async login(@Body() authLoginUserDto: AuthLoginUserDto) {
return this.awsCognitoService.authenticateUser(authLoginUserDto);
}
}
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:
Seem's to be working, the user is created and verified. Let's test the login:
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 😊
Top comments (2)
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 :)
Yeah, you're right, corrected and updated, thanks