DEV Community

Cover image for Auth.js v5 + Google OAuth: How to Create Users With the Correct Role (The Right Way)
Tanzim Hossain
Tanzim Hossain

Posted on

Auth.js v5 + Google OAuth: How to Create Users With the Correct Role (The Right Way)

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:

  1. User clicks “Continue with Google”
  2. Browser leaves your app
  3. Google handles authentication
  4. 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("; ");
}
Enter fullscreen mode Exit fullscreen mode

Then start OAuth:

setOAuthRole(role);
await signIn("google");
Enter fullscreen mode Exit fullscreen mode

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";
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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)