One App, Two Roles: The Dual Google Provider Strategy for NextAuth.


Introduction: The Role Dilemma ?
In this post, We will be exploring how to implement dual Google authentication using NextAuth, and How this approach is useful in scenarios where user can have different roles — like in my case, building a job portal where a user can either be a candidate or a recruiter, Why we have opted for setting up separate Google providers instance for each role. What wrong with method of where we could have just used the global state containing role value. Keep reading to find out everything…
Why Standard Methods Fall Short with OAuth.
I first encountered this issue while i was building my job portal project. My goal was to allow users to sign in as either a recruiter or a candidate — and my initial idea was to use the value of the selected role directly from global state. which I was managing using Context API in my application.
I started off trying the “obvious” route: when the user clicks either the Recruiter or Candidate button on the landing page, I’d store that role in global state and redirecting user to the login page like:
/signin
On the sign-in page, when the user clicks the “Sign in with Google” button, I’d trigger the Google Auth flow using NextAuth:
const googleHandler = async () => {
setIsLoading(true);
try {
await signIn("google"); // Using signIn function given by next js itself , passing google as provider to trigger Authentication Via GOOGLE
} catch (error) {
toast("Server Error");
console.log("error", error);
} finally {
setIsLoading(false);
}
};
It would start the Google Auth flow using NextAuth. The plan was that once Google sends back the user data (Google returns a profile object containing details like id, email, name, and image), the idea was to grab the role from global state and create the user on the backend with that role if already exist with such email we move forward with authentication.
But here's the catch: NextAuth’s logic — including the signIn()
and other callbacks — runs on the server, and you can't access global state inside the server, This means the role value I was relying on was essentially unavailable when I needed it.
This led me to explore a better and more scalable approach: Setting up Dual Google Providers.
But before move forward let’s quickly break down how sign-in with Google works in this setup.
When the user clicks “Sign in with Google,” we hit the NextAuth server-side
signIn
callback.There, we receive the user's profile info from Google.
Then, we make a call to our backend API (
/auth/signup/google
) to check if this email already exists in the database.If the email doesn’t exist, we register the user as a new candidate or recruiter based on their role.
If the email already exists, we simply return success and proceed — no need to create a new user.
We then return
true
from thesignIn()
callback to allow the authentication to continue.
So yes — even though it’s called signIn
, we’re actually handling both sign-up and sign-in flows based on whether the user already exists. This hybrid approach is common in OAuth-based systems where you don’t have a traditional password field, and you rely on the OAuth provider (in this case, Google) for identity verification.
Implementation Walkthrough:
Pre-requisite: Needing a role
field in your user model/table
Step 1: Google Cloud Console - Create Two Client IDs
Navigate to your Google Cloud Project -> APIs & Services -> Credentials.
Click on Create credentials → click "OAuth Client IDs" (Type: Web application).
Add your authorized JavaScript origins (e.g.,
http://localhost:3000
, your production URL).Add redirects URLs like (These url’s help google to know where to redirect user after successful verification):
For development:
For production
http://{YOUR_DOMAIN}/api/auth/callback/google-candidate
http://{YOUR_DOMAIN}/api/auth/callback/google-recruiter
Step 2: Environment Variables (.env)
— Copy the Client ID and Client Secret and save them to your .env file.
Step 3: Configure NextAuth.
Import
GoogleProvider
.Create the
providers
array containing twoGoogleProvider
instances.Crucially: Assign unique
id
properties (e.g.,"google-candidate"
,"google-recruiter"
).
Like this :-
// api/auth/[...nextauth]
import NextAuth from "next-auth";
import { authConfig } from "../../../config/nextauthConfig";
const handler = NextAuth(authConfig);
export { handler as GET, handler as POST };
// config/nextauthConfig
import GoogleProvider from "next-auth/providers/google";
export const authConfig = {
providers: [
GoogleProvider({
id: "google-recruiter", // id for uniquely identify to which googleProvider should run on based on different user role.
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
profile(profile) { // profile() function allows you to shape or modify the user object that NextAuth will use.
return {
id: profile.sub,
name: profile.name,
email: profile.email,
role: "recruiter",
};
},
}),
GoogleProvider({
id: "google-candidate",
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
role: "candidate",
};
},
}),
],
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
async signIn({ account, profile }) {
console.log("google auth started");
if (account.provider.startsWith("google-")) {
const { email, name, role } = profile;
const response = await fetch(
`http://localhost:8383/v1/auth/signup/google`, // api to check register user if not exists with that email
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name,
email,
password: null,
role,
}),
}
);
const data = await response.json();
if (data.existingUser.role !== role) {
throw new Error(
`Already registered as ${data.existingUser.role}. Please sign in.`
);
}
return true;
}
return true; // allowing signIn for other providers as well
},
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.role = user.role;
token.name = user.name;
token.email = user.email;
}
return token;
},
async session({ token, session }) {
if (token) {
session.user.id = token.id;
session.user.role = token.role;
session.user.name = token.name;
session.user.email = token.email;
}
return session;
},
},
pages: {
signIn: "/signin",
error: "/auth/error",
},
};
NOTE: Do not forget to wrap you layout with sessionProvider
Frontend: Dynamic Provider Selection
Now in the frontend on the basis the value of the global role state, we have to use role respective google provider instance. We will do something like this:-
import { useRoleContext } from "../../app/context";
export function Signup() {
const { role } = useRoleContext();
const handleGoogleSignUp = async () => {
try {
const providerId =
role === "recruiter" ? "google-recruiter" : "google-candidate";
await signIn(providerId, {
callbackUrl: "/", // redirect acordingly...
}); // passing the id of the respective google provider instance
} catch (error) {
console.log("error", error);
} finally {
}
};
// your componenent logic and structure for signup form with google continue button clicking on which, will trigger handleGoogleSignUp function
}
Same on signin page:
import { useRoleContext } from "../../app/context";
export function SignIn() {
const { role } = useRoleContext();
const handleGoogleSignIn = async () => {
try {
const providerId =
role === "recruiter" ? "google-recruiter" : "google-candidate";
await signIn(providerId, {
callbackUrl: "/", // redirect acordingly...
}); // passing the id of the respective google provider instance
} catch (error) {
console.log("error", error);
} finally {
}
};
// your componenent logic and structure for signup form with google continue button clicking on which, will trigger handleGoogleSignIn function
}
Benefits of This Approach
Immediate Role Assignment: User gets the correct role right from the OAuth sign-up.
Improved UX: Avoids the mandatory post-signup role selection step.
Clean Separation: Logic is reasonably contained within NextAuth config and UI triggers.
Leverages Standard OAuth: Doesn't require complex hacks around the OAuth flow itself.
Thanks for reading till the end. If you found this interesting or learned something new, please give a follow.
Subscribe to my newsletter
Read articles from Neeraj bhatt directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
