Server-Side Authentication with NextJS and NestJS — Part 1: The Basics

Server-Side Authentication with NextJS and NestJS — Part 1: The Basics

Two of my favorite frameworks when working in Typescript fullstack applications are NextJS and NestJS.

They give you sensible defaults for your app and make it easy to plugin additional functionality. One feature that most apps share is authenticating users.

In this series I will walk through a complete authentication flow with NextJS / NestJS. But I will not just provide you with copy and paste examples. I will give a primer on authentication and explain exactly why we make certain design decisions around our authentication. Let’s jump in!

The basics of authentication

Authentication in our app is responsible for making sure that we can identify each user that is trying to access resources on our app. On the other hand, authorization is responsible for making sure exactly which resources the user is allowed to access. In this tutorial we will focus on the authentication aspect.

In web applications (many of the concepts here are also applicable to native mobile and desktop applications) we generally need to enable the following functionality:

  1. A user can sign up for our services
  2. A user can log in to our service
  3. A user can prove who they are by providing credentials
  4. A user can log out of our service

Signing up can take many forms. The user might sign up with an OAuth provider like Google or Facebook. One of the most common sign up methods is by providing an email address and password.

By logging in, email and password or an external OAuth token are exchanged for some kind of credentials. These credentials are like a keycard. By using it the user proves who they are and that they should have access to our app.

Each time the user wants to access resources, they need to provide these credentials. On the server we then check if the credentials are valid and provide resources in return.

Sessions or Tokens — Server or Client

You can manage your authentication in two different places: on the server or on the client.

Server-stored authentication uses the concept of sessions. A session id is stored on the client. The server then uses this session id to look up which user the session belongs to and other information it needs to fulfill the request.

So, you are still storing some information on the client, but your important user information is stored in a database behind the server.

This approach has lost in popularity because it has some major drawbacks. One is that you need to store credentials in a database and therefore will need more resources. The data needs to be retrieved on every request — your app performance might be slower.

It is also harder to scale your app if you want to split it out into microservices in the future. If information about a session lives with the authentication service, all other services would need to make an additional request to the authentication service to retrieve the right user data.

Client-stored sessions, or often called token-based authentication, solve these issues. The client stores not a session id but all required information to identify the user. You could store arbitrary data with the token like company ids or email address. Whenever you make a request to the server this data is included in an auth header or cookie — rendering the additional database call unnecessary.

We will use token-based authentication by managing the session on the client.

Where should you store authentication data on the client?

There are two common ways to store credentials on the browser: local / session storage and cookies.

Cookies have been the go-to way of storing credentials for a long time. They provide some unique features that make them suitable for authenticating a user.

First, how is a cookie created? The server can set a cookie header on a request and with that save the cookie in the client browser on receiving the request. In ExpressJS you can easily set a cookie like so:

res.cookie('cookieName','cookieValue', { maxAge: 900000, httpOnly: true });

Cookies can have an expiration date. This is very useful since you don’t want your users’ sessions to last forever.

Local and session storage are newer Browser APIs that allow you to store larger amounts of data as key-value pairs. Session storage is persisted through page reloads, but is cleared when the user closes the tab or browser. Local storage is persisted even after the browser is closed.

You can set either storage versions with Javascript by using their APIs like so:

const { sessionStorage, localStorage }  = window; 

sessionStorage.setItem('key', 'value');

let sessionData = sessionStorage.getItem('key');

localStorage.setItem('key', 'value');

let persistedData = localStorage.getItem('key');

We will make use of both storage solutions throughout the tutorial. It is just important to understand that the two exist and why they are different.

What to store

We have discussed where we can store authentication information. Now let’s discuss what we can store.

In theory, you can store your authentication information however you want. As you saw in the examples above, both cookies and the storage APIs are simple key-value stores. You could simply store a user id in either of these and then make sure that the user provides the correct id.

However, there are a couple of problems with this approach. First, it is easy to fake and hack. If your user ids are simple incrementing integers a hacker could retrieve information by trying out different numbers when calling your API.

The second problem is that we might want to store additional information that is related to the user. We might need to know which company they belong to or know their access role. We could extend our key-value to include an object, but there is a popular standard that can help us solve both problems: Json Web Tokens (JWTs).

JWTs are an open industry standard to securely share claims between two parties. They allow you to encode your authentication information. You can play around with how JWTs work on the official website.

The important takeaway is that they allow you to both send an expiry time and also can be encoded with a private key. This makes it really hard to provide fake JWTs to your server for potential hacking attacks. JWTs can still be stolen, but they are hard to fake.

Many libraries exist to interact with JWTs both on the client and on the server side. We will make use of them through the popular Passport library that integrates with JWTs.

I have read many tutorials that contrast Cookie with JWT authentication. This is not a valid comparison. One is mostly a storage mechanism while the other is an encryption standard. Oftentimes JWTs are used together with local / session storage and that is why many confuse them with it.

You can use JWTs together with Cookies and that is actually what we are going to do in this tutorial.

Local Storage and Server-Side Rendering — A match made in hell

You might be wondering now why we are picking cookies for authenticating our app? Local / Session storage is a more modern api and seems more flexible.

The problem lies with how NextJS and all server-side rendered apps work. In server-side rendering (SSR) our client app runs on both the application server and the client browser. This means that we don’t have access to any browser APIs when our code is running server-side.

If you ever worked with NextJS, you probably know the infamous error log window is not defined.

We could check whether the user is authenticated on the client only, but that would defeat the purpose of using a SSR app in the first place. However, cookies are passed along with each request from the server. They can be accessed on both server and client side.

This concludes our fast introduction into authentication. In the next part of this series we will start implementing the strategies outlined here for our NextJS and NestJS application.