DEV Community

Cover image for How to Integrate Next-Auth with Your Next.js Application
Faruq Abdulsalam
Faruq Abdulsalam

Posted on

How to Integrate Next-Auth with Your Next.js Application

Recently, I migrated a frontend application from an older version of Next.js to Next.js 14. During this process, I decided to upgrade the session handling approach. After extensive research, I chose to use NextAuth.js, now known as Auth.js.

In the application, server-side authentication with AWS Cognito is utilized, and I wanted to maintain this setup. The goal was to seamlessly integrate authentication tokens and user data retrieved from successful authentications into Auth.js, ensuring session availability across the entire app and enabling authorization enforcement for different pages.

In this article, I'll guide you through the process of successfully integrating Auth.js with your Next.js application using the app router. We will cover both the credentials (email and password) approach and Google social login.

Note: Next-Auth v5 is still in beta mode, so I wouldn't fully recommend integrating it into a production application just yet. However, the final decision is up to you.

Prerequisites

  1. Familiarity with the basics of React.js
  2. Brief experience with Next.js app router
  3. Basic understanding of Typescript, as we'll be using Next.js with TypeScript

You don't need to have prior experience with Next-Auth.js (now Auth.js) to follow this article.

Note: For the rest of the article, I’ll be using next-auth and Auth.js interchangeably.

Let's get started!!


1. Create your Next.js application
Start by generating a new Next.js application:

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

After running the command above, follow the prompts, accepting the defaults for TypeScript and Tailwind CSS as we'll be using TypeScript and will apply minor styling with Tailwind. Also choose Yes for the import alias and accept the default alias configuration.

2. Install Auth.js
Install the latest version of auth.js using npm or yarn

npm install next-auth@beta
Enter fullscreen mode Exit fullscreen mode

or

yarn add next-auth@beta
Enter fullscreen mode Exit fullscreen mode

3. Generate and Configure Your Environment Variables
Create a .env.local file and add your Auth.js secret. Auth.js uses this for encryption of your JWTs and cookies.

touch .env.local
Enter fullscreen mode Exit fullscreen mode

Generate the secret string by running any of the two commands:

# Linux/Mac terminal
openssl rand -base64 33

# Alternatively, use `auth` to generate the secret
npx auth secret
Enter fullscreen mode Exit fullscreen mode

Add the generated secret to your .env.local file:

AUTH_SECRET="your-secret"
Enter fullscreen mode Exit fullscreen mode

You can escape running into major issues when you don't set this in your development environment. But it is compulsory to set it in the production environment.

You should also add your backend URL here

NEXT_PUBLIC_BASE_URL="your-backend-url"
Enter fullscreen mode Exit fullscreen mode

4. Create your interface and types.
To manage your types effectively, create a new directory named types in your root directory (or in your /src directory if you are using it), then inside it, add two new files: login.ts and user.ts.

├── app
├── public
├── types
│   ├── login.ts
│   ├── user.ts
├── .env.local
└── ...
Enter fullscreen mode Exit fullscreen mode

Your app directory should look something like this with some other files and folders which are not shown here.

login.ts
Add the following types for different authentication scenarios:

type CredentialsType = {
  username: string;
  password: string;
};

type SocialCredentialsType = {
  auth_code: string;
};

export type { CredentialsType, SocialCredentialsType };

Enter fullscreen mode Exit fullscreen mode

CredentialsType: Defines the structure for email and password login credentials.
SocialCredentialsType: Defines the structure for social login credentials, specifically for Google authentication.

user.ts
Define interface and type for user data to manage session details:

interface UserType {
  id: string;
  name: string;
  email: string;
  avatar: string;
  premiumSubscription: boolean;
  accessToken: string;
  refreshToken: string;
  subId: string;
}

type UserResponseType = {
  id: string;
  name: string;
  email: string;
  avatar: string;
  premium_subscription: boolean;
  access_token: string;
  refresh_token: string;
  sub_id: string;
};

export type { UserType, UserResponseType };

Enter fullscreen mode Exit fullscreen mode
  • UserType: Interface specifying the expected properties for user session management.
  • UserResponseType: Type specifying the format of user data as received from the backend.

5. Create your Auth.js configuration.
Create a new file named auth.ts in your root directory. If you are using the src/ directory approach, place the file directly within the src/ folder.

├── app
├── public
├── types
│   ├── login.ts
│   ├── user.ts
├── .env.local
├── auth.ts
└── ...
Enter fullscreen mode Exit fullscreen mode

auth.ts
At the top of the file import the necessary libraries and types for
configuring Auth.js.

// library imports
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

// types imports
import type { NextAuthConfig, Session, User } from "next-auth";
import type { UserType, UserResponseType } from "@/types/user";
import { AdapterUser } from "next-auth/adapters";
import { CredentialsType, SocialCredentialsType } from "@/types/login";
import { JWT } from "next-auth/jwt";
Enter fullscreen mode Exit fullscreen mode

Next, we have to modify some of the types in the next-auth library with some of our own properties.

declare module "next-auth" {
  interface User extends UserType {}
}

declare module "next-auth/adapters" {
  interface AdapterUser extends UserType {}
}

declare module "next-auth/jwt" {
  interface JWT extends UserType {}
}
Enter fullscreen mode Exit fullscreen mode

Here, we used the TypeScript module augmentation feature to extend the User, AdapterUser, JWT interfaces from NextAuth with properties defined in UserType from our user.ts file. This ensures that our custom user properties are recognized throughout the Auth.js configuration.

Next we declare authOptions object which we'll use to initialize NextAuth at the end.

const authOptions = {
  providers: [
// Add authentication providers here (e.g., CredentialsProvider, GoogleProvider)
  ],
  callbacks: {
 // Add custom authentication callbacks here (e.g., signIn, signOut, jwt, session)
  },
  pages: {
// Customize authentication-related pages (e.g., signIn, error)
  },
  session: {
// Configure session options (e.g., JWT settings, session management)
  },
} satisfies NextAuthConfig;

export const { handlers, auth, signIn, signOut } = NextAuth(authOptions);
Enter fullscreen mode Exit fullscreen mode

A.providers: accepts an array of providers such as CredentialsProvider, GoogleProvider, e.t.c.

B.callbacks: specifies custom callback functions that can be triggered at various points in the authentication process.

C.pages: auth.js has default authentication-related pages for signIn, error, signOut e.t.c. But in this block , we can override it with our customized pages by specifying their paths.

D.session: Configures session options, such as how sessions are handled and stored. e.g jwt or database approach

  • Finally, we initialize NextAuth with the authOptions configuration and destructure the returned methods (handlers, auth, signIn, signOut) for use in our application.
    • handlers: Middleware for handling NextAuth requests.
    • auth: A universal method to interact with Auth.js in your Next.js app.
    • signIn: Method to trigger sign-in.
    • signOut: Method to trigger sign-out.

The code in the auth.ts file should look like this now

// library imports
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

// types imports
import type { NextAuthConfig, Session, User } from "next-auth";
import type { UserType, UserResponseType } from "@/types/user";
import { AdapterUser } from "next-auth/adapters";
import { CredentialsType, SocialCredentialsType } from "@/types/login";
import { JWT } from "next-auth/jwt";

declare module "next-auth" {
  interface User extends UserType {}
}

declare module "next-auth/adapters" {
  interface AdapterUser extends UserType {}
}

declare module "next-auth/jwt" {
  interface JWT extends UserType {}
}

const authOptions = {
  providers: [
  ],
  callbacks: {
  },
  pages: {
  },
  session: {
  },
} satisfies NextAuthConfig;

export const { handlers, auth, signIn, signOut } = NextAuth(authOptions);
Enter fullscreen mode Exit fullscreen mode

Now, let's complete the authOptions object with the necessary values.

A. providers:
We will use CredentialsProvider for handling authentication on the server side by making requests to our server URL. We will set it up twice: once for email and password login, and once for social login with Google. To distinguish between the two, we specify unique id values.

providers: [
    CredentialsProvider({
      id: "credentials",
      name: "Credentials",
      authorize: async (credentials) => {
        try {
          const user = await fetchUser(
            `${process.env.NEXT_PUBLIC_BASE_URL}/auth/login`,
            {
              username:
                typeof credentials.username === "string"
                  ? credentials.username
                  : "",
              password:
                typeof credentials.password === "string"
                  ? credentials.password
                  : "",
            }
          );

          return user ? createUser(user) : null;
        } catch (error) {
          console.error("Error during authentication", error);
          return null;
        }
      },
    }),
    CredentialsProvider({
      id: "social",
      name: "Custom Social Login",
      authorize: async (credentials) => {
        try {
          const user = await fetchUser(
            `${process.env.NEXT_PUBLIC_BASE_URL}/auth/social_login`,
            {
              auth_code:
                typeof credentials.authCode === "string"
                  ? credentials.authCode
                  : "",
            }
          );

          return user ? createUser(user) : null;
        } catch (error) {
          console.error("Error during authentication", error);
          return null;
        }
      },
    }),
  ],
Enter fullscreen mode Exit fullscreen mode
  • Email and Password Login:

    • The first CredentialsProvider is configured for email and password login.
    • It makes a request to ${process.env.NEXT_PUBLIC_BASE_URL}/auth/login with the provided credentials via the fetchUser function(replace this URL with your backend URL).
    • If a user is found, createUser(user) is called to format the user data.
  • Social Login with Google:

    • The second CredentialsProvider is configured for social login using Google.
    • It makes a request to ${process.env.NEXT_PUBLIC_BASE_URL}/auth/social_login with the provided auth code via the fetchUser function(replace this URL with your backend URL).
    • If a user is found, createUser(user) is called to format the user data.

Each provider has a unique id to distinguish between the two methods of authentication.

By using CredentialsProvider twice with different id values, we can manage multiple authentication methods seamlessly.

Here are the definitions of the fetchUser and createUser functions:

// Function to authenticate and fetch user details
async function fetchUser(
  url: string,
  body: CredentialsType | SocialCredentialsType
) {
  try {
    const res = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    const user = await res.json();

    if (res.ok && user) {
      return user;
    } else {
      console.error(`Failed to fetch user: ${res.status} ${res.statusText}`);
      return null;
    }
  } catch (error) {
    console.error(`Error during fetch: ${error}`);
    return null;
  }
}

// Function to create a user object
function createUser(user: UserResponseType) {
  const userObject: UserType = {
    id: user.id,
    name: user.name,
    email: user.email,
    avatar: user.avatar,
    premiumSubscription: user.premium_subscription,
    accessToken: user.access_token,
    refreshToken: "", //add subId from the auth service here
    subId: "", // add refresh token here
  };

  return userObject;
}
Enter fullscreen mode Exit fullscreen mode

B. callbacks:
The callbacks section allows us to customize the behavior of the authentication process. Here, we’ll add two async functions: jwt and session.

callbacks: {
    async jwt({ token, user }: { token: JWT; user: User }) {
      // Add the user properties to the token after signing in
      if (user) {
        token.id = user.id as string;
        token.avatar = user.avatar;
        token.name = user.name;
        token.email = user.email;
        token.premiumSubscription = user.premiumSubscription;
        token.accessToken = user.accessToken;
        token.subId = user.subId;
        token.refreshToken = user.refreshToken;
      }
      return token;
    },
    async session({ session, token }: { session: Session; token: JWT }) {
      // Create a user object with token properties
      const userObject: AdapterUser = {
        id: token.id,
        avatar: token.avatar,
        name: token.name,
        premiumSubscription: token.premiumSubscription,
        accessToken: token.accessToken,
        subId: token.subId,
        refreshToken: token.refreshToken,
        email: token.email ? token.email : "", // Ensure email is not undefined
        emailVerified: null, // Required property, set to null if not used
      };

      // Add the user object to the session
      session.user = userObject;

      return session;
    },
  },
Enter fullscreen mode Exit fullscreen mode
  • jwt callback:

    • The jwt function is called during the authentication process to add user properties to the JWT token. When a user signs in, their properties, such as id, avatar, name, email, accessToken, subId, and refreshToken, are added to the token. This token is then used to securely manage user sessions.
  • session callback:

    • The session function is called whenever a session is accessed to add user data to the session object. A user object is created using the properties from the token. This user object is then added to the session, ensuring that user data is available whenever the session is accessed. The email property is ensured to be a string, and emailVerified is set to null if not used.

C. pages:
In the pages section of the authOptions object, we define paths to our custom pages that will override the default authentication-related pages provided by Auth.js. This allows us to customize the user experience for specific authentication actions.

  pages: {
    signIn: "/auth/login", // Custom sign-in page
    // error: "/auth/error", // Custom error page
  },
Enter fullscreen mode Exit fullscreen mode
  • signIn: Specifies the path to a custom sign-in page. By setting this to /auth/login, we direct users to our custom login page instead of the default Auth.js sign-in page.
  • error: Optionally, we can specify a custom error page. This is commented out here, but if we wanted to use a custom error page, we could set its path like so: error: /auth/error.

D. session:
In the session section of the authOptions object, we specify how sessions should be managed. In this case, we are using JSON Web Tokens (JWT) as the session strategy.

  session: {
    strategy: "jwt",
  },
Enter fullscreen mode Exit fullscreen mode
  • strategy: By setting this to "jwt", we ensure that sessions are managed using JSON Web Tokens. This means that session data is encoded and stored in a JWT, which is then sent to the client.

Here is what the entire file should look like now

// library imports
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

// types imports
import type { NextAuthConfig, Session, User } from "next-auth";
import type { UserType, UserResponseType } from "@/types/user";
import { AdapterUser } from "next-auth/adapters";
import { CredentialsType, SocialCredentialsType } from "@/types/login";
import { JWT } from "next-auth/jwt";

// Modify NextAuth types with custom properties
declare module "next-auth" {
  interface User extends UserType {}
}

declare module "next-auth/adapters" {
  interface AdapterUser extends UserType {}
}

declare module "next-auth/jwt" {
  interface JWT extends UserType {}
}

const authOptions = {
  providers: [
    CredentialsProvider({
      id: "credentials",
      name: "Credentials",
      authorize: async (credentials) => {
        try {
          const user = await fetchUser(
            `${process.env.NEXT_PUBLIC_BASE_URL}/auth/login`,
            {
              username:
                typeof credentials.username === "string"
                  ? credentials.username
                  : "",
              password:
                typeof credentials.password === "string"
                  ? credentials.password
                  : "",
            }
          );

          return user ? createUser(user) : null;
        } catch (error) {
          console.error("Error during authentication", error);
          return null;
        }
      },
    }),
    CredentialsProvider({
      id: "social",
      name: "Custom Social Login",
      authorize: async (credentials) => {
        try {
          const user = await fetchUser(
            `${process.env.NEXT_PUBLIC_BASE_URL}/auth/social_login`,
            {
              auth_code:
                typeof credentials.authCode === "string"
                  ? credentials.authCode
                  : "",
            }
          );

          return user ? createUser(user) : null;
        } catch (error) {
          console.error("Error during authentication", error);
          return null;
        }
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }: { token: JWT; user: User }) {
      // Add the user properties to the token after signing in
      if (user) {
        token.id = user.id as string;
        token.avatar = user.avatar;
        token.name = user.name;
        token.email = user.email;
        token.premiumSubscription = user.premiumSubscription;
        token.accessToken = user.accessToken;
        token.subId = user.subId;
        token.refreshToken = user.refreshToken;
      }
      return token;
    },
    async session({ session, token }: { session: Session; token: JWT }) {
      // Create a user object with token properties
      const userObject: AdapterUser = {
        id: token.id,
        avatar: token.avatar,
        name: token.name,
        premiumSubscription: token.premiumSubscription,
        accessToken: token.accessToken,
        subId: token.subId,
        refreshToken: token.refreshToken,
        email: token.email ? token.email : "", // Ensure email is not undefined
        emailVerified: null, // Required property, set to null if not used
      };

      // Add the user object to the session
      session.user = userObject;

      return session;
    },
  },
  pages: {
    signIn: "/auth/login", // Custom sign-in page
    // error: "/auth/error", // Custom error page
  },
  session: {
    strategy: "jwt",
  },
} satisfies NextAuthConfig;

// Function to authenticate and fetch user details
async function fetchUser(
  url: string,
  body: CredentialsType | SocialCredentialsType
) {
  try {
    const res = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    const user = await res.json();

    if (res.ok && user) {
      return user;
    } else {
      console.error(`Failed to fetch user: ${res.status} ${res.statusText}`);
      return null;
    }
  } catch (error) {
    console.error(`Error during fetch: ${error}`);
    return null;
  }
}

// Function to create a user object
function createUser(user: UserResponseType) {
  const userObject: UserType = {
    id: user.id,
    name: user.name,
    email: user.email,
    avatar: user.avatar,
    premiumSubscription: user.premium_subscription,
    accessToken: user.access_token,
    refreshToken: "", //add subId from the auth service here
    subId: "", // add refresh token here
  };

  return userObject;
}

export const { handlers, auth, signIn, signOut } = NextAuth(authOptions);
Enter fullscreen mode Exit fullscreen mode

6. Utilize the configuration in the Next.js API route.
To integrate the NextAuth configuration into a Next.js API route, follow these steps:

Create a new file named route.ts in the app directory at the following path:

├── app
│   ├── api
│   │   ├── auth
│   │   │   ├── [...nextauth]
│   │   │   │   ├──  route.ts
├── auth.ts
└── ...
Enter fullscreen mode Exit fullscreen mode

In the route.ts file, add the following lines of code:

// library imports
import { handlers } from "@/auth";

export const { GET, POST } = handlers;
Enter fullscreen mode Exit fullscreen mode

At the top, we import the handlers object from auth.ts, then destructure it to extract the GET and POST methods and exports them. When a request is made to the /api/auth endpoint, the route.ts file delegates the request to the appropriate handler (GET or POST) provided by NextAuth. These handlers manage the authentication logic, session management, and other related tasks based on the configuration we defined earlier in auth.ts.

We are done with setting up Auth.js. Now let's use the magic we created and see it in action


7. Using Auth.js in our Next.js Application
Now that we have completed the setup for Auth.js, it's time to see it in action. We'll integrate authentication into our application by creating email and password sign-in, social login and protected pages.

A. Create the authentication components
First, create a components directory in the root directory of your app (in the src/ directory if you are using this approach). Then add auth and protected directories in it.

  • Create a new file named login.tsx in the components/auth directory and a new file named profile.tsx in the components/profile directory:
├── app
├── components
│   ├── auth
│   │   ├── login.tsx
│   ├── profile
│   │   ├── profile.tsx
Enter fullscreen mode Exit fullscreen mode
  • login.tsx: Here we will add the following code to create a sign-in form and a google login button.
"use client";

// library imports
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";

export default function SignIn() {
  const searchParams = useSearchParams();
  const googleLogin = "Your google login url which will contain your client id and redirectURI";

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [authenticated, setAuthenticated] = useState(false);
  const [error, setError] = useState("");

  useEffect(() => {
    if (authenticated) {
      // Redirect to previous page or home page
      const next = searchParams.get("next") || "/";
      window.location.href = next;
    }
  }, [authenticated]);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    try {
      const res = await fetch("/api/login", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ username, password, type: "credentials" }),
      });

      if (res.ok) {
        setAuthenticated(true);
      } else {
        // handle error state here
        setError("Invalid credentials");
      }
    } catch (error) {
      // handle error state here
      console.error("Error during sign-in", error);
      setError("Internal server error");
    }
  };

return (
    <div className="mx-auto w-[200px] h-full border-red-100">
      <div>
        <p className="text-xl w-full flex justify-center mt-3 mb-5">Sign In</p>
        <form onSubmit={handleSubmit}>
          <label>
            Username:
            <input
              type="text"
              className="w-full rounded-sm"
              value={username}
              onChange={(e) => setUsername(e.target.value)}
            />
          </label>
          <label>
            Password:
            <input
              className="w-full rounded-sm"
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
            />
          </label>
          <button
            className="w-full flex justify-center bg-teal-500 text-white mt-3 rounded-md"
            type="submit"
          >
            Sign In
          </button>
          {error && <p style={{ color: "red" }}>{error}</p>}
        </form>
      </div>
      <div className="my-2">
        <div className="flex justify-center"> or </div>
      </div>
      <div className="w-full bg-red-700 rounded-md mb-2">
        <Link href={googleLogin} className="flex ">
          <p className="w-full text-white flex justify-center">
            Sign in with Google
          </p>
        </Link>
      </div>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

We'll send a POST request to an API route (/api/login) to handle authentication. If the login is successful, the user will be redirected to the next page or home page. If the Sign in with Google button is clicked, the user will be redirected to the google-login page

Next, we will create the /api/login endpoint to handle the authentication logic on the server side. Create a new folder named login in the app/api directory and create a route.ts file in it.

├── app
│   ├── api
│   │   ├── auth
│   │   ├── login
│   │   │   ├── route.ts
├── components
├── auth.ts
└── ...
Enter fullscreen mode Exit fullscreen mode
  • route.ts:
// library imports
import { NextResponse, NextRequest } from "next/server";

// internal imports
import { signIn } from "@/auth";

export async function POST(req: NextRequest, res: NextResponse) {
  const data = await req.json();
  const { username, password, type } = data;

  try {
    const result =
      type === "credentials"
        ? await signIn("credentials", { redirect: false, username, password })
        : await signIn("social", { redirect: false, authCode: username });

    // handle the result of the sign-in attempt
    if (!result || result.error) {
      return NextResponse.json({ error: "Invalid credentials" });
    } else {
      return NextResponse.json({ success: true });
    }
  } catch (error) {
    console.error("Error during sign-in", error);
    return NextResponse.error();
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, we handle authentication for both credentials and social login types. Based on the type of login, we call the signIn function with either credentials (username and password) or social login (auth_code).

Next, we will create the social login page. Create a new file named socialAuth.tsx in the components/auth directory.

├── app
├── components
│   ├── auth
│   │   ├── login.tsx
│   │   ├── socialAuth.tsx
│   ├── profile
Enter fullscreen mode Exit fullscreen mode
  • socialAuth.tsx: Add the following code to socialAuth.tsx.
"use client";

// library imports
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";

export default function SocialAuth() {
  const googleLogin =
    "Your google login url which will contain your client id and redirectURI";

  const router = useRouter();
  const searchParams = useSearchParams();

  const [authSuccess, setAuthSuccess] = useState(true);
  const [tokenStatus, setTokenStatus] = useState(false);

  useEffect(() => {
    // check query string for authentication code
    if (authSuccess || tokenStatus) {
      const url = window.location.href;
      const code = url.match(/\?code=(.*)/);
      if (!tokenStatus && authSuccess) {
        if (code) {
          authenticateUser(code[1]);
        } else {
          router.push("/auth/login");
        }
      } else if (tokenStatus) {
        // Redirect to previous page or home page
        const next = searchParams.get("next") || "/";
        router.push(next);
      } else {
        router.push("/auth/login");
      }
    }
  }, [tokenStatus, authSuccess]);

  const authenticateUser = async (code: string) => {
    try {
      const res = await fetch("/api/login", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ username: code, type: "social" }),
      });

      if (res.ok) {
        setTokenStatus(true);
      } else {
        // handle error state here
        setAuthSuccess(false);
      }
    } catch (error) {
      // handle error state here
      setAuthSuccess(false);
    }
  };

  return (
    <>
      <div>
        {authSuccess ? (
          <div>
            <h1>Authenticating...</h1>
          </div>
        ) : (
          <div>
            <h1>
              {" "}
              An error occurred while attempting to authenticate your account
              with Google{" "}
            </h1>

            <div>
              <div>
                <Link href={googleLogin}>Please try again</Link>
              </div>
            </div>
          </div>
        )}
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

This ensures that users can authenticate via social login providers like Google and handles the authentication flow smoothly.

B. Create the Authentication Pages
Next, let's create the custom sign-in and social login pages that utilize the components we created above.

Create a new directory called auth in the app/ directory. Inside auth, create folders named login and social, and then add a page.tsx file in both folders.

├── app
│   ├── auth
│   │   ├── login
│   │   │   ├── page.tsx
│   │   ├── social
│   │   │   ├── page.tsx
Enter fullscreen mode Exit fullscreen mode
  • In login/page.tsx, add the following code:
//component imports
import SignIn from "@/components/auth/login";

export default function LoginPage() {
  return <SignIn />;
}

Enter fullscreen mode Exit fullscreen mode
  • In social/page.tsx, add the following code:
//component imports
import SocialAuth from "@/components/auth/socialAuth";

export default function SocialAuthPage() {
  return <SocialAuth />;
}
Enter fullscreen mode Exit fullscreen mode

We are now set with authentication, and you can confirm the state of what we have done so far.

  • Attempt to navigate to this url http://localhost:3000/api/auth/signin. You should be redirected back to the custom sign-in page. This redirection occurs because of the URL path we specified in the pages section of our authOptions object while setting up the configuration in auth.ts.

B. Create protected page
The final step is to create a protected page that users cannot access unless they are authenticated.

-profile.tsx
Let's add the code below to our empty file profile.tsx in the components directory.

"use client";

// library imports
import React, { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter, usePathname } from "next/navigation";

export default function Profile() {
  const router = useRouter();
  const pathname = usePathname();

  const { data: session } = useSession();

  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;

  const [loadingProfile, setLoadingProfile] = useState(false);

  useEffect(() => {
    if (session?.user?.accessToken) {
      // fetch user profile if access token is available
      getUserProfile(session.user.accessToken);
    } else {
      // Redirect to `/login` if no access token or no session
      router.push("/auth/login?next=" + pathname);
    }
  }, []);

  const getUserProfile = (token: string) => {
    setLoadingProfile(true);
    fetch(`${baseUrl}/auth/user_profile`, {
      headers: { Authorization: "Bearer " + token },
    })
      .then((response) => {
        setLoadingProfile(false);
      })
      .catch((error) => {
        // handle error here
        console.error(error);
      });
  };

  return (
    <>
      {" "}
      {loadingProfile ? (
        <p>Loading...</p>
      ) : (
        <div>
          <p>User Profile</p>
          <p>Name: {session?.user?.name}</p>
          <p>Email: {session?.user?.email}</p>
        </div>
      )}
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

Here we use the useSession hook to check if a user session with an access token exists. If the token is present , we go ahead and make a call to the backend route /auth/user_profile . In my case i have a protected route in the backend which an unauthenticated user cannot access without a token. While waiting for the profile data to load, a loading indicator is displayed. Once loaded, the component displays the user's name and email. If there is no active session or access token, it redirects the user back to the login page to authenticate.

Finally, we create the page which utilizes the profile.tsx component above. Create a new folder in the app directory named profile and in it a page.tsx file.

  • page.tsx:
// library imports
import { SessionProvider } from "next-auth/react";

// internal imports
import { auth } from "@/auth";

//component imports
import Profile from "@/components/profile/profile";

export default async function ProfilePage() {
  const session = await auth();

  return (
    <SessionProvider session={session}>
      <Profile />
    </SessionProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

First we import the SessionProvider component from next-auth/react to serve as a context provider for managing the user session state throughout the application. Additionally, we import the auth method from auth.js. Since this operation requires asynchronous behavior, we utilize a server-side component, using await to fetch the session using the auth method. Subsequently, we pass this session object obtained from auth to the SessionProvider component which acts as a wrapper around the Profile component. We have to wrap the component with Session Provider here if we want the session to be accessible within profile.tsx using the useSession hook, similar to how we handled authentication state above.

Now if we try to access the /profile route, we would get redirected to the /auth/login page since we are not logged in. Once we login, we would be redirected back to the profile page, where we can see our user data.


Congratulations , you have successfully integrated your next js application using the app router with auth.js v5.

You might have noticed that in the profile.tsx component, we check the session for a token and then redirect the user back to the login page if the token is not present. This means that Next.js already starts rendering our page before redirecting to the login page if the token is absent. If you pay attention, you'll also notice this in the UI.

An alternative to this approach is to use middleware to handle authentication. Next.js has a middleware feature that is triggered when a user attempts to access a route. We can use this to protect our authenticated routes. This way, the protected page is not rendered if an unauthenticated user attempts to access it, as the middleware intercepts the request and redirects the user to the login route. This ensures we have a central location to check the session and redirect users if they are unauthenticated, thereby improving the user experience with the UI.

8. Adding a Middleware
Create a new file called middleware.ts in your root directory. If you are using the src/ directory approach, place the file directly within the src/ folder.

├── app
├── public
├── types
├── .env.local
├── auth.ts
├── middleware.ts
└── ...
Enter fullscreen mode Exit fullscreen mode

-middleware.ts
In the file add the following lines of code:

import { NextResponse } from "next/server";
import { auth } from "@/auth";

export default auth((req) => {
  const currentPath = req.nextUrl.pathname;

  // Redirect to login page if user is not authenticated
  if (!req.auth) {
    return NextResponse.redirect(
      new URL(`/auth/login?next=${currentPath}`, req.url)
    );
  }
});

// Manage list of protected routes
export const config = {
  matcher: ["/profile/:path*", "/another-protected-route/:path*"],
};

Enter fullscreen mode Exit fullscreen mode

This middleware checks if the user is authenticated before allowing access to specified routes. If the user is not authenticated, they are redirected to the login page. The config object defines the routes that should trigger this middleware, ensuring that only protected routes are affected.

By setting up this middleware, you centralize the authentication logic, making your codebase cleaner and more maintainable as your application grows.

Note: In my application, I have several authenticated and unauthenticated routes, which is why I am not triggering the middleware with all requests and instead maintaining a protected routes list. The little downside to this is that you have to manage a list of all your protected routes. This might end up becoming a long list and could become difficult to manage, or someone might implement a protected page and forget to update the middleware. If your base path is the only unprotected route, your job will be much easier.

It's been a long ride, but we have finally come to the end of this tutorial. With what you have learned, I believe you can seamlessly integrate Auth.js v5 into your `next.js application.

Here is the link to the github repo for this project. Cheers!!!

If you have any questions, feel free to drop them as a comment or send me a message on LinkedIn and I'll ensure I respond as quickly as I can. Ciao 👋

Top comments (0)