Role-based authorization using NextAuth and Next.js server actions
Alamin Shaikh
Developer/Maker. Building software for everyone. Recently launched ???? devcoa.ch
In this article, we'll explore how to implement authentication using NextAuth's Google and Credentials providers, use NextAuth's callback functions to implement role-based authorization, configure NextAuth in the latest App Router, access sessions in server and client components, and sign in using Google and Credentials providers.
This is a step-by-step guide if you're looking to implement these features in a Next.js project using the App Router and server actions (non-API approach). Let's get started!
Install packages and create environment variables
First, install next-auth and bcrypt as dependencies and @types/bcrypt as a dev dependency in your project. Next, open your terminal and run openssl rand -base64 32 to create a random string. Copy the string and create an environment variable named NextAuth_SECRET in your project. After that, log in to your <MDXLink href='https://console.cloud.google.com/apis/credentials' text='Google Developers Console' target='_blank'/>, get the client ID and secret, and create two more environment variables named GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in your project.
Extend TypeScript modules
As we are using TypeScript, we need to extend the next-auth and next-auth/jwt modules to include the user's role type in the interfaces. To do so, add the following code in your types.ts file.
import { DefaultJWT } from 'next-auth/jwt';
import { DefaultSession, DefaultUser } from 'next-auth';
declare module 'next-auth' {
interface Session extends DefaultSession {
user: {
role: string;
} & DefaultSession['user'];
}
interface User extends DefaultUser {
role: string;
}
}
declare module 'next-auth/jwt' {
interface JWT extends DefaultJWT {
role: string;
}
}
Create auth options
Most of the NextAuth magic happens in the authOptions object. Within the lib directory, create an auth.ts file with the following code:
Note: We're using Prisma ORM with MongoDB in this implementation. You may need to adjust the database query part depending on the ORM and database you're using.
import bcrypt from 'bcrypt';
import { db } from '@server/config/db';
import { NextAuthOptions } from 'next-auth';
import Google from 'next-auth/providers/google';
import Credentials from 'next-auth/providers/credentials';
export const authOptions: NextAuthOptions = {
secret: process.env.NextAuth_SECRET,
session: {
strategy: 'jwt',
},
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}),
Credentials({
name: 'Credentials',
credentials: {
email: {
label: 'Email',
type: 'email',
placeholder: '[email protected]',
},
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if (!credentials || !credentials.email || !credentials.password)
throw new Error('Email or password is missing');
try {
const user = await db.user.findUnique({
where: { email: credentials.email },
});
if (!user || !user.hashedPassword) {
console.log('Invalid credentials');
throw new Error('Invalid credentials');
}
const isCorrectPassword = await bcrypt.compare(
credentials.password,
user.hashedPassword
);
if (!isCorrectPassword) {
console.log('Invalid credentials');
throw new Error('Invalid credentials');
}
return {
id: user.id,
role: user.role,
};
} catch (err) {
console.log(err);
throw err;
}
},
}),
],
callbacks: {
async signIn({ profile }) {
if (profile && profile.name && profile.email) {
const { name, email } = profile;
await db.user.upsert({
where: { email },
update: { name, email },
create: { name, email, role: 'USER' },
});
}
return true;
},
async jwt({ token, user }) {
if (user) token.role = user.role || 'USER';
return token;
},
async session({ session, token }) {
if (session.user) session.user.role = token.role;
return session;
},
},
};
In the above code, we export the authOptions object. Within the object, we add the secret and define the session strategy as jwt.
Next, we add the providers array. In this array, first, we set up the Google provider. After that, we set up the Credentials provider with the authorize function.
The authorize function runs on Credentials sign-in. Within the function, we first ensure that the email and password are provided. After that, we start a try...catch block to perform some asynchronous operations. Within the block, we query the database to ensure that a user exists in the database with the provided email address. Next, we compare the passwords using the bcrypt.compare method to ensure that the provided password is correct. Finally, we return a user object with the id and role properties.
Next, we have a callbacks object with three callback functions: signIn, jwt, and session.
The signIn callback runs on every sign-in. We utilize this feature to get the user's details and save them in our database on Google sign-in. To do so, in the function, we get the profile object by destructuring the function parameter. Within the function, we ensure that the profile object itself and the name and email properties in the profile object exist. After that, we destructure the profile object and get the name and email out of it. Finally, we use the upsert method to update the user in the database if they exist; otherwise, we create a new user and return true to allow the user to sign in.
The jwt callback runs when a user signs in, the JWT token is accessed, and the session is refreshed. We utilize these features to get the user's role to the token object. To do so, in the function, we get the token and user objects by destructuring the function parameter. Within the function, we ensure that the user exists and update the role property of the token object with the user's role if the role exists; otherwise, we update the role property of the token object with 'USER' and return the updated token.
The session callback runs when a session is created on sign-in and whenever it is accessed in the application. We utilize these features to get the user's role to the session.user object. To do so, in the function, we get the session and token objects by destructuring the function parameter. Within the function, we ensure that the user object exists in the session object and update the role property of the session.user object with data from the token object and return the updated session.
Hopefully, you now see the purpose of each callback and how we use them to meet our application's requirements: to save the user's data in our database on Google sign-in and get the user's role to the session.user object to implement role-based authorization.
Enjoying the content? Read the full article here.