Complete Authentication Guide Using Next-Auth v5 in Next.js 14

calendar-emoji

28 Feb. 2024

JavascriptNext JS

So this is part two of a two-part blog I’m writing about implementing Complete Authentication in Next.js 14 using Next-Auth v5. If you haven’t read the first blog I would highly recommend checking it out first: https://medium.com/@nikhilc2209/client-side-form-validation-with-zod-useformstate-in-next-js-14-dc011a9c44fb

In this blog, I’m going to talk about how easy it is to implement Authentication & OAuth in your Next.js projects using Next-Auth v5.

Setting up Next-Auth

NextAuth.js is an open-source authentication library for Next.js applications which tries to simplify the process of implementing authentication in your Next.js projects by providing a set of utilities, middleware, and strategies for various authentication providers making Authentication hassle-free for developers.

Installing Next-Auth

To include Next-Auth v5 in your Next.js project simply run:

npm install next-auth@beta

since this version is still in beta and not included in the main release.

Changes from Next-Auth v4

The Next-Auth library has been around for a while now and has undergone major changes recently since the introduction of Next-Auth v5, though these changes introduce a lot of new features the publishers have tried their best to minimize the number of breaking changes.

Here is a summary of most of these changes from the official docs:

<Github Gist goes here>

The most notable change here is that most session related calls are now replaced by a universal auth() call which can be imported in any Server component and makes the code more readable.

Adding Auth handler

First we’ll set up a auth file which will handle all of our authentication based requests, create this file in the src directory so this way we can easily import it anywhere in our application using absolute imports starting with @ . Below is an example of how our auth.js file should look like. Starting off we have two options either login through Credentials(username/password) or login through an OAuth provider, in our case Google.

auth.js

import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import CredentialsProvider from "next-auth/providers/credentials";

export const {
  handlers: { GET, POST },
  auth,
} = NextAuth({
  session: { strategy: 'jwt' },

  providers: [

    CredentialsProvider({

      credentials: {
        username: { label: "Username", type: "text" },
        password: { label: "Password", type: "password" }
      },

      async authorize(credentials) {
        // authorization logic here
      }

    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
   ]
})

Now we’ll make a folder named api/auth inside the app directory which will contain all our logic for authentication. Inside this folder we create a subdirectory named [...nextauth] with a file named route.js. Interestingly, this naming convention is really important because this way every API request beginning with /api/auth/* will be handled by the code written in the [...nextauth]/route.js file.

Note: In Next-Auth v4 all the authorization logic was handled by the [...nextauth].js file, but since the v5 upgrade we have moved our authorization logic to the root of our repository so that it can be easily imported everywhere.

And now this file becomes a 1-line handler for GET and POST requests for those paths.

app/api/auth/[...nextauth]/route.js

export { GET, POST } from "@/auth"

Setting up Credentials Provider

Setting up Credentials Provider is fairly simple, remember that login form we created in the previous blog, we just need to pass the form data using the signIn function provided by the next-auth library and await for a response.

const response = await signIn("credentials", { 
   username: formData.get("username"),
   password: formData.get("password"),
   redirect: false, 
  });

Now we can handle this request in the authorize function in the Credentials Provider we just created. For the purpose of this blog I’m not going to connect to a database, I’ll just use a dummy.json file to store username & passwords for easier demonstration. So in our authorize function we’ll just loop through the json file and try to find matching credentials for our username and password.

If we manage to find it we return a user object with user.id & user.username and next-auth will create a session using the strategy defined in auth.js else we just return null indicating that the credentials are incorrect.

Similar to signIn function Next-Auth also provides a signOut function which deletes the current session and just like that we can easily login and logout using Next-Auth.

Also, to get our jwt tokens working we need to define a secret key named as NEXTAUTH_SECRET in our .env file, this is important because every jwt token has to be signed by a private key which is not meant to be shared. To generate this simply go to your terminal and run openssl rand -base64 32 to create a random 32-bit base64 string which we’ll use as our private key.

Defining public, protected & auth routes

Now that we have our login page working we can define our public, private and protected routes, the idea is pretty simple:

  • Public routes should always be accessible by everyone, therefore no sensitive data should be kept here and hence no checks are required if user wants to visit this url. For example: home page.
  • Protected routes should only be accessible to authenticated users as they might contain user-specific or some other kind of sensitive data, user needs to login to be able to view this and we can verify if the user is logged in by checking if the session token is valid or not. For example: user dashboard page.
  • Auth routes refer to login/register pages where the user is supposed to login credentials to verify his identity, these pages should only be visible to the user if the user is not logged in. If the user is logged in we redirect him back to the dashboard page, if the user wants to login using a different ID he needs to logout first to access an auth route. For example: login page.

Adding Middleware config

Adding a middleware using Next-Auth is recommended and makes our job much easier, when configured the middleware acts as a filter through which all the requests go through. Here we can check which request belongs to which type of route and can handle them accordingly, below is an overview of how Middleware works.

Condensed view of the Middleware module

import { auth } from "@/auth";
import { privateRoutes, 
         authRoutes, 
         DEFAULT_REDIRECT_LOGIN_URL, 
         DEFAULT_REDIRECT_HOME_URL } from './routes';

export const config = {
  matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
}

We also define a routes.js file along with middleware which contains all our private, public and protected routes.

Note: This crazy looking regex basically matches every single request made to the server, meaning that every request has to go through the middleware checks. You can however define only private routes here, but the former gives you more control and is strongly advisable.

Using Sessions to render data

We can also use the session we created to pass user-specific information such as the username/email of the authenticated user. If we console.log() the session we just created we see:

We get the username that we passed on successful login and the expiry time of the current token, we can also add additional information if we want from the authorize function in auth.js . We can use this information to display which user is logged in on the dashboard page or any other public/protected route.

Adding redirects for protected and Auth routes

The last thing to complete our Credentials Authentication is to set up redirect urls, these will be triggered in middleware.js based on the type of route and if the user session exists or not.

// When user is not logged in and tries to access protected routes redirect to login page
export const DEFAULT_REDIRECT_LOGIN_URL = '/login'

// When user is logged in and tries to access login page redirect to dashboard
export const DEFAULT_REDIRECT_HOME_URL = '/dashboard'

And now, we have successfully configured our Credentials Authentication using Next-Auth v5.

Adding OAuth provider

For our last act we just need to configure the OAuth providers we talked about so the user has the option of logging in via Credentials or through providers like Google, Github etc. With Next-Auth by our side adding OAuth is really a child’s play, Next-Auth provides a ton of OAuth options, but to keep this blog concise I’ll only demonstrate how to set up GoogleProvider.

From the Next-Auth official docs, we need a GOOGLE_CLIENT_ID and a GOOGLE_CLIENT_SECRET to set up our GoogleProvider, we can easily generate these by going to https://console.developers.google.com/apis/credentials and making a new project. From this project we can generate OAuth 2.0 credentials, generate them and paste them in a .env file and don’t expose them anywhere.

While in development use localhost as your URI and http://localhost:3000/api/auth/callback/google as your redirect URI, this way every OAuth request is handled by auth.js file.

Google Console settings for GoogleProvider

To trigger this we define a button for Google OAuth on our login page and when triggered this again invokes the signIn function we used earlier, we pass in the name of our provider along with a callbackUrl to redirect the user on successful login.

signIn("google", { callbackUrl: 'http://localhost:3000/dashboard'});

And with that, we have successfully configured our Login through Credentials & setup OAuth in just minutes Congrats!

As usual, for all those bums who skipped through the blog and are just interested in the code, I’ve attached the source code below.

I hope you enjoyed reading this and I hope you have a great day!

Source Code:

https://github.com/NikhilC2209/Next-Auth-v5

References: