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 {}
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 };
}
}
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();
}
}
Now, If we don't pass the bearer token produced when we log in, we will get a 401 Unauthorized
from the endpoint:
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;
}
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);
},
});
});
}
...
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);
}
...
After the tests, all seems to be working correctly:
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;
}
...
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;
}
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);
},
});
});
}
...
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,
);
}
...
Now let's test the new endpoints:
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 (7)
Thanks for sharing!
Could you let me know why it's
_audience
instead ofaudience
injwt.strategy.ts
?From reading the docs of
passport-jwt
, it'saudience
and I couldn't find_audience
anywhere. But only_audience
works and notaudience
. That's weird and I feel blind here :(Is indeed audience, sorry for the typo, corrected
very useful for me <3
Very nice guide Fausto. Thanks very much!
Hi, I was trying to access the Protected API via Postman with Login Access token, But getting Unauthorized!
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.
Yeah that was the case! Thank you