NestJS Authentication Deep Dive: Leveraging Passport, JWT, and bcrypt
BLOGS
- Posted on May 24, 2024
By Yunus (Application Developer)
- May 24, 2024
Introduction:
User authentication is a critical aspect of any application, providing a layer of security to protect user data and sensitive information.
Passport is the most popular node.js authentication library that is successfully used in many production applications. Nest applications can be integrated with the Passport library using the @nestjs/passport module. Passport uses the concept of strategies to authenticate requests. Strategies can range from verifying username and password credentials, delegated authentication using OAuth (for example, via Facebook or Twitter) etc. Please refer https://www.passportjs.org/packages/ to identify the strategies suitable for your application needs.Â
- passport-local: to authenticate a user based on username and password
- passport-jwt: to authenticate a user based on the JSON Web Token.
After an initial successful local username and password authentication by the user, corresponding JWT can be generated. This JWT can then be used to get authenticated when accessing subsequent protected routes within the application.
Pre-requisites
In a real world Nest application, following setup would have been completed before implementing user authentication. We will however not be discussing them in detail.
- Install Nest CLI
- Create a new NestJs project using the Nest CLI that generates a base project structure with the initial core Nest files and supporting modules
- Install @nestjs/config package and import its ConfigModule and use it to read the environment variables configurations from the .env file
- Install the associated client API libraries for TypeORM integration with PostgreSQL database
- Create a ‘database’ module and into that import TypeOrmModule and configure it using the ConfigService by reading the PostgreSQL database environment variables
Packages required
Following is a list of packages that are to be installed or commands to be executed to meet the prerequisites and to get the authentication function implemented.
Project Folder structure
The folder structure of the described NestJS application is shown below.
User Module
The user module represents the user store within our Nest JS application.The User entity and its DTO are shown here.
The UsersController has a user signup route /api/v1/users configured and it invokes the create() method of UsersService
The utils method hashpassword() makes use of the bcrypt package.
The findOneByUsername() method of the UsersService returns a full user object if the passed username matches, but otherwise returns a null object. We will soon observe that this method is invoked from an AuthService.
Apart from these methods, there are other methods too like finding all users, updating the details of a specific user or removing a user from the user store etc, however they are not related to the topic of user authentication and hence not relevant in this discussion.
Authentication requirements
For this use case, we have following authentication requirements:
- A client application authenticates with a username and password via a login route. Once authenticated, the server will issue a JWT.
- We will create a protected route that is accessible only to requests that contain a valid JWT. The client application can send the JWT received from step-1 (after successful login), as a bearer token in the authorization header on subsequent requests to prove authentication – this would be required to access the protected route/s.
- In case the routes are protected globally across the application or protected at a module-level, we will need to skip JWT authentication checks on a few of the routes. For example, protect all API routes of the user module but only keep the user signup route open / public.
Local authentication
Local authentication refers to when the client application or end-user sends in username and password for login, to a login API route.
A login route is set-up in the AppController of our Nest application as shown below. There is a guard named ‘LocalAuthGuard’ placed in front of the login route and so all login requests are first handled by this guard, before it hands over the processed result to the login() method.
In this case, the LocalAuthGuard uses the string parameter ‘local’ and so invokes the passport-local strategy for the incoming username and password parameters of the login request.
- A set of options that are specific to that strategy. These strategy options are passed by calling the super() method
- A “verify callback”, which is implemented by a validate() method and defines some kind of interaction with the user store (either checking if the user exists, or checking if credentials are valid or fetching more information about the user). In all these cases, if validation succeeds it returns a user object or returns null if it fails.
Note! Instead of using default property names (username and password) for login, if for example you are using ’email’ instead of username, then this needs to be included in the options when calling super(), example: super({ usernameField: ’email’ }).
In the validate() method, it uses the AuthService.
The validateUser() method checks if a user exists with the passed-in username. This is done by calling the findOneByUsername() of the UsersService. If the user exists and the passed-in password matches with the actual password of the user, then it returns the user object containing the id and the name of the user (after stripping off the sensitive details such as the username and password); otherwise it returns null. So essentially a user object or null is returned back to the LocalStrategy.
If null was returned, LocalStrategy throws a ‘401 Unauthorized’ exception, otherwise it returns the user object to the Passport. Passport, under the hood, then attaches the returned user information to the request object.
Now that the LocalAuthGuard has completed its authentication check, it hands over the result (i.e. the request object) to the login() method of the AppController. Refer to the related code-snippet above. Here it calls the login() method of the AuthService by passing in the ‘req.user’. This method then generates a JWT token from a payload created out of the user object passed to it. It makes use of the sign() method of JwtService.
Please also refer below to the Auth module to understand which other modules are imported here, which providers are registered here and so on.
Â
When importing the JwtModule, it is also registered with necessary settings such as secret key used, expiration time etc, and these settings are used when generating the JWT. LocalStrategy is registered as a provider and that is needed for the LocalAuthGuard to internally invoke its service
Note! JwtStrategy is also registered here and will be explained when we discuss about JWT authentication
Let us now perform a test by passing valid credentials to the login route.
Result: Upon successful local authentication, the corresponding JWT is returned
JWT authentication
JWT authentication refers to when the client application or end-user sends valid JWT as a bearer token in the authentication header (or through other means) of subsequent requests to access protected API route/s.
First, let us examine a protected route /api/v1/protected that is setup in the AppController.Â
There is a guard named ‘JwtAuthGuard’ placed in front of this route and so an incoming request is first handled by this guard, before it hands over the processed result to the getHello() method.
In this case, the JwtAuthGuard uses the string parameter ‘jwt’ and so invokes the passport-jwt strategy for the incoming request.
In the JwtStrategy, we call super() by specifying the JWT settings as options.
Under the hood, the passport-jwt then uses these JWT settings to extract the bearer token (if it exists) from the authorization header of the incoming request, then verifies the JWT using the secret key specified here. Thus it is worth noting that the secret key provided here and the secret key provided when registering JwtModule in AuthModule are the same.
If the verification fails, then the JwtStrategy throws an unauthorized exception. If JWT verification succeeds, the decoded payload is passed as parameter to the validation() method. The validation() method then returns a user object to the Passport with the properties we choose to include. Passport, under the hood, then attaches the returned user information to the request object.
Now that the JwtAuthGuard has completed its authentication check, it hands over the result (i.e. the request object) to the getHello() method of the AppController. Refer to the related code-snippet above. The getHello() method is then able to include the value of ‘req.user’ property in its response back to the client.
Let us now perform tests to check the response to the requests made to the protected route:
a) Case 1: JWT is not provided in the authorization header of the request
Result: Authentication failed, when accessing a protected route without JWT included in the authorization header
b) Case 2: Pass valid JWT as bearer token in the authorization header of the request
Result: Upon successful JWT authentication, expected response is returned from the protected route
Exclude JWT authentication on a selected route
There will be situations when it is convenient to protect all API routes of a specific resource at a global or module-level, instead of placing JwtAuthGuard in the front of each route.To protect all routes at a global level, the JwtAuthGuard needs to be registered as a provider in the AppModule. Instead, to protect all routes of a specific resource alone, the JwtAuthGuard can be placed at the controller level.
Find the configuration made below to protect all API routes of the user module except the user signup route which is made public.
The @UseGuards(JwtAuthGuard) is placed at the controller level, so all routes defined in this UserController are protected by default. However the @Public decorator sets the metadata defined below to the execution context of this specific signup route alone.
JwtAuthGuard implementation is also enhanced as shown below, to check if the execution context contains this metadata. If ‘isPublic’ is true, then it skips the authentication check, without activating passport-jwt strategy and thus not performing JWT verification.
Let us now perform a test to check the response to the request made to the signup route, without including JWT in the authorization header.
(Token is not provided)
Result: Expected response returned from the public route (without JWT in header)
Conclusion
In conclusion, mastering user authentication in NestJS using Passport, JWT, and bcrypt is crucial for building secure and reliable applications. By implementing local authentication with Passport, we ensure that users can securely log in using their credentials. The integration of JWT allows for stateless authentication, enhancing scalability and performance. Leveraging bcrypt ensures that user passwords are securely hashed, protecting sensitive information from unauthorized access.Â
Furthermore, by protecting routes with JWT guards, we can control access to specific endpoints, ensuring that only authenticated users with valid JWT tokens can access protected resources. Additionally, the ability to selectively mark certain routes as public provides flexibility in designing API endpoints, allowing for a combination of public and private routes within the same module. As we’ve demonstrated throughout this guide, NestJS offers powerful tools and techniques for implementing robust authentication mechanisms, empowering developers to build secure APIs with confidence.