Firebase does not authenticate when using next auth
Asked Answered
D

2

6

I'm using next-auth with firebase adapter. Everything is working fine in terms of saving users in database, but something is not working in terms of authentication.

import NextAuth from "next-auth"

import GoogleProvider from "next-auth/providers/google"
import { FirebaseAdapter } from "@next-auth/firebase-adapter"

import { db } from "../../../utils/firebase/firebase"
import * as firestoreFunctions from 'firebase/firestore'

import { adminAuth } from "../../../utils/firebase/firebaseAdmin"

import { getAuth, signInWithCustomToken } from "firebase/auth"

const auth = getAuth()

export default NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET,
      state: false,
    }),
  ],
  adapter: FirebaseAdapter({
    db: db,
    ...firestoreFunctions,
  }),
  callbacks: {
    async signIn({ user, account, profile, email, credentials }) {
      console.log(user, 'user')
      const customToken = await adminAuth.createCustomToken(user.id)
      const customSignIn = await signInWithCustomToken(auth, customToken)
      console.log(customSignIn, 'customSignIn')
      console.log(customSignIn.user, 'customSignIn.user')
      user = customSignIn.user
      console.log(user, 'user 2')
      return true
    },
    async redirect({ url, baseUrl }) {
      return baseUrl
    },
    async session({ session, user, token }) {
      if (session?.user) {
        session.user.id = token.sub
      }
      return session
    },
    async jwt({ token, user, account, profile, isNewUser }) {
      if (isNewUser) {
        const additionalClaims = {
          isStudent: true,
          isTeacher: false,
          isStaff: false,
          isAdmin: false
        }
        const customToken = await adminAuth.createCustomToken(token.sub, additionalClaims)
        const customSignIn = await signInWithCustomToken(auth, customToken)
        user = customSignIn.user

      }
      return token
    }

  },

  session: {
    strategy: 'jwt',
  },
})

My users can log in, but are not authenticated.

const auth = getAuth()

onAuthStateChanged(auth, (user) => {
  if (user) {
    // User is signed in, see docs for a list of available properties
    // https://firebase.google.com/docs/reference/js/firebase.User
    console.log('user')
    console.log(user)
    // ...
  } else {
    console.log('no user')
    // User is signed out
    // ...
  }
})

The Observer returns 'no user', but I'm logged in.

Detinue answered 28/3, 2022 at 22:4 Comment(6)
I don't understand, if you're using next-auth why are you using firebase auth sdk?Kirkwall
Because I need to write firestore rules, as I'm setting custom claims and also, use other firebase services such as Storage. Does it make sense? How would you do it?Chewink
does auth have a value before you pass it on? did you try to put onAuthStateChanged inside useEffect and see if it works?Kirkwall
nop, it does not have a value. I copied both inside useEffect (client) and outside it, (server), and my user is not recognized.Chewink
@DiegoGonzálezCruz Hi. I've faced the same problem. To solve the problem, I made a customToken issue api that can be used after sign in from next-auth, and the frontend kept requesting customToken after 1 hour. I also added a rule that invalidates the issued customToken when sign out of next-auth, creating an example of linking next-auth and firebase authentication. I think this example has secured a similar level of security to the session, and I wonder what you think. I will organize it more neatly and leave an answer.Irrespirable
@Irrespirable thanks! I saw your response in github, I will check it out next week.Chewink
E
5

You're not seeing a logged in user because the firebase auth is being done on the server side, and not the client side.

You should try passing the customToken to the session, then use it on the client side to sign in the user to firebase auth.

You would also want to wrap the useSession hook in a custom hook like below and use that instead of useSession.

const useFirebaseSession = () => {
  const session = useSession();
  const [status, setStatus] = useState(session.status);

  useEffect(() => {
    if (session && session.status === 'authenticated') {
      signInWithCustomToken(auth, session.customToken).then(() => {
        setStatus('authenticated');
      });
    }
  }, [session]);

  useEffect(() => {
    if(session.status !== 'authenticated') {
      setStatus(session.status)
    }
  }, [session.status]);

  return { data: session.data, status };
}
Empirin answered 30/3, 2022 at 8:3 Comment(8)
Hi! Another issue happened. If I pass the customToken to the session, then all the process creating documents could be potentially blocked by rules requiring auth !== null. Any ideas on how to trigger the signInWithCustomToken function client-side after signing in?Chewink
I'm not sure where the process is done, but if it's server-side, then you should sign in the user to firebase auth in the siginIn callback just like your original code did.Jumble
Would that still be considered as Signing In server-side?Chewink
Yea, all next-auth logic runs server-side, so anything you do in its callbacks is done server-side.Jumble
Do you think it's possible to use with firebase cloudfunction and firebase-admin to verify logged in user?Clinker
@KarolWojtulewicz yes, it should work.Jumble
I ended up using firebase-admin sdkChewink
This answer worked for me too, thanks @Empirin Note: a related github issue around security rules in next-auth repository github.com/nextauthjs/next-auth/issues/5049Siret
D
3

Ok, so what I finally did is: as @esi're said, pass the customToken created with firebase auth, to the session:

async jwt({
  token,
  user,
  account,
  profile,
  isNewUser
}) {
  if (isNewUser || user) {
    const additionalClaims = {
      isStudent: true,
      isTeacher: false,
      isStaff: false,
      isAdmin: false
    }
    const customToken = await adminAuth.createCustomToken(token.sub, additionalClaims)
    console.log(customToken, '***customToken')
    token.customToken = customToken
  }
  return token
}

Then, in the session callback:

async session({
  session,
  token,
  user
}) {
  if (session ? .user) {
    session.user.id = token.sub
    session.customToken = token.customToken
  }
  return session
},

And finally, in the page where users are first redirected:

  const [user, setUser] = useState()

  useEffect(() => {

    if (status === "authenticated") {
      const loggedInUser = {
        userName: session.user.name,
        userEmail: session.user.email,
        userPhoto: session.user.image,
        userUID: session.user.id,
      }

      signInWithCustomToken(auth, session.customToken)


      dispatch(setActiveUser(loggedInUser))

    }
  }, [status])

  const handleLogOut = () => {
    dispatch(setUserLogOutState())
    logout()
    signOut({
      callbackUrl: '/'
    }) //TODO: goodbye page
  
  
  // Rest of jsx code when session is true

And that's it. Hope it works for someone else too.

Thanks !

Detinue answered 30/3, 2022 at 10:45 Comment(2)
Note that you in general shouldn't be using hooks inside an if statement. They depend on execution order to be intact. It is most likely an easy fix in this case though.Schema
Wouldn't the signInWithCustomToken call make sense in the next-auth signIn callback?Geomancy

© 2022 - 2024 — McMap. All rights reserved.