Nest.js Authentication
This article is Part 6 in a 10 Part Series.
- Part 1 - Bootstrapping a Nest.js Project on Ubuntu
- Part 2 - Create Nest.js Migrations
- Part 3 - Create Nest.js CRUD Resources
- Part 4 - Create Nest.js Seeders
- Part 5 - Create a Customer Module in Nest.js
- Part 6 - This Article
- Part 7 - Create a Products Module in Nest.js
- Part 8 - Create a Stripe Module in Nest.js
- Part 9 - Create an Orders Module in Nest.js
- Part 10 - End to End Tests for our Storefront Backend
Continuing with the series, in this iteration of the tutorial we shall be focusing on implementing a way to keep track of our customers, as well as some basic security for our application. For starters, we shall add functionality for registering customers, logging them in as well as logging them out. We will need this as each order in our database will be attached to a single customer. All the above functionality will be contained within an application module.
In the typescript/javascript world, the most popular method for authenticating users of a system is through JWT authentication. In this method, once a user successfully logs in to the application, a JWT authentication token is sent to the user’s browser via a cookie in the server response. This token is usually valid for a short period of time, and the user can utilise it to make subsequent requests to access protected resources on the application.
In case of any issues, you can refer to my Github repository here.
Authentication Flow
Here’s a sequence diagram of our authentication flow, from registration to login to logout:
It might look intimidating at first, but we shall tackle all the various stages above step by step.
Install Redis
We’ll be using redis to store our refresh tokens (until the user logs out). We will therefore need to install it first. Here’s how to do so on Ubuntu:
sudo apt install redis-server
It is strongly recommended to secure redis post installation by enforcing authentication. In order to so, check out the tutorial here, as well as the first comment in the tutorial.
Install Install bcrypt
npm i @types/bcrypt bcrypt --save
Install passport and JWT
npm i @nestjs/passport @nestjs/jwt passport passport-jwt @types/express @types/passport-jwt --save
In order to secure our JWT tokens from being tampered with, we shall create two RSA keypairs to sign our JWT tokens, which is the approach recommended by the Nest.js core developers.
Generate JWT Access Token keypairs
Within the project root, run to create the keys directory:
mkdir rsa-keys
Use the ssh-keygen command to generate a 4096 bit RSA private key in the PEM format:
ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
Use the openssl command to generate a correspoding RSA public key also in PEM format using the previously generated private key as an input:
openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
Check if the private key has been successfully created by printing it out to the console:
cat jwtRS256.key
Check if the public key has been successfully created by printing it out to the console:
cat jwtRS256.key.pub
Rename the private key to a more memorable name:
mv -v jwtRS256.key access-token-private-key.key
Rename the public key to a more memorable name:
mv -v jwtRS256.key.pub access-token-public-key.pub
Clear the console:
clear
This last step is recommended since you would not want an attacker being able to view a copy of your keys via the terminal.
Generate JWT Refresh Token keypairs
Repeat the same steps as above when creating the access token keypairs in order to create the refresh token keypairs Within the same directory. This time rename the outputted files to refresh-token-private-key and refresh-token-public-key respectively:
cd rsa-keys
ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
cat jwtRS256.key
cat jwtRS256.key.pub
mv -v jwtRS256.key refresh-token-private-key.key
mv -v jwtRS256.key.pub refresh-token-public-key.pub
Clear the console:
clear
This last step is recommended since you would not want an attacker being able to view a copy of your keys via the terminal.
Add new configuration to app module
Here is how my updated app.module file looks like. I have imported the JwtModule from @nestjs/jwt, ConfigModule and ConfigService from @nestjs/config, CustomersModule (that we previously created) as well as the fs utility (in order to access files in our local filesystem). I also created and imported some strategy files which I will include shortly.
Update Secrets File
Don’t forget to add the references to the RSA keys generated above to your secrets file (.env), as well as the redis configuration.
# Access Token Secrets
JWT_ACCESS_TOKEN_PUBLIC_KEY='rsa-keys/access-token-public-key.pub'
JWT_ACCESS_TOKEN_PRIVATE_KEY='rsa-keys/access-token-private-key.key'
JWT_ACCESS_TOKEN_EXPIRATION_TIME=600s
# Cookie Secret
COOKIE_SECRET=thisisateststringforcookies
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
REDIS_USERNAME=your_redis_username
REDIS_PASSWORD=your_redis_password
REDIS_PREFIX='development:'
# Refresh Token Secrets
JWT_REFRESH_TOKEN_PUBLIC_KEY='rsa-keys/refresh-token-public-key.pub'
JWT_REFRESH_TOKEN_PRIVATE_KEY='rsa-keys/refresh-token-private-key.key'
JWT_REFRESH_TOKEN_EXPIRATION_TIME=600s
Update and validate environment variables
Don’t forget to also update and validate your environment variables:
Add Cookie Secrets to the main.ts file
Since we’ll be signing our cookies to prevent them from being tampered with, we’ll have to add a signing key as a parameter to cookie parser in our main.ts file:
Strategies
In the Typescript and Node.js world, authentication is done by means of strategies. Since Nest.js doesn’t enforce any default authentication strategy, we will have to implement one for ourseleves from scratch. In order to do so, we will be using the passport module, which we had added earlier on in our project. We will be creating three different strategies to handle authentication in our application: local, refresh and JWT.
The local strategy:
This strategy extends from the parent PassportStrategy, and it directs the passport module on what to do once a user supplies us with a username/password combination. By default, the local stategy expects a username/password combination, but we instead direct it to anticipate an email field in place of a username. Next, in order to validate our customer, we direct it to the getAuthenticatedCustomer method in our AuthService.
The JWT strategy:
This strategy directs our application on how to validate JWT tokens - which the API will be sending and receiving by means of signed cookies. First, the JWT token will be fetched from the cookie through the ExtractJwt library. We will be using the public key that we created previously to decode the JWT access token. By default, the passport module will return a 401 Unauthorized error if a JWT token is expired/invalid, and it will do this behind the scenes. In the validate method, we instruct passport to return a customer object in the event the JWT token received via the cookie is valid. In the event no token exists, to throw an unauthorized exception.
The Refresh strategy:
This strategy will be used to validate refresh tokens. It is very similar to the JWT strategy, however, with a few differences:
Since we don’t intend to expire our refresh tokens, we tell the Passport to ignore the expiry of our JWT tokens. To avoid errors when parsing a refresh token from a cookie such as the error below:
[Nest] 284242 - 06/27/2022, 4:45:16 PM ERROR [ExceptionsHandler] Cannot read properties of undefined (reading 'auth-cookie')
TypeError: Cannot read properties of undefined (reading 'auth-cookie')
at RefreshStrategy.validate (/home/isaiah/Projects/nest_projects/storefront-backend/src/modules/auth/strategies/refresh.strategy.ts:40:48)
at RefreshStrategy.<anonymous> (/home/isaiah/Projects/nest_projects/storefront-backend/node_modules/@nestjs/passport/dist/passport/passport.strategy.js:20:55)
at Generator.next (<anonymous>)
at /home/isaiah/Projects/nest_projects/storefront-backend/node_modules/@nestjs/passport/dist/passport/passport.strategy.js:8:71
at new Promise (<anonymous>)
at __awaiter (/home/isaiah/Projects/nest_projects/storefront-backend/node_modules/@nestjs/passport/dist/passport/passport.strategy.js:4:12)
at RefreshStrategy.callback [as _verify] (/home/isaiah/Projects/nest_projects/storefront-backend/node_modules/@nestjs/passport/dist/passport/passport.strategy.js:17:45)
at /home/isaiah/Projects/nest_projects/storefront-backend/node_modules/passport-jwt/lib/strategy.js:123:34
at /home/isaiah/Projects/nest_projects/storefront-backend/node_modules/jsonwebtoken/verify.js:223:12
at getSecret (/home/isaiah/Projects/nest_projects/storefront-backend/node_modules/jsonwebtoken/verify.js:90:14)
We have to set passReqCallback to true. In a similar fashion to the JWT strategy, we use the refresh token public key to decrypt the token. Lastly, we extract the refresh token from the cookie and return it as a parameter.
Here are the authentication interfaces:
The Jwt payload interface the data that is returned once a JWT Access/Refresh token is decrypted.
The secret data interface represents the data that will be encoded within the signed cookie.
Authentication Guards
We will also be implementing guards, which are special classes that Nest.js uses to limit access to endpoints/routes in our application. They are the recommended method for performing authorization. I have created a jwt-auth guard, which we will be using later on to limit access to certain endpoints to logged in users, and a refresh-auth guard, which can only be accessed via a valid refresh token. Both the guards extend the AuthGuard parent class that comes with Nest.js.
Jwt Auth Guard:
Refresh Guard:
Auth Service
We will be implementing most of the actual authentication logic in the auth service. Since we will be using redis as a temporary cache for our refresh tokens, where they will be automatically exipred after one day, we will need to install te nestjs-redis node module so as to interface with redis:
npm install nestjs-redis --save
Now, onto implementing the auth service. Here is is:
The register method handles customer registration by first doing a password validation, then uses the bcrypt library to encrypt the customers password just before saving it together with all the other customer details. Since we have enforced a uniqueness constraint on our customer table, we want the method to throw a bad request exception if a customer attempts to register twice with the same email.
Customer logins are handled by the getAuthenticatedCustomer method, which calls the verifyPassword method to verify if the customer’s supplied password matches what is saved in the database, as well as the getByEmail method in the customers service that checks if a customer’s supplied email matches what is saved in the database. The createJwtAccessToken method creates the customer’s access token that an authenticated customer uses to access protected routes. The createJwtRefreshToken method creates the refresh token that is used to fetch a new access token once the previous one in use expires. Using the redis client, we explicity tell redis to store the refresh token for 86,400 seconds or 24 hours once it has been created an returned to the customer via a cookie. For security reasons, we encrypt the generated refresh tokens using bcrypt before saving them on redis, to prevent their misuse by unauthorized users, and also since refresh tokens are private data only meant for the customer.
We use the validateJwtRefreshToken method to validate the refresh token received from the cookie. To do so, we must first validate the customer’s ID, which we also fetch from the cookie, then if the customer exists on our backend, we reconstruct a redis key denoted by string: ${RedisKeys.RefreshToken}:${customer.id}:${refreshTokenId}
. We use this key to fetch the encrypted access token, then use bcrypt once again to compare the encrypted refresh token with the refresh token that has been received from the cookie. At this point, if the tokens don’t match we throw a bad request exception (http 400 error), otherwise we return a customer object.
Lastly, the removeJwtRefreshToken removes the refresh token from redis, and is called as soon as a customer logs out from our application.
I have also included an enum for storing the names of redis keys that the auth service will be using:
And another for storing the postgres error code:
Auth Controller
Here is our auth controller with the register, login, logOut and refreshToken methods, that call the services that we defined above.
We pass to our login method the decorator:
@UseGuards(AuthGuard('local'))
This tells our login method to use our local strategy that we defined earlier, and to restrict our login route via that strategy. Since we want to direct our response object to set cookies, we will need to pass the passthrough parameter like so:
@Res({ passthrough: true })
We are also using the library-specific method for handling requests and responses, so don’t forget to import their corresponding libraries from the express framework:
import { Response, Request } from 'express';
Next, we fetch the customer details via the request object, and proceed to create both our JWT access and refresh tokens. We store these tokens in a cookie called auth-cookie, which we send back in the response, together with a success message.
The refreshToken method is protected via the refresh guard in the line:
@UseGuards(RefreshGuard)
Meaning, only a customer with a valid refresh token can access it. In a similar fashion to the login method, it fetches the customer information via the request object, then generates a fresh JWT access and refresh token which are stored in a cookie named refresh-cookie, which is sent back in the response to the customer along with a success message.
The register method calls the similarly named method in the auth service in order to register our customer, while the logOut method logs out our customer by first removing the JWT refresh token from redis, then deleting the auth-cookie and refresh-cookie.
The cookie names enum:
Unit Tests
As is common in modern software development, it is a good idea to include tests with the functionality that we have just built. These tests have the main purpose of verifying the correctness of our logic, and making it easier to debug issues later on, as well as add new functionality without the worry of introducing breaking changes. Nest.js supports two types of tests: unit tests and end to end tests. Unit tests usually test functionality contained within a single class, usually a service or controller class, while end to end tests are for testing the functionality of an entire endpoint. We shall include unit tests for both our controller and service, as shown below. Later on, we will also include end to end tests for the controller as well.
Auth Controller tests:
For now, I have created just a basic initialization check for the controller class, since most of the functionality in the controller class will be tested later on via end to end tests:
Auth Service tests:
In the auth service tests, I have tried as much as possible to mock as many dependencies as I could, so as to prevent the calling of actual resources, as well as to make the tests as light as possible and to execute as fast as possible. Behind the scenes, we’re using jest as our testing framework, which comes with Nest.js by default. The describe blocks within the test spec explain pretty much what the given spec is required to do:
Here’s how the tests should look like when you run them:
Auth Controller:
Since it’s only one test case, you should see that only one test has run and passed.
Auth Service:
In total, the console should indicate that 10 tests have run and passed. In one of the authentication specs, I have added a console.log line to log the exception thrown when wrong credentials are supplied, but in your case adding this line can be optional.
Postman Tests
For completeness, we can proceed to do actual tests using a REST client such as Postman. You can use any REST client you prefer, but I’ve chosen Postman since it’s the one that I have the most experience in. In case you’re new to using Postman, I’d recommend creating a collection within Postman by first clicking: File > New. This will take you to the Create New screen. Under the Create New screen, choose Collection. Click on the newly created collection, then give it a more memorable name (if you want). Within this collection, click the Variables tab, then add a new variable called api with both an initial and current value of http://localhost:3000 like so:
Click Persist All.
You can then proceed to create new requests within this collection by right clicking it then selecting Add Request.
Here are how my tests look like. Kindly observe the contents of both the request and response bodies:
Registration:
Registering a customer with a duplicate email/phone number:
In order to test the entire authentication flow via Postman, we start by logging in using our customer credentials:
Click on Cookies to the right of the Postman window. You should see the following screen:
An auth-cookie has been sent to Postman after logging in. Click on auth-cookie. You should see the following screen:
Click Save. The cookie should now be saved within the client, and will be sent back to the server with every subsequent request we make. You should now see the following:
You can now proceed to close the Cookies window.
Here are the tests for our refresh token endpoint. Make sure you have first logged in as a customer with valid credentials:
If you click on the Cookies tab at the bottom, you should see that two cookies have been returned in the response: auth-cookie and refresh-cookie. Let us proceed to test if our refresh-cookie works well.
Click on the Cookies link to the right of Postman and you should see the screen below. The Manage Cookies dialog should now appear.
Click on the refresh-cookie that we received, then click Save.
The refresh-cookie should now be saved on Postman as shown below:
Finally, let us test if our log out endpoint is sound. We’ll start by navigating to our logout endpoint via postman and submitting the request - note, it’s a DELETE request:
We should receive a success message in the response body. If we click the Cookies link to the right of the screen, we should note that all the cookies that were previously saved on Postman have now been deleted:
We’re now done with our Postman tests. Kindly comment on any feedback that you might have.
References
- Authentication flow sequence diagrams:
- Nest.js Documentation: https://docs.nestjs.com