awinogrodzki / next-firebase-auth-edge

Next.js Firebase Authentication for Edge and Node.js runtimes. Compatible with latest Next.js features.
https://next-firebase-auth-edge-docs.vercel.app/
MIT License
523 stars 44 forks source link

Authenticate with Firebase Firestore and other client-side SDK's using `signInWithCustomToken` #204

Closed Henex1 closed 4 months ago

Henex1 commented 4 months ago
import { getApp, getApps, initializeApp } from "firebase/app";
import {
  connectAuthEmulator,
  getAuth,
  inMemoryPersistence,
  setPersistence,
  signInWithCustomToken,
} from "firebase/auth";
import { clientConfig } from "@/config/client-config";
import { getOrInitializeAppCheck } from "@/app-check";
import { doc, getDoc, getFirestore, onSnapshot } from "firebase/firestore";
import { useAuth } from "@/authContext/AuthContext";

export const app = !getApps().length ? initializeApp(clientConfig) : getApp();
export const getFirebaseApp = () => {
  if (getApps().length) {
    return getApp();
  }

  const app = initializeApp(clientConfig);

  if (process.env.NEXT_PUBLIC_FIREBASE_APP_CHECK_KEY) {
    getOrInitializeAppCheck(app);
  }

  return app;
};

export function getFirebaseAuth() {
  const auth = getAuth(getFirebaseApp());

  // App relies only on server token. We make sure Firebase does not store credentials in the browser.
  // See: https://github.com/awinogrodzki/next-firebase-auth-edge/issues/143
  setPersistence(auth, inMemoryPersistence);

  if (process.env.NEXT_PUBLIC_EMULATOR_HOST) {
    // https://stackoverflow.com/questions/73605307/firebase-auth-emulator-fails-intermittently-with-auth-emulator-config-failed
    (auth as unknown as any)._canInitEmulator = true;
    connectAuthEmulator(auth, process.env.NEXT_PUBLIC_EMULATOR_HOST, {
      disableWarnings: true,
    });
  }

  return auth;
}

export async function userDb(refName: string) {
  const { user }: any = useAuth();
  if (!user) {
    return;
  }
  const auth =  getFirebaseAuth();
  const { user: firebaseUser } = await signInWithCustomToken(
    auth,
    user.customToken
  );
  const db =  getFirestore(getFirebaseApp());
  const docRef = doc(db, refName, firebaseUser.uid);
  const docSnap = await getDoc(docRef);
  if (docSnap.exists()) {
    console.log("Document data:", docSnap.data());
  } else {
    // docSnap.data() will be undefined in this case
    console.log("No such document!");
  }
}
awinogrodzki commented 4 months ago

Hey @Henex1!

Client-side firestore requires to be called in authenticated environment.

You are using setPersistence(auth, inMemoryPersistence); as in starter example, which causes firebase client authentication to reset on page refresh.

You basically have two options here:

  1. You remove setPersistence(auth, inMemoryPersistence); and keep both server and client session active. You will have to keep those two session synchronised, otherwise you will run into consistency issues

  2. You can use signInWithCustomToken from firebase/auth before you use client-side firestore. You can pass server token to signInWithCustomToken and it should enable authenticated firebase environment until next page refresh

See https://firebase.google.com/docs/auth/web/custom-auth#authenticate-with-firebase for more information on how to use signInWithCustomToken

Henex1 commented 4 months ago

Please can you give me a sample code using the first option. I am still confused

Henex1 commented 4 months ago

This one is still not working.

"use client";
import { getFirebaseApp, getFirebaseAuth } from "../../lib/utils/auth/Firebase";
import { doc, getDoc, getFirestore } from "firebase/firestore";
import { useAuth } from "@/authContext/AuthContext";
import { signInWithCustomToken } from "firebase/auth";

const AuthUser = () => {
  const { user }: any = useAuth();
  if (!user) {
    return <>Loading</>;
  }
  console.log(user)
  const auth = getFirebaseAuth();
  const dataFetecher = async () => {
    try {
      const { user: authUser } = await signInWithCustomToken(
        auth,
        user.customToken
      );
      // Signed in
      // Handle the signed-in user information
      const db = getFirestore(getFirebaseApp());
      const docRef = doc(db, "umu-uzongwa-members", authUser.uid);
      const docSnap = await getDoc(docRef);
      if (docSnap.exists()) {
        console.log("Document data:", docSnap.data());
      } else {
        // docSnap.data() will be undefined in this case
        console.log("No such document!");
      }
    } catch (error) {
      // const errorCode = (error as { code: string }).code;
      const errorMessage = (error as { message: string }).message;

      // Handle the error
      console.log(errorMessage);
    }
  };
  dataFetecher();
  return (
    <main>
      <div className="flex items-center">
        <h1 className="text-lg font-semibold md:text-2xl text-red-500">
          Data information, please check
        </h1>
      </div>
    </main>
  );
};

export default AuthUser;
awinogrodzki commented 4 months ago

Hey, I just tested signInWithCustomToken and can confirm that it does not work with server token. I am investigating the issue and will get back to you shortly

awinogrodzki commented 4 months ago

In the meantime, you can just remove following lines to make firestore work on the client-side:

  // App relies only on server token. We make sure Firebase does not store credentials in the browser.
  // See: https://github.com/awinogrodzki/next-firebase-auth-edge/issues/143
  setPersistence(auth, inMemoryPersistence);
awinogrodzki commented 4 months ago

I created a pull-request that exposes customToken in handleValidToken and getTokens. This customToken can be used together with signInWithCustomToken to authenticate Firebase Client-side SDKs.

https://github.com/awinogrodzki/next-firebase-auth-edge/pull/206

I will be releasing canary version with those changes soon. I'll keep you updated within this issue.

Henex1 commented 4 months ago

Thank you so much for your effort, I have updated the npm version to 1.5.3. I have removed this line

  setPersistence(auth, inMemoryPersistence);

But I am still getting this error from the console on the browser.

Cannot read properties of undefined (reading 'customToken') AuthUser.tsx: undefined

Is there anything I am suppose to modify on the middleware?

awinogrodzki commented 4 months ago

I think you are getting this error by trying to access user.customToken value and user is undefined in this case

awinogrodzki commented 4 months ago

@Henex1 you can now install next-firebase-auth-edge@v1.6.0-canary.7

In this version, getTokens and handleValidToken exposes customToken string, which you can use together with signInWithCustomToken

Vicidevs commented 4 months ago

Ok @awinogrodzki please any code sample on how to use it to read data from firebase firestore client db? Thank you so much.

awinogrodzki commented 4 months ago

Hey @Vicidevs @Henex1! Thank you for you patience.

I have improved starter example and added Firestore Client SDK code that updates user counters collection

Screenshot 2024-07-15 at 13 49 20

Here's the function where we call signInWithCustomToken and Firestore Client SDK methods: https://github.com/awinogrodzki/next-firebase-auth-edge/blob/main/examples/next-typescript-starter/app/profile/UserProfile/user-counters.ts

Don't forget to pass custom token to user object, as in here: https://github.com/awinogrodzki/next-firebase-auth-edge/blob/main/examples/next-typescript-starter/app/shared/user.ts#L28

In case you plan to setup starter example, please remember to setup Firestore Database rules as mentioned in starter readme: https://github.com/awinogrodzki/next-firebase-auth-edge/tree/main/examples/next-typescript-starter#configuring-firestore-rules

I also added documentation on how to use client-side APIs: https://next-firebase-auth-edge-docs.vercel.app/docs/usage/client-side-apis#using-firebase-client-sdks

Please make sure to upgrade to next-firebase-auth-edge@1.6.0

Also, please note that with the introduction of custom token, the size of authorization cookie has increased twice in size, so if you use custom claims, it's recommended to set enableMultipleCookies to true, unless you're using Firebase Hosting: https://next-firebase-auth-edge-docs.vercel.app/docs/usage/middleware#multiple-cookies