Integrate Google, Microsoft, Github multi-provider authentication in NextJS using Firebase Auth

wild_animal_96b2b14e183f8

Wild Animal

Posted on September 5, 2024

Integrate Google, Microsoft, Github multi-provider authentication in NextJS using Firebase Auth

Image description

I started developing my book review blog site refanswer.com using NextJS 14 with Google Firebase a month ago. Now that the website is live, I will document the solutions to several tricky problems encountered during the website development process and share them with Medium netizens.

This article mainly explains the problems encountered when NextJS 14 is connected to Google Authentication, and how I solved them.

Let me first present the solution I chose so that readers can better understand the process.

I used several main packages including firebase, firebase-admin, and iron-session to implement client-side login, server-side authentication, and data sharing between the two.

Client Side Configuration

Create a project and a web application under the project in the Google Firebase Console, and obtain key information such as: apiKey, authDomain, etc. (Please pay special attention to the authDomain parameter here, which will be very useful later). Provide this information to the configuration items of the Firebase client SDK to initialize the application, like this:

// lib/firebase.ts

import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
import { env } from "../../env";

const firebaseConfig = {
 authDomain:"projectId.firebaseapp.com"
 apiKey: "apiKey",
 projectId: "projectId",
 storageBucket: "projectId.appspot.com",
 messagingSenderId: "some_numbers",
 appId: "some_letters",
 measurementId: "some_letters",
};

export const firebaseApp = initializeApp(firebaseConfig);
Enter fullscreen mode Exit fullscreen mode

These parameters initialize the client application to request the Google Firebase Auth service.

Server Side Configuration

The most important process of server-side configuration is to create a service account in the Google Firebase Console and obtain the account private key for the server to authorize requests to Firebase.

The benefit of using a service account is that confidential data is placed on the server side for interaction, while reducing the complexity of Firebase security configuration.

After creating a service account, you will get a json file containing a private key. This file can be copied to any non-public location in your project directory, and then introduced in the Firebase Admin configuration file like this:

// lib/firebaseAdmin.ts

import admin from "firebase-admin";
import { getAuth } from "firebase-admin/auth";
import { getFirestore } from "firebase-admin/firestore";
interface FirebaseError extends Error {
 code: string;
}

if (!admin.apps.length) {
 admin.initializeApp({
 credential: admin.credential.cert(
 require("file_path_of_the_private_key.json")
 ),
 });
}

export const db = getFirestore();
export const auth = getAuth();
export const isFirebaseError = (error: any): error is FirebaseError => {
 return error && typeof error.code === "string";
};
Enter fullscreen mode Exit fullscreen mode

Enable Firebase Auth

Then, enable Firebase Authentication in the Google Firebase Console and enable the three providers Google, Microsoft, and Github in its login method.

Each provider you enable requires you to enter the clientId and clientKey. Here is a brief description. If you have any questions, please leave a message.

Google Provider

It is easiest to enable Google as a provider. You just need to turn on the switch, check the associated web application, click the Save button, and the clientId and clientKey will be automatically generated.

It is important to note that the authDomain you configured in lib/firebase.ts is the domain name of the Google Provider callback URL.

Since I am not using the host provided by Google, but Vercel’s edge network service, I cannot use the default authDomain provided by Google.

The default authDomain is refanswer-com.firebaseapp.com, and the corresponding callback URL is https://refanswer-com.firebaseapp.com/__/auth/handler.

I can only use the real domain name of my website, it is refanswer.com, and the callback address becomes https://refanswer.com/__/auth/handler.

Then, in the Auth settings page, add the authorized domains: refanswer.com

To use your own domain as the callback URL’s domain, you must also implement the /__/auth/handler route to properly handle the data on callback.

A simple way is to download the files directly from the Google server and save them in the public//auth/ directory, and configure the routing in next.config.mjs so that the URL https://refanswer.com//auth/handler can be correctly accessed to the public/__/auth/handler.js file.

// public/__/auth/download.sh

wget https://projectId.firebaseapp.com/__/auth/handler -O handler
wget https://projectId.firebaseapp.com/__/auth/handler.js -O handler.js
wget https://projectId.firebaseapp.com/__/auth/experiments.js -O experiments.js
wget https://projectId.firebaseapp.com/__/auth/iframe -O iframe
wget https://projectId.firebaseapp.com/__/auth/iframe.js -O iframe.js
Enter fullscreen mode Exit fullscreen mode

Configure next.config.mjs file:

// next.config.mjs

// …
headers: async () => {
 return [
      {
        source: "/__/auth/(handler|iframe)",
        headers: [
          {
            key: "Content-Type",
            value: "text/html; charset=utf-8",
          },
        ],
      },
   ];
 },
//…
Enter fullscreen mode Exit fullscreen mode

So far, you can use your own domain name to implement login callback.

Microsoft Provider

For the Microsoft provider, you need to log in to https://entra.microsoft.com/ to create an application, provide a callback address with your own domain name, and get the clientId and clientKey, fill in the google firebase auth method page, and then enable it.

Github Provider

For Github providers, you need to log in to github.com and create an application in the developer settings, and provide the above callback address to obtain clientId and clientKey.

Conflicts between multiple providers for one email

The Google Firebase Auth console provides a user account association feature that specifies how to handle conflicts when a user logs in to your application from different providers using the same email address.

The default option is to associate accounts that use the same email address. This has the advantage of keeping a one-to-one correspondence between the account and the email.

The difficulty is that you have to automate this process in your application.

The official solution is to catch errors during the user login process, obtain the provider previously used for the email from the error message, and then prompt the user whether to use the previous provider to log in to the application.

The main implementation process is as follows:

// components/Auth.tsx

import {
  getAuth,
  signInWithRedirect,
  getRedirectResult,
  GoogleAuthProvider,
  GithubAuthProvider,
  OAuthProvider,
  AuthProvider,
  signInWithPopup,
} from "firebase/auth";

export default function Auth(){
// ...
  useEffect(() => {
    if (searchParmas.has("redirected")) {
      setLoading(true);
      getRedirectResult(auth)
        .then((result) => {
          if (!result) {
            console.log("No redirect result");
            setLoading(false);
            return;
          }
          // sign in to server
          auth.currentUser
            ?.getIdToken()
            .then((idToken) => {
              if (!user) {
                setIsPending(true);
                postRequest<any>("/api/signInWithIdToken", { idToken })
                  .then((res) => {
                    setUser(res.user);
                    setEmitter({ name: "close" });
                  })
                  .catch((err) => {})
                  .finally(() => setIsPending(false));
              }
            })
            .catch((error) => {
              alert(error.message);
            });
        })
        .catch(async (error) => {
          // Handle Errors here.
          const errorCode = error.code;
          const errorMessage = error.message;
          // The email of the user's account used.
          const customData = error.customData;
          // The AuthCredential type that was used.
          const currentCredential = OAuthProvider.credentialFromError(error);
          // Get sign-in methods for this email.
          const signInMethods = await fetchSignInMethodsForEmail(
            auth,
            error.customData.email
          );
          console.error(
            "errorCode:" + errorCode,
            "errorMessage:" + errorMessage,
            "customData:" + customData,
            "currentCredential:" + currentCredential,
            "signInMethods:" + signInMethods
          );
          // Found conflicts from errorCode
          if (errorCode === "auth/account-exists-with-different-credential") {
            if (signInMethods.length > 0) {
              const existingProvider = getProviderForSignInMethod(
                signInMethods[0]
              );
              confirmAlert({
                title: `Sign in with ${signInMethods[0]}?`,
                message: `You have already logged in from ${signInMethods[0]} using ${customData.email}. You can only use the method you have logged in using this email. Do you want to log in with ${signInMethods[0]}?`,
                closeOnClickOutside: true,
                buttons: [
                  {
                    label: "Continue",
                    onClick: () => {
                      signInWithRedirect(
                        auth,
                        existingProvider as AuthProvider
                      );
                    },
                  },
                  {
                    label: "Cancel",
                    onClick: () => {
                      console.log("Cancel");
                    },
                  },
                ],
              });
            } else {
              alert(errorMessage);
            }
          } else {
            alert(errorMessage);
          }
          setLoading(false);
        })
        .finally(() => setLoading(false));
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [searchParmas]);
// ...
}
Enter fullscreen mode Exit fullscreen mode

There is a very important detail here, you must uncheck the “Email enumeration protection (recommended)” checkbox in user actions on the Firebase Auth settings page.

Image description

Otherwise, the value of signInMethods is empty and you will not be able to get the previous provider.

This article ends here. If you have any questions, please leave a message.

💖 💪 🙅 🚩
wild_animal_96b2b14e183f8
Wild Animal

Posted on September 5, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related