In this blog post, we’ll walk you through the step-by-step process of creating role-based authentication for your Next.js application using Next Auth and Prisma adapter. You’ll have a solid foundation to build flexible and scalable user access control systems by the end.
In the project, we’ll be using Next.js App Directory. As you know, after Next.js 13, we create API routes using the App directory and route files.
Let’s install the authentication library and start creating the auth API route.
npm install next-auth
To add Next Auth.js to the project create a file called route.ts in app/api/auth/[…nextauth] folder.
You can directly add your auth options in this file, but I prefer using a different folder to be able to reuse the options later.
Let’s create an auth.ts file in src/utils folder and give providers.
import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
export const authOptions: NextAuthOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID!,
clientSecret: process.env.GOOGLE_SECRET!,
}),
],
};
If you want to add other providers such as GitHub, Facebook.
We are now ready to create a route handler and add these options. Open up the route file and add this.
app/api/auth/[…nextauth]/route.ts
import { authOptions } from "@/utils/auth";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Now, we can sign in using a Google account. But we don’t save the users in a database. So let’s install Prisma and create our authentication schema.
npm install prisma @prisma/client @next-auth/prisma-adapter
to initialize it:
npx prisma init
Open schema.prisma file in prisma folder and add this code block:
datasource db {
//You can use any database provider. It doesn't have to be PostgreSQL
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
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
image String?
isAdmin Boolean @default(false)
emailVerified DateTime?
accounts Account[]
sessions Session[]
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
We use This standard schema for the Prisma adapter; you can find it in the official documentation.
I’ve only added the isAdmin field in the user model to give a role for users. I use a boolean because my application has only two roles (admin/regular user). But if you want to, you can create more roles by changing that field.
This will create an SQL migration file and execute it:
npx prisma migrate dev
Let’s get back to the auth options and add our prisma adapter.
src/utils/auth.ts
const prisma = new PrismaClient();
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID!,
clientSecret: process.env.GOOGLE_SECRET!,
}),
],
};
From now on, the adapter will handle authentication and automatically add new users, sessions, and accounts into the database.
But when we try to react the user using sessions (useSession hook for the client side and getServerSession for the server side), it’ll return only, name,email and image. But we need isAdmin property for the authorization.
To do that, we should first find the user in the database and add its isAdmin value to the session. Let’s add the following code:
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
session: {
strategy: 'jwt',
},
providers: [
clientId: process.env.GOOGLE_ID!,
clientSecret: process.env.GOOGLE_SECRET!,
}),
],
callbacks: {
async session({ token, session }) {
if (token) {
session.user.isAdmin = token.isAdmin;
}
return session;
},
async jwt({ token }) {
const dbUser = await prisma.user.findUnique({
where: {
email: token.email!,
},
});
token.isAdmin = Boolean(dbUser?.isAdmin);
return token;
},
},
};
In the jwt callback, we take the isAdmin value from the database and hide it in the JWT. And in the session callback, we take the token, find the isAdmin value that we’ve set, and add it into session. And know when we attempt to reach the session, it’ll return the isAdmin property along with other user properties.
If you have any problem at this point, try to clean the cache in the .next folder and cookies in the browser.