This blog article will walk you through the process of utilizing Next Auth and the Prisma adaptor to create role-based authentication for your Next.js application. You'll have a solid basis for creating flexible and scalable user access control systems by the end.
Next.js App Directory will be used in the project. As you may know, from Next.js 13, we use the App directory and route files to define API routes.
Let's get started by installing the authentication library and constructing the auth API route.
npm install next-auth
To include Next Auth.js in the project, create a route.ts file in the app/api/auth/[...nextauth] folder.
You may add your authentication settings directly to this file, but I like to put them in a separate subdirectory so that I can reuse them later.
Create an options.ts file in the api/auth subdirectory and supply 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 more providers like GitHub or Facebook.
We can now design a route handler and include these options. Open the route file and paste this in.
For simplicity for this blog, we will only focus on role-based login. For a detailed explanation, you can refer the Freecodecamp Blog.
app/api/auth/[…nextauth]/route.ts
import { authOptions } from "@/utils/auth"; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };
We may now login in with our Google account. However, we do not save the users in a database. So let's get Prisma installed and start working on our authentication schema.
npm install prisma @prisma/client @next-auth/prisma-adapter
to initialize it:
npx prisma init
This will create a Postgresql migration file and execute it:
npx prisma migrate dev
Open schema.prisma file in prisma folder and add this code block:
datasource db { 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? role String? emailVerified DateTime? accounts Account[] sessions Session[] } model VerificationToken { identifier String token String @unique expires DateTime @@unique([identifier, token]) }
We will be using PostgreSQL with Supabase. Create a new project in Supabase named 'Role Based Auth.'
Once the project is created go to setting and then select the database option. Then in the connnection string flied select uri to get the Url. Now save the URL in the env file.
Return to the authentication settings and add our prisma adapter.
const prisma = new PrismaClient(); export const authOptions: NextAuthOptions = { adapter: PrismaAdapter(prisma), session: { strategy: 'jwt', }, providers: [ clientId: process.env.GOOGLE_ID!, clientSecret: process.env.GOOGLE_SECRET!, }), ], }, };
The adapter will now handle authentication and will add new users, sessions, and accounts to the database automatically.
However, when we try to react to the user using sessions (useSession hook on the client side and getServerSession on the server side), we get just the user's name, email, and picture. However, we require the role property for authorization.
To do this, we must first locate the user in the database and add its role value to the session. Let us now insert the following code:
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { AuthOptions } from "next-auth";
import { getServerSession } from "next-auth/next";
import GoogleProvider from "next-auth/providers/google";
export const authOptions: AuthOptions = {
session: {
strategy: "jwt",
},
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
profile(profile) {
return {
id: profile.sub,
name: `${profile.given_name} ${profile.family_name}`,
email: profile.email,
image: profile.picture,
role: 'user',
};
},
}),
],
callbacks: {
async jwt({ token, user }) {
return { ...token, ...user };
},
async session({ session, token }) {
if (session.user) {
session.user.role = token.role;
}
return session;
},
},
};
Now that everything is in place, when a person signs in, they will be allocated the user role. You can assign yourself as the admin using the Supabase dashboard or
npx prisma studio
. You can change your user role to admin there.If you are using Typescript you need to tell the Typescript that we have the add the role field as Google does not have the role field when using it.
Here we need to use module augmentation. Module augmentation enables us to expand the types or interfaces described in external modules or libraries without altering the original source code directly.
Now in your root file where we create the env file create another file next-auth.d.ts.
Inside the next-auth.d.ts file.
// Ref: https://next-auth.js.org/getting-started/typescript#module-augmentation import { DefaultSession, DefaultUser } from "next-auth" import { JWT, DefaultJWT } from "next-auth/jwt" declare module "next-auth" { interface Session { user: { id: string, role: string, email: string, } & DefaultSession } interface User extends DefaultUser { role: string, } } declare module "next-auth/jwt" { interface JWT extends DefaultJWT { role: string, } }
Here's a breakdown of what each section does:
Extending Session Interface in next-auth:
declare module "next-auth" { interface Session { user: { id: string, role: string, email: string, } & DefaultSession } }
This extends NextAuth.js's Session interface. It adds new properties (id, role, and email) to the session's user property, which now contains the default session properties (& DefaultSession). This change guarantees that the session object has these custom values (id, role, email) in addition to the normal ones.
Extending User Interface in "next-auth":
interface User extends DefaultUser { role: string, }
This extends the existing User Interface given by NextAuth.js. It adds a role property to the User interface. Now, whenever you refer to a User object in NextAuth.js, it will have this new role property.
Extending JWT Interface in "next-auth/jwt":
declare module "next-auth/jwt" { interface JWT extends DefaultJWT { role: string, } }
This extends the JWT interface provided by NextAuth.js for handling JSON Web Tokens (JWTs). It adds a role property to the JWT interface. When you work with JWTs using NextAuth.js, these JWTs will now include the role property.
To add Protected Routes for user and admin create a middleware file inside the app folder and paste the code.
import { withAuth } from "next-auth/middleware"; export default withAuth({ callbacks: { authorized: async ({ req, token }) => { if (req.nextUrl.pathname.startsWith("/admin")) return token?.role === "admin"; return !!token; }, }, }); export const config = { matcher: ["/admin:path*"] };
This code ensures that only the User with the Admin role assigned to them can access the admin page. You can add other routes as per your needs.