Streamlining Authentication with Next-Auth: A Comprehensive Guide

Streamlining Authentication with Next-Auth: A Comprehensive Guide

ยท

11 min read

In this blog I'll be implementing authentication using next-auth and more precisely I will use the google provider of next-auth. I will be doing this project in the Next.js and will also dive into how to implement this authentication in client and server components separately. I will be using Prisma as my ORM with Mongodb as database.

So that was a short introduction of what this blog will be about.

Now let's dive into coding.

Step 1 : Project Setup

Step 1.1 : Next.js project setup

Run the following the command to create a next project.

npx create-next-app@latest next-auth

Step 1.2 : Shadcn - component library setup

Run the following the command to initialize shadcn.

npx shadcn-ui@latest init

Step 1.3 : Prisma setup

First we need to install prisma.

npm install prisma @prisma/client

Then we need to initialize it.

npx prisma init

This creates a new directory called prisma that contains a file called schema.prisma.

Next we need to create a free mongodb cluster and obtain the connection url and set it in the .env file.

Next we change the schema.prisma file like this:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mongodb"
  url      = env("DATABASE_URL")
}

model User {
  id       String  @id @default(auto()) @map("_id") @db.ObjectId
  email    String  @unique
  name     String
  image    String
  sub      String  @unique
  username String?
}

Next we run the following command to make these above changes be in sync with the database:

npx prisma db push

This command also generates a prisma client which we can then use for querying the database.

Next we create a db folder with an index.ts file in the src folder and write the following code.

import { PrismaClient } from "@prisma/client";

declare global {
  // eslint-disable-next-line no-var
  var cachedPrisma: PrismaClient;
}

let prisma: PrismaClient;
if (process.env.NODE_ENV === "production") {
  prisma = new PrismaClient();
} else {
  if (!global.cachedPrisma) {
    global.cachedPrisma = new PrismaClient();
  }
  prisma = global.cachedPrisma;
}

export const db = prisma;

We can now use this db variable which we are exporting from here whenever we want to query the database.

Step 1.4 : Next-Auth Setup prerequisite

In this step we will just configure some basic stuff we need to work with next-auth like some environment variables. The actual next-auth setup will come later.

First we need to install next-auth.

npm install next-auth

We will be implementing Google auth in next auth so we need 2 things for that.

We need to create Google Client Id and Google Client Secret from https://console.cloud.google.com and set it in the .env file.

Next we need to create a Next Auth Secret which will be used by next-auth in jwt verification.

openssl rand -base64 32

This creates a password which you can copy and then save in the .env file. So the final .env file should look like this:

The whole project setup has been done successfully. Now we can move forward with coding.

Step 2 : Creating the Navbar

First we make changes in the src/app/page.tsx file.

// src/app/page.tsx
export default function Home() {
  return (
    <main className="p-6">
      <h1 className="text-5xl font-bold">Welcome to this Auth App!</h1>
    </main>
  );
}

Then we create a Navbar.tsx file in the component folder.

// src/components/Navbar.tsx
import Status from "./Status";

const Navbar = async () => {
  return (
    <nav className="flex items-center justify-between p-5 bg-blue-100 rounded-md shadow-lg">
      <Status />
    </nav>
  );
};
export default Navbar;

Then we create a Status component.

// src/components/Status.tsx

import { cn } from "@/lib/utils";

const Status = async () => {
  const session = false;
  return (
    <div
      className={cn(
        "py-2 px-4 max-w-[200px] w-full rounded-md ring-2 ring-zinc-700 shadow-lg text-center",
        {
          "bg-green-200": session,
          "bg-red-100": !session,
        }
      )}
    >
      Status:{" "}
      <span
        className={cn("mr-2 underline underline-offset-2 font-medium text-lg", {
          "text-blue-500": session,
          "text-red-500": !session,
        })}
      >
        {session ? "Logged In" : "Logged Out"}
      </span>
    </div>
  );
};
export default Status;

Here we are using a session variable which we are hard-coding for now but after we implement next-auth we will be getting this value from there.

Next we create 2 folders inside src/app - protected-server and protected-client and create page.tsx inside each of them.

// src/app/protected-server/page.tsx
const page = async () => {
  return (
    <div className="p-6">
      <h1 className="text-3xl font-semibold">
        This is a server side page or component
      </h1>
    </div>
  );
};
export default page;
// src/app/protected-client/page.tsx
"use client";

const ProtectedClient = () => {
  return (
    <div className="p-6">
      <h1 className="text-3xl font-semibold">
        This is a client side page or component
      </h1>
    </div>
  );
};
export default ProtectedClient;

Next we include links to these pages in the Navbar component. But before that we have to add a button component from the shadcn library.

npx shadcn-ui@latest add button

So for now the Navbar component should look like this:

// src/components/Navbar.tsx
import Link from "next/link";
import Status from "./Status";
import { Button, buttonVariants } from "./ui/button";

const Navbar = async () => {
  const session = false; // this session will come from next-auth

  return (
    <nav className="flex items-center justify-between p-5 bg-blue-100 rounded-md shadow-lg">
      <Status />

      <Link
        href="/"
        className={buttonVariants({
          variant: "outline",
        })}
      >
        Home
      </Link>

      <Link
        href="/protected-server"
        className={buttonVariants({
          variant: "outline",
        })}
      >
        Server
      </Link>
      <Link
        href="/protected-client"
        className={buttonVariants({
          variant: "outline",
        })}
      >
        Client
      </Link>
      {!session ? (
        <Button>Sign In</Button>
      ) : (
        <Button variant="destructive">Sign Out</Button>
      )}
    </nav>
  );
};
export default Navbar;

And we are also conditionally rendering a sign-in and a sign-out button based on the session variable.

So the final outcome of all this should look like this:

Although nothing is functional at the moment. So we will tackle this problem next.

Step 3 : Implementing Next-Auth

First we will implement auth for the server side i. e. we are going to protect the protected-server page first.

For that we need to create an auth folder inside the src folder with an authOptions.ts file.

// src/auth/authOptions.ts
import { db } from "@/db";
import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
export const authOptions: NextAuthOptions = {
  secret: process.env.NEXT_AUTH_SECRET,

  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
    }),
  ],
  callbacks: {
    async session({ session, token }) {
      const { user } = session;

      if (user && token) {
        // check if the user already exists
        const dbUser = await db.user.findFirst({
          where: {
            email: user.email!,
            sub: token.sub,
          },
        });

        if (!dbUser) {
          //@ts-ignore
          session.isFirstTime = true;
          //@ts-ignore
          session.sub = token.sub;
          await db.user.create({
            data: {
              email: user.email!,
              image: user.image!,
              name: user.name!,
              sub: token.sub!,
            },
          });
        }
      }
      return session;
    },
  },
};

Now we can use this authOptions to fetch the session if the user is signed in using a function getServerSession which we get from next-auth.

import { authOptions } from "@/auth/authOptions";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";

const page = async () => {
  const session = await getServerSession(authOptions);

  if (!session) redirect("/");

  return (
    <div className="p-6">
      <h1 className="text-3xl font-semibold">
        This is a server side page or component
      </h1>
    </div>
  );
};
export default page;

This way we can use this getServerSession function whenever we want to protect a server component or page.

Now we will implement auth for the client side i. e. we are going to protect the protected-client page.

For the client side we can not use getServerSession but next-auth provides a hook that we can use which is useSession. But in order for the useSession hook to work we need to wrap the entire application in an AuthProvider components. So let's create that first.

// src/auth/AuthProvider.tsx
"use client";

import { SessionProvider } from "next-auth/react";
import { PropsWithChildren } from "react";

const AuthProvider = ({ children }: PropsWithChildren) => {
  return <SessionProvider>{children}</SessionProvider>;
};
export default AuthProvider;

Now we will use this in the layout.tsx file.

// src/app/layout.tsx
...
<html lang="en">
      <AuthProvider>
        <body className={inter.className}>
          <Navbar />
          {children}
        </body>
      </AuthProvider>
    </html>
...

Next we need to create an api route from which the SessionProvider can access the session and pass it to useSession .

So we create a route.ts file in [...nextauth] folder in auth folder in api folder in app folder.

import { authOptions } from "@/auth/authOptions";
import NextAuth from "next-auth";

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

Now finally we can use the useSession hook inside the protected-client .

// src/app/protected-client/page.tsx
"use client";

import { useSession } from "next-auth/react";
import { redirect } from "next/navigation";

const ProtectedClient = () => {
  const { data: session } = useSession();

  if (!session) redirect("/");

  return (
    <div className="p-6">
      <h1 className="text-3xl font-semibold">
        This is a client side page or component
      </h1>
    </div>
  );
};
export default ProtectedClient;

So at this point we have successfully protected server and client components.

Step 4 : Implementing Sign In

For this we will create a new component SignInDialog which will popup whenever we click the Sign In button.

So first let's download the dialog component from shadcn.

npx shadcn-ui@latest add alert-dialog
// src/components/SignInDialog.tsx
"use client";

import {
  AlertDialog,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import GoogleSignInBtn from "./GoogleSignInBtn";
import { buttonVariants } from "./ui/button";

const SignInDialog = () => {
  return (
    <AlertDialog>
      <AlertDialogTrigger
        className={buttonVariants({
          variant: "default",
        })}
      >
        Sign In
      </AlertDialogTrigger>
      <AlertDialogContent className="max-w-sm">
        <AlertDialogHeader>
          <AlertDialogTitle className="text-3xl font-bold text-center">
            Welcome!
          </AlertDialogTitle>
          <AlertDialogDescription className="text-center">
            Sign in to unlock endless possibilities!
          </AlertDialogDescription>
        </AlertDialogHeader>

        <GoogleSignInBtn /> {/* we will create this next*/}
        <AlertDialogCancel
          className={buttonVariants({
            variant: "destructive",
          })}
        >
          Cancel
        </AlertDialogCancel>
      </AlertDialogContent>
    </AlertDialog>
  );
};
export default SignInDialog;
// src/components/GoogleSignInBtn.tsx
import Image from "next/image";
import { Button } from "./ui/button";
import { signIn } from "next-auth/react";
import { useState } from "react";
import { Loader } from "lucide-react";

type GoogleSignInBtnProps = {
  callbackRoute?: string;
};

const GoogleSignInBtn = ({ callbackRoute = "/" }: GoogleSignInBtnProps) => {
  const [isLoading, setIsLoading] = useState(false);

  return (
    <Button
      className="flex items-center justify-center gap-4 py-6 flex-1 w-full text-md font-semibold mt-10"
      variant="secondary"
      onClick={() => {
        setIsLoading(true);
        signIn("google", { callbackUrl: callbackRoute });
      }}
    >
      {isLoading ? (
        <div className="flex items-center justify-center gap-x-2">
          <Loader className="h-6 w-6 animate-spin" />
          <p>Redirecting...</p>
        </div>
      ) : (
        <>
          <Image
            src="/google.png" {/*download this and save it in public folder*/}
            height={512}
            width={512}
            alt="google"
            className="w-6 h-6"
          />
          Sign In using Google
        </>
      )}
    </Button>
  );
};
export default GoogleSignInBtn;

Next in Navbar.tsx instead of using a hard-coded session variable we can now actually get the session value and in place of a functionless Sign In button we can use the SignInDialog component.

import Link from "next/link";
import Status from "./Status";
import { Button, buttonVariants } from "./ui/button";
import { getServerSession } from "next-auth";
import { authOptions } from "@/auth/authOptions";
import SignInDialog from "./SignInDialog";

const Navbar = async () => {
  const session = await getServerSession(authOptions);

  return (
    <nav className="flex items-center justify-between p-5 bg-blue-100 rounded-md shadow-lg">
      <Status />

      <Link
        href="/"
        className={buttonVariants({
          variant: "outline",
        })}
      >
        Home
      </Link>

      <Link
        href="/protected-server"
        className={buttonVariants({
          variant: "outline",
        })}
      >
        Server
      </Link>
      <Link
        href="/protected-client"
        className={buttonVariants({
          variant: "outline",
        })}
      >
        Client
      </Link>
      {!session ? (
        <SignInDialog />
      ) : (
        <Button variant="destructive">Sign Out</Button> // this is still functionless
      )}
    </nav>
  );
};
export default Navbar;

The SignInDialog should look like this:

We can also change the Status component to get the actual value of session.

// const session = false;
const session = await getServerSession(authOptions);

And now after we Sign In we should see the changes in effect.

The Status changes after we log in.

And we can access the protected routes.

The only thing left for the application to finish in the Sign out functionality.

Step 5 : Implementing Sign Out

Implementing this is very simple and straight forward. We just need to call the signOut function which is provided by the next-auth library when we press the Sign Out button.

// src/components/SignOutBtn

"use client";

import { signOut } from "next-auth/react";
import { Button } from "./ui/button";

const SignOutBtn = () => {
  return (
    <Button variant="destructive" onClick={() => signOut()}>
      Sign Out
    </Button>
  );
};
export default SignOutBtn;
// src/app/page.tsx
...
{!session ? <SignInDialog /> : <SignOutBtn />}
...

We use the SignOutBtn component in the app/page.tsx file.

At this point the application done. But I would implement one more thing which is like a dialog box showcasing user info when the user is signed in.

Step 6 : Implementing UserInfo dialog

For this we need the dropdown-menu component from shadcn .

npx shadcn-ui@latest add dropdown-menu

Next we create the UserInfo component.

"use client";

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

import { UserCircle } from "lucide-react";
import SignOutBtn from "./SignOutBtn";
import { getServerSession } from "next-auth";
import { authOptions } from "@/auth/authOptions";
import Image from "next/image";

const UserInfo = ({
  session,
}: {
  session: Awaited<ReturnType<typeof getServerSession<typeof authOptions>>>;
}) => {
  if (session) {
    return (
      <DropdownMenu>
        <DropdownMenuTrigger>
          {session.user?.image ? (
            <Image
              src={session.user.image}
              width={37}
              height={37}
              alt="image"
              className="rounded-full"
            />
          ) : (
            <UserCircle className="h-9 w-9" />
          )}
        </DropdownMenuTrigger>
        <DropdownMenuContent className="mr-6 w-[200px] space-y-2">
          <DropdownMenuLabel>My Account</DropdownMenuLabel>
          <DropdownMenuSeparator />
          <DropdownMenuItem>{session.user?.name}</DropdownMenuItem>
          <DropdownMenuItem>{session.user?.email}</DropdownMenuItem>
          <DropdownMenuSeparator />
          <DropdownMenuItem asChild>
            <SignOutBtn className="w-full" />
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    );
  }
};
export default UserInfo;
// src/components/Navbar.tsx
...
{!session ? <SignInDialog /> : <UserInfo session={session} />}
...

The UserInfo component looks like this:

Conclusion

NextAuth.js offers a robust solution for implementing both server-side and client-side authentication seamlessly. Through this blog, we've delved into the intricate details of its implementation, understanding how it operates efficiently on both ends of the spectrum. Leveraging powerful tools like Prisma for database management and integrating Google Auth for streamlined user authentication, NextAuth.js empowers developers to create secure and user-friendly authentication systems with relative ease.

For further exploration: https://next-auth.js.org


Connect with me on LinkedIn to stay updated on my latest projects and insights, or visit my Portfolio website to learn more about my work and expertise.

Thank you for joining me on this journey of exploring NextAuth.js and its capabilities in crafting seamless authentication experiences for web applications. I hope you found this blog informative and inspiring. Remember, the world of web development is vast and full of possibilities, so keep exploring, learning, and creating amazing things!

Happy Coding! ๐Ÿš€

ย