Authentication in Next.js using NextAuth.js

Authentication in Next.js using NextAuth.js

ยท

6 min read

If you are a front-end developer, thinking about authentication can be quite challenging. In authentication, from storing JWT in local storage to managing cookies, we consider it all. NextAuth.js is currently the most popular library for handling authentication in your Next.js projects.

What will we use?

In this guide, we will be using the NextAuth.js credentials provider, so that we can set up custom fields for authentication. These fields will be username and password. On the Next.js side, we will be using API routes (pages router) as these are currently being used in most of the codebases. We will also use our custom login page rather than relying on NextAuth.js's default page.

Setting Up the Credentials Provider

  • App setup - I hope you already have a next app, if not then start by creating one using official docs.

  • Start by installing NextAuth.js using :

      npm install next-auth
    
  • Create [...nextauth].ts file under the directory /pages/api/auth/[...nextauth].ts and add the following code :

      import NextAuth from 'next-auth';
      import CredentialsProvider from 'next-auth/providers/credentials';
      import dbConnect from '../../../lib/dbConnect'; //import your dbConnect, if you have one.
      import User from '../../../models/userModel'; //ignore this
      import { Provider } from 'next-auth/providers';
    
      export const authOptions = {
        // Configure one or more authentication providers
        providers: [
          CredentialsProvider({
            id: 'credentials',
            type: 'credentials',
            name: 'Credentials',
            credentials: {
              username: { label: 'Username', type: 'text' },
              password: { label: 'Password', type: 'password' },
            },
            async authorize(credentials, req) {
              //Add logic here to look up the user from the credentials supplied
              await dbConnect(); // connect to your db, to check the user
              if (credentials?.password?.length && credentials?.username?.length) {
                //I am using a mongoose model in this example.
                const existingUser = await User.findOne({
                  username: credentials.username,
                  password: credentials.password,
                });
                if (existingUser) {
                  return existingUser;
                } else return null;
              } else {
                return null;
              }
            },
          }),
        ] as Provider[],
        secret: process.env.NEXTAUTH_SECRET,
        session: {
          strategy: 'jwt',
          maxAge: 30 * 24 * 60 * 60,
        },
        jwt: {
          encryption: true,
        },
        pages: {
          signIn: '/login',
        },
        // callbacks in the next section
      };
    
      export default NextAuth(authOptions);
    

Let's break this code snippet piece by piece :

  • As we are using the Credentials provider, we start by adding the provider to the provider list. We mention the id as "credentials" to access this provider on the frontend side while using the custom sign-in page. We also mention the type as "credentials" as we are using the same type.

  • In the next part, we define a credentials object which tells NextAuth about what kind of fields, we are dealing with. In this case, these fields are username and password.

  • Next, we define an async function called "authorize" which NextAuth uses while authentication, this function gets called up when we click on the login/sign-in button. In this function, we write the logic to verify the user credentials and if the user exists, we return the user otherwise we return null or we can throw an error.

  • Next, we define a secret variable, which will be our JWT secret (store it in .env.local at the root of your project).

  • In the next part, we define the session object which declares our strategy as jwt and maxAge of 30 days(you can use whatever value you like). This defines the duration for which our user session will remain active, once logged in. We also set the jwt encryption to true, which encrypts our token.

  • For the custom sign-in page, we define the page route to our custom login page in the pages object.

Callbacks (JWT and Session)

  • Extend the above-mentioned credentials provider by adding the callbacks setup below

      export const authOptions:any = {
        providers: [
          CredentialsProvider({
              // above mentioned code,
          callbacks: {
            jwt: async ({ token, user }) => {
              if (user) {
                token.username= user.username;
                token.id=user._id
              }
    
              return token;
            },
            session: ({ session, token }) => {
              if (token) {
                session.user.username = token.username;
                session.user.id=token.id
              }
              return session;
            },
          },
      }
    
  • JWT callback is invoked whenever a JWT token is created by nextAuth (while sign-in). In this case, we receive token and user as arguments. The argument user is the user returned by the authorize function and the token is something that we can use to persist our user details in our session object.

  • Session callback is invoked whenever we try to access the session on the client or server side. We can extend the user object in the session by using the token that we have received from JWT callback.

    This callbacks setup is necessary to get the username and the userId, whenever we try to access the session.

Login page setup

The login page structure and styling depend on your project, so we will just focus on the core functionality. Whenever the user clicks on login after entering the credentials, signIn function from next-auth should be called. Example:

import { signIn } from 'next-auth/react';
// on button click this clickhandler should be invoked
const clickHandler: React.MouseEventHandler<HTMLButtonElement> = async (e) => {
  e.preventDefault();
  try {
    const res = await signIn('credentials', {
      username, // entered by the user
      password, // entered by the user
      redirect: false,
      callbackUrl: '/',
    });
    if (res?.status === 200) {
      addSuccess('Successfully logged in'); //custom function for toast notification, you can just use console.log()
    } else {
      addError('error signing in');
    }
    // Here, you can also redirect based on successfull authentication using useRouter
  } catch (error) {
    if (isAxiosError(error)) {
      if (error.response) {
        addError(error.response.data);
      }
    }
  }
};

We pass the following arguments to signIn() function from nextAuth:

  • The first argument is your provider id, in our case it is "credentials".

  • The second argument is an object containing the credentials that we received from the user and some other parameters like redirect and callbackUrl. In our case, we don't need redirection, so we are setting redirect:false .

Finally, let's talk about accessing the session

We have these ways to access the session :

  • useSession() hook: This is a hook provided by nextAuth to access the session and the authentication status on the client side :

      import { useSession } from 'next-auth/react';
      const { data: session, status } = useSession()
    
      //session contains our user object
      //status can be authentication | unauthenticated | loading
    
  • getSession(): This is a function by nextAuth to call an API and get the session. This may be used when you are managing the loading state on your own. However, useSession() is generally used in most cases.

  • getServerSession(): As the name suggests, this function is used to get the session on the server side. This function accepts 3 arguments. Example:

      //API route example :
      export default async function handler(
        req: NextApiRequest,
        res: NextApiResponse
      ) {
        try {
          const session: Session | null = await getServerSession(
            req,
            res,
            authOptions //we defined the authOptions in our first snippet
          );
          await dbConnect();
          if (session) {
            //if session exists proceed to the backend logic
          } else {
            res.status(401).json({ message: 'Unauthorized' });
          }
        } catch (error) {
          console.log(error);
        }
      }
    

These functions provided by NextAuth make our lives easy.

Securing pages and components

  • RequireAuth Wrapper:
    You can use this wrapper to secure pages and components that require authenticated status on the client side.

      import React from 'react';
      import { signIn, useSession } from 'next-auth/react';
    
      function RequireAuth({ children }: { children: React.JSX.Element | string }) {
        const {status}=useSession() 
        if(status==="unauthenticated"){
          signIn()
        }
        if(status==="loading"){
          return <Loader/>
        }
        if(status==="authenticated"){
          return <>{children}</>
        }
      }
      export default RequireAuth;
    
  • Middleware: NextAuth also provides us with middleware to secure pages: create a middleware.ts file at the same level as the pages directory and use the following code:

      export { default } from 'next-auth/middleware';
      export const config = { matcher: [...] }; // add routes to secure pages
    

Additional advice: Use the developer tools (Application section) as much as possible, to see your cookies and try to play different scenarios by changing/deleting cookies. This will help you to avoid potential bugs related to authentication.

Connect with me on Twitter | Linkedin | Github

Happy Coding! ๐ŸŽˆ๐ŸŽˆ

ย