In the domain of computer security, authentication and authorization are fundamental processes. Authentication, the initial step, validates a user's identity through methods like passwords or biometrics. Once authenticated, authorization comes into play, determining the user's permissions and access rights. It's crucial to note that authentication precedes authorization, establishing the user's identity before enforcing access control measures.
The purpose of this article is to provide a step-by-step guide for implementing authentication system in a NestJS project using the Passport middleware module.
Our authentication system will embody the key attributes of an exemplary system: it will be secure, easily extensible, and simple, as every good authentication system should be.
If you want to look at the full code you can check out my repo on Github.
❓ Why you should use Passport Module to implement your authentication service
For implementing authentication in your NestJs App I highly recommend relying on the Passport module. It enhances security through its robust strategies like JWT and OAuth, aligning with industry best practices. Among other things, it supports a plug-and-play architecture, enabling the addition of new authentication methods or the replacement of existing ones without major code modifications. This means you can effortlessly integrate custom authentication strategies, ensuring scalability and adaptability for your app's future needs. In other words, Passport ensures a smooth developer experience, making it easy for you to understand and implement authentication mechanisms.
Define a model (e.g., a class or an interface) for your users, including necessary fields such as email, password, etc. The user service responsible for all interactions with the database regarding users includes methods to create, find, update, and delete users. However, notice that the signup method must be handled by the authentication service.
Your authentication module should be (at least) responsible for the following crucial tasks:
Create an AuthService service that will inject necessary services, such as UsersService for interacting with the user database and JwtService for handling JWT tokens.
AuthService Methods:
❓ Why is hashing and salting passwords mandatory?
A salt is simply a random data used as an additional input to the hashing function to safeguard your password. The random string from the salt makes the hash unpredictable.
A password hash involves converting the password into an alphanumeric string using specialized algorithms.
Hashing and salting are irreversible and ensure that even if someone gains access to the hashed passwords, they will not be able to decrypt them to recover the original passwords.
Hystorically bcrypt is recognized as the best hashing algorithm. However, in terms of robustness against all the new cryptographic attacks targeting hashing algorithms, the current clear winner is argon2. However, since the “youth" (2015) of this algorithm, I chose to use bcrypt
You need to create a LocalStrategy that extends PassportStrategy.
This authentication method is primarily used for authenticating users based on local credentials such as usernames and passwords. By default the strategy looks for a username field, in the code below we override usernameField to look for an email field since we want to login with an email.
The JwtStrategy validates the token sent by the user. It extracts the user ID from the token and looks it up in the database.
This authentication method is used for authenticating users based on the presence and validity of a JSON Web Token (JWT). It extracts the user ID from the token and looks it up in the database.
Create endpoints for signup (register) and login (login) that both return JWT tokens. For the login endpoint, the @UseGuards decorator is applied to the login method, indicating that the AuthGuard named 'local' should be used, that will trigger the LocalStrategy previously implemented.
❓ Passing the Access Token to the client
In this tutorial we pass the access token to the client in the response body for simplicity reasons, but it can also be done with a cookie. Since cookies are automatically included in every HTTP request, it simplifies the process of including the access token without explicit action from the client.You can read this article Cookie-Based Authentication vs Token-Based Authentication to choose the most adapted method to your case.
Your AuthModule should import the JwtModule to handle JSON Web Token (JWT) generation and verification.
The registerAsync method allows for asynchronous configuration, facilitating the use of the ConfigService to retrieve the JWT secret from environment variables stored in a .env file.
Storing sensitive configurations in a .env file as environment variables is preferred because it enhances security and avoids accidental exposure in version control systems (⚠️ don’t forget to add it in the .gitignore ). This practice facilitates easy configuration management across different environments while preserving confidentiality and minimizing the risk of inadvertent information disclosure.
❓ Why should you use a whitelisting strategy ?
To ensure the security of your app, the best practice is to choose a whitelisting strategy. Whitelisting, involves explicitly allowing only approved elements or actions, providing precise control over permissions in a system. It is considered a better practice than blacklisting as it reduces the attack surface by permitting only known and necessary entities but also minimizes developers' errors resulting from oversights or thoughtlessness.
In our context, to implement whitelisting you basically just have to create a guard for JWT-based authentication, and a decorator to designate public routes exempt from JWT authentication. This strategy offers a granular and flexible approach to securing specific endpoints.
First, create the following decorator:
Then you can add the following JwtGuard class:
Within the providers array, the provider for the JWT guard is registered using the APP_GUARD token. This provider specifies the use of the JwtGuard class as the global guard for the entire application.
Now if the routes are not marked with @Public(), every incoming request undergoes JWT authentication.
For example, in the GET endpoint below, the global guard validates and decodes the JWT access token and attaches the user information to the request object, making it accessible in the controller methods.
You have now the option to apply the @Public() decorator to methods or classes that don't require JWT authentication. In our scenario, we apply this decorator to the AuthController.
In order to make your code more readable and transparent, you can create your own custom decorator and reuse it across all of your controllers. It’s a very good practice to avoid code duplication.
Depending on the specific requirements of your project, (e.g. Role-based access control), you can create numerous and various decorators and guards to address diverse authorization scenarios. For this article we will only create a @User() decorator that extracts the user information from the request object in the NestJS execution context.
Then you just have to pass it to your controller to retrieve the user information encapsulated in your request.
In conclusion, crafting a secure and efficient authentication system in a NestJS application is a meticulous but easy process. We have integrated Passport to handle various authentication strategies, used JWT tokens to enhance security, and the global guards to ensure consistent authentication across the entire project. By prioritizing security, simplicity, and extensibility in our implementation, we have established a foundation that not only safeguards user data but also allows for seamless integration of future features.
By following this step-by-step guide, you can establish a robust authentication system in your NestJS applications, promoting secure user interactions and data protection. The provided code snippets serve as a foundation for building upon and adapting to specific project requirements (Role-based access control, OAuth).
However, I did not get into refresh token even though they are an essential component of secure authentication systems, particularly in scenarios where long-lived sessions are required. A deep dive into implementing refresh tokens in a NestJS context could be valuable for you if you aim to build robust and secure authentication systems.