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 Google Developers Console, 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: 'john@example.com',
},
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.
Create NextAuth API route
Now, let's create the API route for NextAuth. To do so, within the app
directory, create an api
directory. Within the api
directory, create an auth
directory. Within the auth
directory, create the [...nextauth]
directory. Within the [...nextauth]
directory, create a route.ts
file with the following code:
import NextAuth from 'next-auth';
import { authOptions } from '@lib/auth';
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
In the above code, we create a handler
variable by calling the NextAuth
function with the authOptions
object. This is a request handler that will process authentication-related requests (e.g., sign-in, sign-out, etc.).
Create auth context
Now, to make the auth available throughout the application, we need an auth context. So, create a contexts
directory at the root of your project. Within the contexts
directory, create an Auth.tsx
file with the following code:
'use client';
import { SessionProvider } from 'next-auth/react';
export const AuthProvider = SessionProvider;
In the above code, we create and export the AuthProvider
variable by assigning the SessionProvider
to it. Good to know that, as we are using Next.js App Router, we need to export the AuthProvider
from a client component.
Add auth provider to the root layout
Now, navigate to the root layout.tsx
file and add the AuthProvider
like the following code:
import { ReactNode } from 'react';
import { AuthProvider } from '@contexts/Auth';
import { authOptions } from '@lib/auth';
import { getServerSession } from 'next-auth';
export default async function RootLayout({
children,
}: {
children: ReactNode;
}) {
const session = await getServerSession(authOptions);
return (
<html lang='en'>
<body>
<AuthProvider session={session}>{children}</AuthProvider>
</body>
</html>
);
}
In the above code, we use the getServerSession
function provided by the next-auth
package and pass the authOptions
to it to get the session
. After that, we wrap the children
with the AuthProvider
and pass the session
as a prop to it.
Access session in a server component
Following is an example of how we can access the session in a server component:
import { getServerSession } from 'next-auth';
import { authOptions } from '@lib/auth';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const session = await getServerSession(authOptions);
if (!session) redirect('/sign-in');
if (session.user.role !== 'ADMIN') redirect('/');
return <main>Dashboard</main>;
}
In the above code, we have a dashboard page that only an admin can access. As this is a server component, we use the getServerSession
function and pass the authOptions
to it to get the session
. If the session
doesn't exist, we redirect the user to the sign-in
page. If the session
exists but the user's role isn't 'ADMIN', we redirect the user to the home page. Otherwise, if the session
exists and the user's role is 'ADMIN', we allow the user access to the dashboard page.
Access session in a client component
Following is an example of how we can access the session in a client component:
'use client';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
export default function SignInPage() {
const router = useRouter();
const { data: session } = useSession();
if (session) {
if (session.user.role === 'USER') router.push('/');
if (session.user.role === 'ADMIN') router.push('/dashboard');
}
return (
<main>
<SignInForm />
</main>
);
}
In the above code, we have a sign-in page that we don't want a signed-in user to access. As this is a client component, we use the useSession
hook to access the session
. If the session
exists and the user's role is 'USER', we push the user to the home page; otherwise, if the user's role is 'ADMIN', we push the user to the dashboard page.
Sign in with the providers
Let's now take a look at how we can build the sign-in functionalities using the NextAuth Google and Credentials providers.
'use client';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
export default function SignInForm() {
const router = useRouter();
async function credentialSignIn(formData: FormData) {
const email = formData.get('email');
const password = formData.get('password');
const response = await signIn('credentials', {
email,
password,
redirect: false,
});
if (response?.error) return console.log(response.error);
router.push('/');
}
async function googleSignIn() {
signIn('google', { redirect: false, callbackUrl: '/' });
}
return (
<section>
<form action={credentialSignIn}>
<input type='email' name='email' placeholder='Email address' />
<input type='password' name='password' placeholder='Password' />
<input type='submit' />
</form>
<button onClick={googleSignIn}>Google Sign In</button>
</section>
);
}
In the above code, we have a SignInForm
component. It's a client component as the NextAuth signIn
function can only run in a client component. Within the component, we have two functions: credentialSignIn
and googleSignIn
.
Within the credentialSignIn
function, we get the email
and password
from the formData
. After that, we use the signIn
function and pass the provider (credentials
in this case) as the first argument and an object containing the data and options as the second. Finally, we check for errors and push the user to the home page upon successful sign-in.
The googleSignIn
function is straightforward. Within the function, we use the signIn
function and pass google
as the first argument and the options object as the second.
That's it! With this, it's a wrap on how you can use Next.js as a full-stack framework with server actions and NextAuth to implement authentication and role-based authorization. I hope this helps you understand NextAuth a little better and take advantage of the tool in your next project. Cheers!