If you are using Auth.js v5 (NextAuth) with Google OAuth and role-based onboarding, you will likely hit this issue:
No matter what role the user selects,
new users created via Google login always get the same role.
This article explains why this happens and shows a clean, reliable, production-safe solution that actually works.
No hacks. No guessing. No fragile behavior.
The Real Problem
In many applications, users choose a role during signup, for example:
- Student
- Teacher
Then they click Continue with Google.
But after Google login:
- The selected role is gone
- Google does not send role data
- Frontend state is lost
- Backend has no idea what role was chosen
As a result, developers hardcode a default role — which breaks onboarding.
Why This Happens (OAuth Reality)
OAuth is redirect-based.
The flow looks like this:
- User clicks “Continue with Google”
- Browser leaves your app
- Google handles authentication
- Browser returns to your app
Anything stored in:
- React state
- Props
- Local variables
- In-memory values
❌ does not survive this redirect
Google only returns identity, not application roles.
So unless you persist the role before OAuth starts, the backend will never see it.
The Correct Mental Model
OAuth handles identity
Your backend handles roles
Redirects destroy frontend state
To solve this, you need something that:
- Survives redirects
- Is available on the server
- Is short-lived
- Does not grant authority
That thing is a secure, temporary cookie.
The Solution: Secure Short-Lived Cookie
We store the user’s role choice in a cookie before starting Google OAuth.
Cookies automatically survive redirects and are available on the server.
This is a common pattern in real production systems.
Step 1: Store Role in a Secure Cookie (Client)
Before calling signIn():
function setOAuthRole(role: "student" | "teacher") {
document.cookie = [
`oauth_role=${role}`, // The data we need
"Path=/", // Available across the app
"Max-Age=300", // 5 minutes
"SameSite=Lax",
"Secure", // HTTPS only
].join("; ");
}
Then start OAuth:
setOAuthRole(role);
await signIn("google");
At this point:
- The role is safely stored
- Redirects won’t erase it
Step 2: Read the Cookie on the Server
Inside your Auth.js configuration:
import { cookies } from "next/headers";
function resolveOAuthRole(): "STUDENT" | "TEACHER" {
const cookieStore = await cookies();
const role = cookieStore.get("oauth_role")?.value;
return role === "teacher" ? "TEACHER" : "STUDENT";
}
This function:
- Validates input
- Applies a safe default
- Prevents invalid roles
Step 3: Create the User With the Correct Role
Inside callbacks.signIn:
async signIn({ user, account }) {
if (account?.provider === "google") {
const role = await resolveOAuthRole();
let authUser = await prisma.authUser.findUnique({
where: { email: user.email! },
});
if (!authUser) {
authUser = await prisma.authUser.create({
data: {
email: user.email!,
role,
provider: "google",
providerId: user.id,
},
});
}
user.id = authUser.id;
(user as any).role = authUser.role;
}
return true;
}
Now:
- New users get the correct role
- Existing users keep their role
- JWT and session reflect the database
Step 4: Delete the Cookie After Use (Important)
Always clean up after user creation:
cookies().delete("oauth_role");
This prevents accidental role reuse on future logins.
Security Considerations
This approach is safe because:
- Cookie is short-lived
- Role is validated server-side
- Database is the source of truth
- Cookie does not grant permissions
- Authorization still relies on RBAC
The cookie only represents temporary intent, not authority.
Why This Works Reliably
- Cookies survive OAuth redirects
- Auth.js v5 always exposes cookies on the server
- No dependency on provider behavior
- No fragile assumptions
- No undocumented internals
This is why this solution works consistently in real applications.
Final Takeaway
If you are using Auth.js v5 with:
- Google OAuth
- App Router
- Role-based onboarding
👉 Store role intent in a secure, short-lived cookie before OAuth
It is simple, predictable, and production-ready.
Remember This
OAuth gives you who the user is
Your system decides what the user can do
Once you separate those concerns, OAuth stops being confusing.
If you want follow-up articles:
- Role upgrade flow (student → teacher)
- Teacher approval workflow
- Multi-tenant roles per organization
- Auth.js v5 RBAC architecture
Just tell me — happy to help 🚀
Top comments (0)