Authentication in Next.js 14

Imagen de vecstock en Freepik

I’m deeply in love with NextAuth but this is the most confusing moment if you arrive at the library. The problem here is that we are in the middle of a version change and wherever you look at, you’ll find contradictions, even in the official documentation.

Probably you are asking yourself, then why are you writing this? First of all, I’m curious about the changes, this people always outperforms and I want to know what’s their next step. On the other hand, when Vercel published their last version, Next.js 14, they also published an incredible learning resource where they suggested to use the beta version of NextAuth. With all these evidences I told myself, you need to upgrade your knowledge as soon as possible.

At the moment of writing this article, Next.js 14 is cutting-edge, so you’ll need to install some dependencies that aren’t stable. Once again, remember, do it at your own risk. And for sure, maybe this is not the best idea for projects that are in production.

Installing NextAuth

Probably the most easy way to get authentication almost out of the box in a Next.js based project is using NextAuth. The project have been around for a while and it’s widely used. However, the support for the new app router is still in beta. At this moment NextAuth is a little messy. They have been called NextAuth for a while but after reaching their version 5 it looks like they are renaming the project to Auth.js.

It seems that there won’t be differences between installing the last NextAuth beta version or installing directly Auth.js library.

To install NextAuth you just need to run the next command in the terminal at the root folder of your project.

npm i next-auth@beta

Next step is to generate a secret key for your app, this is used to encrypt cookies. It can be whatever string that you want, but for sure it’s better if you generate it randomly. As its own name suggest, please keep it secret 😉

To generate the key you can use, for example:

openssl rand -base64 32

Now you must store the output of that command in your .env file with the variable name AUTH_SECRET

AUTH_SECRET=yoursecret

Once you go to production, these variables must also be set at Vercel project.

Basic configuration

Now create a auth.config.ts file in your project’s src folder. The main objective of this file is to export a valid authConfig object that will be used to configure the behavior of the library.

In this object we can fine tune a lot of parameters that help us to customize our experience with NextAuth.

One important tweak here appears in the “callbacks” section. We are adding that “authorized” function that will be our watchdog, the piece of code that will decide if someone can access or not to any of our app routes. The logic is pretty much in our hands here, at the end, if this function returns false then the user will be redirected to the login page and if it returns true she will be able to reach the route. The degree of complexity you choose to include is at your discretion.

This function will be used before a request is completed, so if someone is unauthorized, the page that is being accessed won’t even start rendering.

To make this work properly you need to define a middleware that uses this NextAuth configuration. As a convention, our middleware definition should be in /src/middleware.ts file.

Finally we will generate a third file, called auth.ts, where we will export some functions provided by NextAuth.

Your app’s src folder should looks like:

Setting up an authentication provider

Next step will diverge from the incredible documentation provided by Vercel because in their tutorial, they use a custom authentication provider but I’m more interested in using Google as the provider.

To nail this goal, we need to reach our Google’s console and generate a new OAuth client ID. The process is quite straightforward but if you never did it before, maybe it make sense to let it documented here.

On the first hand go to Google’s console and click on create credentials. Once you click in “Create credentials” you’ll see a small menu with a couple of options, select OAuth client ID.

Now you’ll need to configure the consent screen to proceed.

Due to I want to make it accessible for anyone interested in testing our app, I’ll choose this option.

The only other value values that you should take care of are:

And finally the Scopes. The app scopes determine which user data your app will have access to. In our case we only need the user account and profile data just to show here a wonderful avatar.

And after that you should click again on create credentials, select OAuth Client ID and fill the form that will appear.

The only two key steps that you should take into account is to set the Authorized JavaScript origins and Authorized redirect URI’s as shown in the picture above.

Of course, this values are only for development, in production you should set your app’s real domain.

Ending with this process, Google will show you your credentials on the screen, you can use the button to download your credentials in JSON as you’ll need them in a moment or use the copy buttons.

We need to store these values in our .env file.

Now we can go to our /src/auth.config.ts file and configure the Google provider. That file now should look like this:

At this point we just have a working authentication solution based on Google OAuth infrastructure. But I want to add an additional layer. I want to manage my sessions using my own database.

Auth Api route

We are getting closer to the end of this post, now we need to create a new folder structure this must be located in /src/app/api/[…nextauth] and must contain a route.ts file.

Inside this route.ts we will add the two functions that we are exporting in our /src/auth.ts file. When we receive a request that uses the http “GET” method it will be served by GET function and if we receive a POST, well I believe that you can guess what will happen then 😉

Managing sessions in your application’s database

In the last projects that I’ve been working on we decided to use Prisma to abstract our persistence infrastructure.

Probably I will write a new post about that, but I also believe that Prisma documentation is pretty good. So I won’t focus on its configuration process now. I will consider that you have a Prisma ORM in place up and running.

If that’s the case, then you can use one of the database adapters that NextAuth provides. The goal of this tool is to help us persist all session related data independently of the chosen provider.

To install all the needed dependencies for this to work properly you just need to execute the next command lines:

npm install @prisma/client @auth/prisma-adapter
npm install prisma --save-dev

If you have installed Prisma before in your project installing only “@auth/prisma-adapter” should do the trick.

Adding this adapter only requires you to make a little update in /src/auth.config.ts file.

Basically it consists in adding the import for PrismaClient and configuring PrismaAdapter as the adapter that we want to use. We also configure it to use JWT for session maintenance.

One important detail is that Google only provides Refresh Token to an application the first time a user signs in, so if you were doing tests and didn’t take care of this and you think that you’ll need that token, take a look at this link.

In fact, the lines from 33 to 39 are absolutely optional but they address that small problem. You can add these to force google to send this data again.

After that you’ll need to update your Prisma Schema to add the models that NextAuth needs to keep track of your future users sessions.

This model is provided by NextAuth on its documentation you only need to update your schema.ts and update your database using Prisma cli tools.

In my local environment I use a couple of Docker containers to emulate how my app will be deployed in the real world, one of them is an image of adminer, that is a kind of database CRUD web UI.

As you can see, before login in for the first time, my database is empty.

If you remember, we defined an API route inside our app folder it’s time to use it. We have signIn and signOut endpoints for free. Don’t you believe me? Ok just point your browser to http://localhost:3000/api/auth/signin

If you click on the button you’ll see the login procedure that you are used to when it comes to google’s ecosystem.

But the best part comes when you go to your development database an see it populated with your new user’s data.

A word about session strategies

NextAuth gives us the chance of using different strategies when it comes to persist the session data. Probably you noticed the value “jwt” in the session field of our last auth.config.ts screenshot. That field can take only two values “database” or “jwt”.

If you tried to use the database only strategy chances are that you noticed a weird error related to JWE encryption or something similar. At the moment of writing this post PrismaClient can’t run in Vercel’s edge functions, so we are forced to use the hybrid approach as stated here.

Under the hood we are using a hybrid approach, the session data is persisted in our database, but we still need to use JWT at some point to set an authentication cookie for our clients.

As you can see, here is the cookie.

Role based authorization

One of the most common authorization patterns is role based one.

With this approach you only need to assign a set of privileges to different user roles as if they where the groups in the operating systems realm.

We can implement this pattern in an easy way using NextAuth and the Prisma adapter.

In your schema definition file you’ll just need to update the user model in the way I show you in the next screenshot.

As you can see I’ve just added an enum with the different roles that I plan to support and just a new role field in the User model.

Remember to update your models in the database and making the migration.

After that you can create a profile callback inside the configuration of your Google provider, and let me repeat it, the profile callback goes inside the configuration of your Google Provider as you can see in the next screenshot.

The object that you return in this function will be the one used to create a new user in the database. It’s important that you return an object that adheres to your Prisma model for the User entity, if you don’t do that, you’ll begin to experiment a lot of weird errors. On the other hand, you can enrich your user definition as much as you want inside this callback.

In my example, if a new user arrives to my app, it will receive an “USER” role automatically. If after that, I update that user in my database, my changes will persist as expected.

Finally, if you want to have access from your client side to the role, you should update both the jwt and session callbacks.

Authenticating API routes

The thing that impressed me the most of this version is how they have encapsulated all the functionality for authenticating server, client or api routes inside the auth function.

For this example I will use an small API route that retrieves all the users from the database and returns them. To implement it just create a route.ts file in a folder structure that makes sense for you inside your app folder. I will create it in app/api/v1/user/route.ts

After that you just need to define a function with the name of the HTTP method that you want to serve, a function called GET for “GET” requests, another called POST for “POST” requests and so on.

In this example I just gonna define a single GET function but, what’s new here is that if you pay attention the whole body of our function it’s wrapped by auth function. As you can see if Auth.js authenticated the request, we will have access to req.auth that will store the session object.

If req.auth isn’t in place we can be sure that the request wasn’t authenticated.

From the client side accessing that endpoint is as simple as hitting it using fetch or axios or whatever after the user is authenticated.

Finally, if you want to use another client like Postman, Thunder client, or whatever you only need to add a Cookie header that must include authjs.csrf-token and authjs.session-token values.

Resources

Internationalization in Next.js 14 projects

Imagen de la tierra vista desde el espacio
Image from Freepik

The last two versions of Next.js adhered to a new paradigm, the App router. This introduces a couple of important changes that impacts directly in the usual approaches used to solve common problems, one of them is internationalization.

In this post we will use next-intl as a solution for this problem.

Installation and project basic tuning

To add this to our project we will only need to install it as a dependency.

At the moment of writing this article, the internationalization support is in a beta stage and they state in next-intl web that they have only tested this on Next.js version 13.5.1 and they set this version in their package dependencies.

As far as I know, there aren’t any real breaking changes from this version to the 14 that should affect next-intl, but you’ll need to force the installation. Well this is a decision that you should take at your own risk 😉

npm install next-intl@3.0.0-beta.19 --force

To make internationalization work properly in a Next.js 14 project there are a couple of moving parts that should be taken into account and the first time they aren’t so intuitive.

As a first step we will change our project’s folder structure.

We will put all our app folder contents inside a new one called [locale]

At this moment, if you navigate to your local development using http://localhost:3000 as usual, your UI will seem broken.

If your IDE hasn’t done it yet, you should modify some of your imports to match the new paths of your files.

As you have seen now it’s expecting a new dynamic segment, so if you try to access, for example to https://localhost:3000/whatever/ you’ll be facing exactly the same ui that you were working on a moment ago.

Message files

If you are used to internationalization libraries you can skip this section, if aren’t don’t worry, there isn’t so much magic here.

Usually internationalization relies on some files or third party services that stores the proper translation for a term in a key-value fashion.

Every time that you need to show a user a string needs to be translated you rely on some methods provided by your library of choice that, at the end will look at these stores to retrieve the value related to the key that you pass them.

We’ll use the simplest approach that is to generate a couple of files, one per each language that we plan to support. In every one of this files we will store these key-value pairs. In fact, you can organize it with more levels of anidation.

One outcome of the use of Next.js server components is that we don’t need to take care of the size of this files as they will be used only in the server, having no impact in the final bundle size.

Well, you can put your translations files wherever you want in your project directory, for this example we will store the files in a new “locales” folder at the root of our “src” folder.

Inside this folder you should place one translations file for each of the languages that you are planning to support.

The structure of these files is quite simple.

Connecting the dots

With all these pieces in place we’ll need to add two files.

On the first hand next-intl needs to generate a configuration object once per request and this is the place where we will feed our translation files so, if you placed yours in a different place, take care of changing the path.

You must create an “i18n.ts” file in the root of your “src” folder

The contents of this file should look like this:

Now we need to modify next.config.js file in the root of our project. And change it from it’s default value:

To this:

As you can see, with this approach, if you have modified this file prior, add all your configurations to the nextConfig object and they should work out of the box.

The middleware

The last step in this configuration journey ends when you put a middleware in place that will check each request and try to detect which language it should use when it comes to render texts on screen.

You can read more about this middleware in next-intl documentation, but in short there are two different strategies for accomplishing this short of language detection, one is relying on the domain name. You can just check if your domain starts with a prefix like, “us”, “es”, “de” and so on. And then apply the right translations. For example:

  • en.example.com
  • es.example.com
  • de.example.com

We will focus on the other strategy that consists in what they call “prefix based routing”, in this case the “magic” happens in the path section of the URL, for example:

  • www.example.com/en/dashboard
  • www.example.com/es/dashboard
  • www.example.com/de/dashboard

The middleware task is to detect that path segment and convert it in a param that will be accesible from the views. In the mid term it will also set a cookie with the last locale used.

From this step onwards I will diverge a little from next-intl documentation. I will add a small change, but I believe that what they suggest is more error prone. If you take a look at their examples you’ll notice that they are constantly using a locales array in the middleware, layouts, …

Well, please, don’t do that, instead of it, create a file that exports the array of valid locales so you can access it from everywhere. Working this way you should only keep in mind updating this file if you introduce a new language in the future.

I will put this file inside the locales folder, close to the translations files:

The contents of this file are pretty simple:

To define such a middleware you need to create a middleware.ts file in your “src” folder root.

As they state in next-intl documentation, they will use this steps to finally determine which locale should be used:

  • A locale prefix is present in the pathname (e.g. /de/about)
  • A cookie is present that contains a previously detected locale
  • The accept-language header is matched against the available locales
  • The defaultLocale is used

Updating the layout file

Well at this point we have placed all the pieces needed to make it work, now let’s begin to use it.

You’ll need to update your layout file with the next code:

If you pay attention you’ll notice that we are passing a new prop to the Layout component that is the params.

That new prop should contain a field called locale where our middleware has placed the detected locale. But, chances are that someone tries to play around with cookies and to try to bypass our original setup. That’s the main reason why next-intl suggests to add this step of validation.

In our case we will use our validLocales array defined in our centralized file so we don’t need to hard code that array everywhere.

We can test if everything is working properly just defining a simple page.tsx at /app/[locale].

If I try to go to https://localhost:3000/ I will notice that it will append “es” to my URL and also set a new cookie called NEXT_LOCALE that will store my default language. In my case “es”

If after that I visit https://localhost:3000/en I will see this:

You’ll notice two different changes, the first is that it removes the “/en” from the URL, this happens because this is our default locale. On the other hand the NEXT_LOCALE cookie value changes and now stores “en” value.

Static rendering

Under the hood what’s happening here is that every page that uses this approach won’t be statically rendered and will be dynamically rendered.

At this point the next-intl team provides a way to ensure that a page that only needs the translations stuff can be also statically rendered.

You need to use two elements, the classical generateStaticParams function as usual but also a new one provided by next-intl and called with the scary name unstable_setRequestLocale.

This function is in charge to distribute the locale params to all server components that will be rendered in each request.

And here comes the drawback of this approach. In next, if an user navigates between views that shares, for example, the same layout, the common layout won’t be rendered again so, you’ll need to add this unstable_setRequesLocale in each Layout or Page but the outcome is that you’ll maintain the static rendering approach as far as you want.

Your layout code should look like this.

And our home page.

And that’s all.

See you soon in the next post.

Disclaimer

One of my best friends and also my favourite technical reviewer pointed out that I shouldn’t use so much default exports and I believe that he is absolutely right.

Using named exports helps to maintain consistency and naming conventions so, I will improve it in the next post 😉

References