A step-by-step guide to setting up role-based authentication in a Next.js project using Prisma and Next-Auth.
pnpm create next-app ./
-
Install the latest version of Shadcn UI:
npx shadcn-ui@latest init
-
Add all the components of Shadcn:
npx shadcn-ui@latest add
-
Go to Railway Dashboard, click "Create Database" and create a new PostgreSQL database.
-
Configure Prisma with the following commands:
npx prisma init
-
Add
.env
to.gitignore
and configure theDATABASE_URL
in the.env
file as follows:DATABASE_URL="your_database_url_here"
-
Create a Prisma client as
prisma/prismaClient.ts
with the following content:import { PrismaClient } from "@prisma/client"; const prismaClientSingleton = () => { return new PrismaClient(); }; type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>; const globalForPrisma = globalThis as unknown as { prisma: PrismaClientSingleton | undefined; }; const MyPrismaClient = globalForPrisma.prisma ?? prismaClientSingleton(); export default MyPrismaClient; if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = MyPrismaClient;
-
Generate the Prisma client:
npx prisma generate
-
Install the necessary packages:
pnpm add next-auth @auth/prisma-adapter
-
In
_app.tsx
, set up the SessionProvider:import { SessionProvider } from "next-auth/react"; import "@/styles/globals.css"; import type { AppProps } from "next/app"; export default function App({ Component, pageProps: { session, ...pageProps }, }: AppProps) { return ( <SessionProvider session={session}> <Component {...pageProps} /> </SessionProvider> ); }
-
Install Prisma client:
pnpm add @prisma/client
Create and configure the API route pages/api/auth/[...nextauth].ts
with the following content:
// imports
import MyPrismaClient from "@/prisma/prismaClient";
import NextAuth, { AuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import { PrismaAdapter } from "@auth/prisma-adapter";
export const authOptions: AuthOptions = {
// adding prisma adapter
adapter: PrismaAdapter(MyPrismaClient),
providers: [
// we have the google auth privider for now
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}),
],
callbacks: {
// jwt is the token that we get from the provider this will run only once when the user logs in
async jwt({ token, user }) {
// here we are getting the user from database
const dbUser = await MyPrismaClient.user.findFirst({
where: {
email: token.email,
},
});
if (!dbUser) {
token.id = user!.id;
return token;
}
// jwt token returning the user data with the role
return {
id: dbUser.id,
name: dbUser.name,
role: dbUser.role,
email: dbUser.email,
picture: dbUser.image,
};
},
// session is the session object that we get from the jwt callback, we can get session data client side using useSession hook
async session({ session, token }: any) {
if (token) {
session.user.id = token.id;
session.user.name = token.name;
session.user.email = token.email;
session.user.picture = token.picture;
session.user.role = token.role;
}
// we returned all the user data
return session;
},
},
// this is the secret that we use to encrypt the jwt token
secret: process.env.NEXTAUTH_SECRET,
session: {
strategy: "jwt",
},
};
export default NextAuth(authOptions);
Define your Prisma schema with the following models:
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
role Role @default(USER)
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
enum Role {
ADMIN
USER
}
last thing lets generate the enviorment variables needed for the next-auth and google auth provider:
we will get these now:
- GOOGLE_CLIENT_ID =
- GOOGLE_CLIENT_SECRET =
- NEXTAUTH_SECRET =
1- go to google cloud platform
2- From the projects list in top left navbar, select a project or create a new one and select it
3- open right side bar and go to APIs & services
then Enabled APIs & services
4- go to oauth consent screen
and fill all the required stuff, just leave the optional ones we dont need them
5- then open credentials
page and click create credentials
button and select oauth client id
6- in Authorized JavaScript origins
add this URL: http://localhost:3000
7- in Authorized redirect URIs
add this: http://localhost:3000/api/auth/callback/google
8- click create, it will give u Client ID and Client secret copy paste them to your env file like this:
GOOGLE_CLIENT_ID = "client id here"
GOOGLE_CLIENT_SECRET = "client seceret here"
NEXTAUTH_SECRET = "put-random-thing-here"
create middleware.ts
file in root of your project:
import { withAuth } from "next-auth/middleware";
// this middleware will be used to protect routes that require authentication
export default withAuth({
callbacks: {
// this callback will run when the user logs in, if this callback returns false the user will be redirected to login page else user will be allowed to access the page
authorized: ({ token }: any) => {
// checking if the user has role of admin
if (token && token.role === "ADMIN") {
return true;
} else {
return false;
}
},
},
secret: process.env.NEXTAUTH_SECRET,
});
// this config is used to select which routed you want to protect, or use this middleware, in our case we want to protect all the routes thats under /admin route
export const config = { matcher: ["/admin/:path*"] };