Best practices in building secure authentication and authorization systems in Nest.js

Authentication verifies a user's identity, while authorization determines what a verified user can access. There are several methods to implement these processes, including basic authentication, token-based authentication (like JWT), and session-based authentication. As with authorization, you would typically see role-based access control or claims-based mechanisms.

Let's go through them all.

Introduction

Authentication is the process of verifying the identity of a user, a device, or a system. It is a way of ensuring that the entity requesting access is indeed who or what it claims to be. This is typically achieved through passwords, multi-factor authentication, or biometric data.

Authorization, on the other hand, takes place after successful authentication and determines what permissions an authenticated user or system has. In other words, it defines what actions the user or system can carry out, what resources they can access, and what operations they are allowed to perform.

There are several methods used to accomplish authentication in the world of web development, with three of the most common being basic, token-based, and session-based authentication.

Basic authentication is a simple method that involves sending a username and password with each HTTP request. This method, while straightforward, is not the most secure and should only be used in scenarios where simplicity is prioritized over security.

Token-based authentication, such as JSON Web Tokens (JWT), involves generating a token upon successful login, which is then sent with subsequent HTTP requests to authenticate the user. This method is secure and stateless, meaning that the server doesn't need to keep a record of tokens, making it an excellent choice for scalable applications.

Session-based authentication, on the other hand, involves creating a session for the user after they have been authenticated. The server stores the session data and sends a session ID to the client. The client then sends this session ID in subsequent requests, allowing the server to authenticate the user based on this session data.

When it comes to building an authentication and authorization system, why not simply rely on a third-party service like Firebase or Auth0? While these services can be a great starting point, there are several compelling reasons to build your system.

Firstly, building your system allows for greater flexibility and customization. You have the freedom to tailor the system to fit your specific needs, whether related to security, user experience, or business logic.

Secondly, by building your system, you retain complete control over your data. With third-party services, your user data is stored on their servers, raising potential privacy and data ownership concerns.

Lastly, while third-party services can save time initially, they might not be the most cost-effective solution in the long run, especially for larger applications with a high number of users.

Basic authentication

Basic authentication is one of the simplest methods for HTTP authentication. It involves sending a username and password with each HTTP request to the server. The credentials are concatenated with a colon (username:password), base64 encoded, and sent in the Authorization header.

The server decodes the Base64 string and extracts the username and password to authenticate the user. While Basic authentication is simple and doesn't require cookies, session identifiers, or login pages, it has significant security issues. The credentials are not encrypted but merely encoded, which means they can easily be decoded and read. Therefore, it's strongly recommended to use this method over HTTPS to encrypt the credentials during transit.

Token-based authentication

Token-based authentication, specifically JSON Web Tokens (JWT), has gained popularity due to its stateless and scalable nature. In this method, the server generates a token upon successful authentication. This token contains a payload (a set of claims about the user) and is signed by the server. When a client sends an HTTP request, the token is included, typically in the Authorization header.

The server verifies the token signature and, if valid, extracts the user information from the payload. The stateless nature of JWTs means the server doesn't need to store session data, making this method highly scalable.

However, token-based authentication comes with its own set of challenges. JWTs can be vulnerable to Cross-Site Scripting (XSS) attacks because they are typically stored in local storage. Also, given that JWTs are self-contained and stateless, revoking or changing permissions immediately can be difficult.

Session-based authentication

Session-based authentication is a widely used method. When the user logs in with valid credentials, the server creates a session for the user and sends back a session ID in a cookie. The client stores this session ID and sends it along with each subsequent HTTP request.

The server then compares the session ID received with the list of IDs stored on the server (either in memory or a database) to authenticate the user. Once the user logs out or after a certain period of inactivity, the server destroys the session.

While session-based authentication can provide a high level of security, especially when combined with other techniques such as Secure Sockets Layer (SSL) encryption and HttpOnly cookies, it has its trade-offs. The server needs to store session data, which can impact performance and scalability, especially in large distributed systems.

Security in details

Let's explore the security of the three authentication methods we've previously discussed: basic authentication, token-based authentication, and session-based authentication.

Security of basic, token-based, and session-based authentication mechanisms

  • Basic authentication

Basic authentication is generally considered the least secure of the three methods. The primary reason is that it involves sending a username and password with every HTTP request, which makes it vulnerable to interception. Even though the username and password are Base64 encoded, this encoding is not a form of encryption and can be easily reversed. If these credentials are intercepted over an unsecured connection, they can be decoded, exposing the user's sensitive information. Hence, basic authentication should always be used over HTTPS, which encrypts the traffic between the client and server, making it harder for an attacker to intercept and decode the credentials.

  • Token-based authentication (JWT)

Token-based authentication is more secure than basic authentication. JWTs are signed by the server, and this signature is verified on every request, ensuring that the token hasn't been tampered with. However, JWTs can be vulnerable to Cross-Site Scripting (XSS) attacks, where an attacker can inject malicious scripts into trusted websites to steal tokens. Furthermore, since JWTs are stateless and contain user information, if a token is intercepted, an attacker could potentially extract sensitive user information from the token.

  • Session-based authentication

Session-based authentication is generally considered more secure than basic and token-based authentication. The server only sends a session ID to the client, and the actual session data (which could include sensitive user information) is stored on the server side. This makes session-based authentication less vulnerable to attacks that intercept the traffic between the client and server. However, it's worth noting that session-based authentication can be vulnerable to Cross-Site Request Forgery (CSRF) attacks, where an attacker tricks a victim into executing unwanted actions on a web application in which they're authenticated.

Enhancing security in the token-based authentication mechanisms

Even though token-based authentication can be vulnerable to certain attacks, some measures can be taken to enhance its security:

1. Use HTTPS

This encrypts the traffic between the client and server, making it harder for an attacker to intercept the token.

2. Short expiration times

Tokens should have short expiration times to limit the damage if a token is intercepted. This does mean users will have to re-authenticate more often.

3. Handle token storage securely

Rather than storing tokens in local storage, which is vulnerable to XSS attacks, consider storing tokens in HttpOnly cookies. These cookies can't be accessed by JavaScript, which helps prevent XSS attacks.

4. Implement refresh tokens

Refresh tokens are a way to obtain new access tokens without requiring the user to re-authenticate. They can be used to maintain a user's authenticated session while having short expiration times on the access tokens.

Enhancing security in the session-based authentication mechanisms

Session-based authentication can be further secured by adhering to the following best practices:

1. Use HTTPS

Using HTTPS encrypts the traffic between the client and server, preventing the session ID from being intercepted​

2. Session ID properties

The session ID should have specific properties to ensure secure session management. The session ID is a name=value pair that is assigned at session creation time and is shared and exchanged by the user and the web application for the duration of session​

3. Session ID name fingerprinting

The name used by the session ID should not be extremely descriptive nor offer unnecessary details about the purpose and meaning of the ID. Changing the default session ID name of the web development framework to a generic name, such as id, can prevent fingerprinting of the technologies and programming languages used by the web application​

4. Session ID length

To prevent brute force attacks, the session ID length must be at least 128 bits (16 bytes). This length is provided as a reference, and other implementation factors might influence its strength​

5. Session ID entropy

The session ID must be unpredictable (random enough). It must provide at least 64 bits of entropy (if possible, 128 bits is better)​

6. Session ID content (or Value)

The session ID should not be meaningful or disclose any information. It should be a random value generated by a secure random number generator. The session ID should not include user information, be predictable, or be vulnerable to information disclosure attacks​

7. Session ID secure transportation

The session ID should always be transmitted securely. If cookies are used to transmit the session ID, attributes such as 'Secure' and 'HttpOnly' should be set. The 'Secure' attribute ensures the cookie is only sent over HTTPS, while the 'HttpOnly' attribute prevents the cookie from being accessed by client-side scripts, reducing the risk of XSS attacks​4​.

8. Session ID life span

The session ID should have a limited lifespan and should expire after a certain period of inactivity or absolute time. This reduces the window of opportunity for an attacker to use a stolen session ID​5​.

So, is that it?

We could stop the article now, and you would have enough best practices that would help you navigate around the complexities of implementing your authentication system. However, I’d be doing you a great disservice if I did not share some practical tips and tricks that I’ve seen in the field.

Hopefully, these might help you shine the next time you’ll have to write some technical documentation.

Tips and tricks you should know

Dealing with JWT revocation

Regarding the revocation of JWTs, because they are stateless, there is no inherent mechanism to invalidate them. A common approach is to create a token blacklist:

1. Create a blacklist

When a user logs out or in any other event requiring token invalidation, the token is added to a blacklist.

2. Check against the blacklist

Whenever a request with a token arrives, the server checks the token against the blacklist. If the token is found in the blacklist, the request is denied.

3. Remove expired tokens

Periodically, the blacklist should be cleaned of tokens that have expired to conserve resources.

For the most part, you might want to consider something like Redis and, as mentioned, run a cron job to remove expired tokens. A good rule of thumb is that if you set the expiration date of your tokens to 1 hour, you should be purging expired tokens from the blacklist every 1.5 - 2 hours.

What data should I put in a JWT?

A JWT typically contains three parts: header, payload, and signature. The payload of a JWT, also known as the claims, is where the data is stored. This can include:

1. Standard claims:

These are predefined claims such as issuer (iss), subject (sub), audience (aud), expiration time (exp), not before (nbf), issued at (iat), and JWT ID (jti).

2. Custom claims

You can also define your claims. However, be cautious not to store sensitive information in these claims, as the content of a JWT can be easily decoded.

Typically, custom claims would contain information that is useful for user identification (such as a unique ID), but make sure you never place any sensitive data into a custom claim (like emails, passwords, personally identifiable data, etc.). You’ll do little to no harm if a JWT is leaked and decoded, and the only information present is a user ID.

Worried about resource usage for a session-based authentication system?

Session-based authentication requires the server to keep track of user sessions, which can consume more resources than token-based methods. Sessions can be stored in a variety of ways:

- Memory:

For simple applications, sessions may be stored in server memory. However, this does not scale well and can lead to issues if the server restarts.

- Caching Systems:

For their fast data access speeds, caching systems like Redis are often used for session storage.

My recommendation is to always start small; for the most part, you can get away with an in-memory cache. Nest.js does have a cache manager, which you can configure to be used either in-memory or with any caching system (like Redis). You can always mix and match cache sources (opening multiple instances).

Authorization

As developers continue to construct increasingly intricate applications in Nest.js, they face the challenge of authorizing users for specific actions, roles, or resources. Authorization, a process that determines what a user can and cannot do after they've been authenticated, is a critical aspect of web security. Multiple strategies exist for handling authorization, each with its strengths and weaknesses. We'll explore these approaches, focusing on Role-Based Access Control (RBAC) and Claims-Based Authorization, which are also well-documented in the official documentation.

One of the simplest approaches to authorization is role-based access control. With this approach, permissions are associated with roles, and users are assigned roles to grant them the necessary permissions. Another approach is claims-based authorization, where claims (typically user attributes or properties) determine what actions a user can perform. Some systems use attribute-based access control (ABAC), where permissions are granted based on a combination of user attributes, actions, and environmental conditions. Other systems might employ a more hierarchical model, where roles inherit permissions from other roles in a hierarchical structure.

Approach #1: RBAC

RBAC is a popular method for implementing authorization and for good reason. It is straightforward and maps well to traditional business roles and structures. A user can be granted a role that corresponds to their job function, and the permissions that role carries determine what resources they can access.

However, RBAC isn't without its drawbacks. The main limitation of RBAC is its lack of flexibility. In a dynamic application where access to resources needs to be finely tuned based on a variety of factors, the rigid structure of RBAC can fall short. For instance, if a user needs to have one-off access to a specific resource that isn't typically part of their role, this could necessitate creating a new role or modifying an existing one, which may not be efficient or feasible.

Approach #2: Claims-based authorization

On the other hand, claims-based authorization offers flexibility that RBAC can't match. Claims are essentially properties or attributes of a user that the system acknowledges as true. They could be anything from a user's email address to their shoe size. The flexibility of claims allows developers to construct sophisticated authorization rules that can adapt to diverse scenarios.

However, the flexibility of claims-based authorization can also be its downfall. More complex rules mean more complexity in the system overall. It can be harder to understand and manage what a user can and can't do because their permissions aren't consolidated into a simple role; instead, they're spread across various claims. In addition, if not properly managed, claims can proliferate, leading to what is known as a 'claims explosion,' where a user ends up with a vast number of claims that are hard to manage and maintain.

Which one should you choose?

As we've mentioned, always start simple and progress to more complex use cases, should you need to. As someone who has worked on large-scale backend systems in the past, RBAC has always served us well. It’s also surprisingly easy to implement using guards, which any Nest.js developer should be familiar with.

Claims-based mechanisms are hard to implement independently, so you would typically need a library like CASL. We can talk about policy guards and similar complex implementations in a separate article because it truly is a work of art to create a well-polished, secure, and easily maintainable claims-based authorization system.

Summary

Security is paramount in web application development, and this encompasses both authentication and authorization. Basic, token-based, and session-based authentication methods each have unique benefits and drawbacks. Basic authentication, while simple, may not suit most applications due to inherent limitations. Token-based authentication, specifically JWT, offers scalability and statelessness but demands careful token management. Session-based authentication provides a stateful and user-friendly approach, but it can require more server resources.

Irrespective of the chosen method, ensuring secure transmission of credentials, preventing token or session hijacking, and properly managing tokens or session identifiers are crucial. For JWTs, token revocation and managing the data contained within tokens are critical considerations. For session-based authentication, managing session lifecycles and session data storage is key.

Authorization strategies like RBAC and claims-based authorization are vital tools in the developer's arsenal. RBAC's simplicity makes it well-suited to applications with clearly defined and stable roles, while the flexibility of claims-based authorization makes it ideal for complex, dynamic applications with varying access requirements.

Building your own authentication and authorization system with Nest.js may be challenging, but with a clear understanding of these concepts and best practices, you can construct robust, secure systems tailored to your application's needs. However, remember that the control and customization offered by building your own system come with the responsibility of maintaining and updating it to meet evolving security standards.

Keep up with the latest developments in web security to ensure your application remains secure against emerging threats.

Find your next developer within days, not months

We can help you deliver your product faster with an experienced remote developer. All from €31.90/hour. Only pay if you’re happy with your first week.

In a short 25-minute call, we would like to:

  • Understand your development needs
  • Explain our process to match you with qualified, vetted developers from our network
  • Share next steps to finding the right match, often within less than a week

Not sure where to start?

Let’s have a chat

First developer starts within days. No aggressive sales pitch.