lucy_who
lucy_who

Reputation: 81

next-auth with EmailProvider - is it possible to assign a new user to a role based on their email domain?

I'm using NextAuth and EmailProvider to handle log-in to a Next.js app, and new users are saved in a postgres database using Prisma. The schema includes a isAdmin field that defaults to false

I'd like to set that field to true for new users who sign in from a specific email domain but can't work out if that's possible - at the moment I need to go into the dB and set the flag manually once the user has signed up, which is obviously very much not ideal.

I'd very much appreciate any pointers, or even confirmation that what I'm trying to do is impossible!

ETA: I now think I may need to add either an Event or a Callback to the NextAuth function, but the documentation focuses on redirecting users rather than on user creation, so I'm still not sure if what I'm trying to do is even possible (https://next-auth.js.org/configuration/events).

Below is my pages>api>auth>[...nextauth].js code.

import NextAuth from "next-auth"
import EmailProvider from "next-auth/providers/email"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import prisma from "lib/prisma"

let data

if (process.env.NODE_ENV === "development") {
    data = {
        server: process.env.EMAIL_SERVER,
        from: process.env.EMAIL_FROM,
    }
} else {
    data = {
        server: {
            host: process.env.EMAIL_SERVER_HOST,
            port: process.env.EMAIL_SERVER_PORT,
            auth: {
                user: process.env.EMAIL_SERVER_USER,
                pass: process.env.EMAIL_SERVER_PASSWORD,
            },
        },
        from: process.env.EMAIL_FROM,
    }
}

export default NextAuth({
    providers: [EmailProvider(data)],

    database: process.env.DATABASE_URL,
    secret: process.env.SECRET,

    session: {
        jwt: true,
        maxAge: 30 * 24 * 60 * 60, // 30 days
    },

    debug: true,
    adapter: PrismaAdapter(prisma),

    callbacks: {
        session: async ({ session, user }) => {
            session.user.id = user.id
            session.user.isAdmin = user.isAdmin
            return Promise.resolve(session)
        },
    },
})

and my Prisma schema

  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
  isAdmin       Boolean   @default(false)
  accounts      Account[]
  sessions      Session[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

model Account {
  id                 String  @id @default(cuid())
  userId             String
  type               String
  provider           String
  providerAccountId  String
  refresh_token      String?  @db.Text
  access_token       String?  @db.Text
  expires_at         Int?
  token_type         String?
  scope              String?
  id_token           String?  @db.Text
  session_state      String?
  oauth_token_secret String?
  oauth_token        String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

Upvotes: 1

Views: 1396

Answers (2)

Philip Beber
Philip Beber

Reputation: 1233

If you want to intercept only at the point of creation you can do this by wrapping or patching the adapter object. For example:

...

const patchedAdapter = PrismaAdapter(prisma);
const originalCreateUser = patchedAdapter.createUser;
if (originalCreateUser) {
  patchedAdapter.createUser = async (user) => {
    const isAdmin = ...
    originalCreateUser({...user, isAdmin});
  }
}

export default NextAuth({
  ...
  adapter: patchedAdapter,
  ...

This intercepts the call from AuthJS to the DB and injects the isAdmin property. It will work a little better than what you have since the first user object returned will also have isAdmin set to true.

Hope this helps!

Upvotes: 0

lucy_who
lucy_who

Reputation: 81

I have solved my own problem and the answer is "no ...but". If it is possible to hijack the next-auth user creation process, I haven't found out how - but I now suspect that it would involve forking the next-auth code and would all round be a Really Bad Idea anyway. So "no, you can't assign a role to a user at the point of creation"

However, as I mentioned in my edit to my question, next-auth has the concept of Events, which allow you to execute your own code in response to next-auth life-cycle occurrences e.g. the signIn event is triggered every time a successful log in occurs.

So I used the signIn event, which receives the user object and an isNewUser boolean as params. I check if the user is newly-created and if so, do they have the specified email domain? If yes, then I update the database after the user has been created.

Since this is all done during sign in, it looks like a single-step process to the user.

In detail:

(1) add the required email domain to the .env file:

ADMIN_EMAIL_DOMAIN="whitelist.domain.com"

(2) add an event to the NextAuth function in [...nextauth].js:

//...code as above

import { updateUserToAdmin } from "lib/data.js"

//...code as above

export default NextAuth({
   //...code as above

    events: {
        signIn: async ({ user, isNewUser }) => {
            if (isNewUser) {
                const userEmail = user.email
                const isAdminEmail =
                    userEmail.split("@")[1] === process.env.ADMIN_EMAIL_DOMAIN

                isAdminEmail
                    ? await updateUserToAdmin(user.id, prisma, isAdminEmail)
                    : console.log(`non-Admin domain`)
            }
        },
    },
})

(3) add the updateUserToAdmin query to lib>data.js:

    return await prisma.user.update({
        where: {
            id: userId,
        },
        data: {
            isAdmin: isAdminEmail,
        },
    })
}

I hope someone else finds this helpful. Do feel free to comment if you want to suggest any improvements.

Upvotes: 3

Related Questions