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! ๐