Blog

Authenticate to Firebase with Google OAuth in a Managed Expo Workflow React Native Application that is compatible with Expo Go

Authenticate to Firebase with Google OAuth in a Managed Expo Workflow React Native Application that is compatible with Expo Go

We will guide you through the process of setting up a Firebase Authentication with Google OAuth login that is Expo Go compatible in a React Native Expo app that utilizes a managed workflow with Typescript.

Written by

Dénes Gutai

Published

AUG 23, 2023

Topics

#dev

Length

7 min read

Expo, Firebase and Google Sign In.

Table of contents


2024 Update: We recommend you to avoid using Expo Go at all if you implement OAuth. Opt-in for expo-dev-client which provides a similar DX, but has no limitations with deep linking.

Intro and prerequisites

In this article, we will guide you through the process of setting up a Firebase Authentication with Google OAuth login that is Expo Go compatible in a React Native Expo app that utilizes a managed workflow with Typescript. We discovered that the original documentation provided by Expo and Firebase was somewhat incomplete regarding this topic, and our intention is to fill in those gaps for you.

UPDATE: Though this solution will make the development phase easier, since there is no need to configure your and your team's environment to build native code, from Expo SDK 49 the GoogleAuth provider became deprecated. For more information on this change, please follow Expo's guidance. If you are just interested in the necessary modifications for production compatibility, there is a fantastic Medium article, which points out how the @react-native-google-signin/google-signin can interact with Firebase.

The code from this article can be found in this github repository.

The packages we'll need (with versions)

"@expo/webpack-config": "^18.1.1",
"dotenv": "^16.3.1",
"expo": "~48.0.18",
"expo-auth-session": "^4.0.3",
"expo-secure-store": "~12.1.1",
"expo-web-browser": "^12.3.2",
"firebase": "^9.23.0"

Google Cloud project setup

Creating the project

First we'll set up a google cloud project.

Head to https://console.cloud.google.com/

In the Select Project dropdown click on the New Project button

Fill out the form

Now head to https://console.cloud.google.com/apis/credentials

If this is your first project you have to setup a consent screen by clicking on Configure Consent Screen

Consent screen

We'll set this up to external users so other people will be able to test it too

Setup required app information

Now we need to add what kind of scopes we'd like to get access to for a user

After selecting them this is what we should see

After this we need to set up our test users who will be able to log in in our app

OAuth client

Okay, let's head back to https://console.cloud.google.com/apis/credentials and create our OAuth client. We'll need 3 different OAuth clients.

  • One for development mode (Web)
  • Two additional for the mobile platforms (Android, iOS)

On the top of the screen click on the + Create Credentials button and select OAuth client ID.

Web Client

This client has a purpose to let us work with the authentication in development mode. This will let us to stay in the Expo Go application and we will not need to change to bare workflow or prebuild our application every time.

Set our application type to Web Application and add a name to it.

Now we need to add the origin and redirect URIs. The origin should be https://auth.expo.io. The redirect should be in the following format https://auth.expo.io/@EXPO_CLI_USERNAME/PROJECT_SLUG

  • The EXPO_CLI_USERNAME is your username which you use to login to Expo.
  • The SLUG is the application's slug that can be set in the app.json or app.config.(js|ts).

It will generate the Google OAuth Client ID that we'll use for expo.

Android

This client will provide our Android platform's id. It will require us to add our Android Package name (found in app config as well) and an android keystore's fingerprint that will be used for signing the app.

iOS

This client will provide our iOS platform's id. It will require us to add our Bunld identifier (found in app config as well)


Firebase project setup

Open https://console.firebase.google.com/ and create a new project.

NOTE: We will not include analytics for the project because setting that up would require us to move to bare workflow.

After this we'll add Authentication to our Firebase project

On the Sign-in methods tab we'll add Google as our provider.

Enable it and fill the form. After that save this information

Now we can see our provider, but let's edit it with our Google Cloud Project information

Fill these two input fields based on the created Google Cloud Project's Client ID and secret

Lastly, head to the Project settings of your Firebase project

And save the Project ID and Web API key somewhere, we'll need them later.


Expo additional configuration with dotenv

For our app to function properly we have to change the metro config to the following ios.bundleIdentifier and android.package will be required later on in the release part.

const { getDefaultConfig } = require('@expo/metro-config');

const defaultConfig = getDefaultConfig(__dirname);
defaultConfig.resolver.assetExts.push('cjs');

module.exports = defaultConfig;

Modify the app.json / app.config.(js|ts) to have this content.

NOTE: The scheme, name, slug properties should match with the redirect URI previously set up in the google cloud project.

import { ExpoConfig } from 'expo/config';
import { config } from 'dotenv';
import path from 'path';

const env_file = path.join(__dirname, '.env');
const env = config({
  path: env_file,
});

if (env.error) {
  console.log('ENV FILE ERROR: ', env_file);
  throw env.error;
}

export const expoConfig: ExpoConfig = {
  scheme: 'scriptide-expo-firebase-demo',
  name: 'scriptide-expo-firebase-demo',
  slug: 'scriptide-expo-firebase-demo',
  version: '1.0.0',
  orientation: 'portrait',
  icon: './assets/images/icon.webp',
  userInterfaceStyle: 'light',
  splash: {
    image: './assets/images/splash.webp',
    resizeMode: 'contain',
    backgroundColor: '#ffffff',
  },

  assetBundlePatterns: ['**/*'],
  ios: {
    bundleIdentifier: 'scriptide.test.expo.firebase',
    supportsTablet: true,
  },
  android: {
    package: 'scriptide.test.expo.firebase',
    adaptiveIcon: {
      foregroundImage: './assets/adaptive-icon.webp',
      backgroundColor: '#ffffff',
    },

    intentFilters: [
      {
        action: 'VIEW',
        category: ['BROWSABLE', 'DEFAULT'],
      },
    ],
  },
  web: {
    favicon: './assets/favicon.webp',
  },
  extra: {
    ...env.parsed,
  },
};

export default expoConfig;

Auth logic

We are going to create a component that handles the auth logic. What we'll do is to save the user in a state variable to display the name and email of the logged in user.

Credentials file

Firstly we'll create a file where all our credentials will be stored. They should not be hardcoded, se we create a .env file first like this

# Information from the Firebase project
FIREBASE_API_KEY=AIza*****
FIREBASE_DOMAIN=scriptide-expo-firebase-demo.firebaseapp.com
FIREBASE_PROJECT_ID=scriptide-expo-firebase-demo


# Information from the Google Cloud Project's OAuth clients
EXPO_CLIENT_ID=560056473355-*******
ANDROID_CLIENT_ID=560056473355-*****
IOS_CLIENT_ID=560056473355-*****

Afterwise we'll create a file called firebaseConfig.ts

import { initializeApp } from 'firebase/app';
import Constants from 'expo-constants';

const firebaseConfig = {
  apiKey: Constants.expoConfig?.extra?.FIREBASE_API_KEY ?? '',
  authDomain: Constants.expoConfig?.extra?.FIREBASE_DOMAIN ?? '',
  projectId: Constants.expoConfig?.extra?.FIREBASE_PROJECT_ID ?? '',
};

export const expoClientId = Constants.expoConfig?.extra?.EXPO_CLIENT_ID;
export const iosClientId = Constants.expoConfig?.extra?.ANDROID_CLIENT_ID;
export const androidClientId = Constants.expoConfig?.extra?.IOS_CLIENT_ID;

export const app = initializeApp(firebaseConfig);

Retrieve id_token from the Google Provider

In the auth flow the first thing we have to do is to acquire the id_token for our Google OAuth Provider. For this we'll use a hook, namely useIdTokenAuthRequest

We need to import it first. But to have room for other providers let's import it this way

import { useIdTokenAuthRequest as useGoogleIdTokenAuthRequest } from 'expo-auth-session/providers/google';

And use it in our component like this

// Hook that gives us the function to authenticate our Google OAuth provider
const [, googleResponse, promptAsyncGoogle] = useGoogleIdTokenAuthRequest({
  selectAccount: true,
  expoClientId,
  iosClientId,
  androidClientId,
});

The two variables that we have access now from this hook is

  • promptAsyncGoogle which is a function that triggers the Google login
  • googleResponse in which we store the response of the Google login event

So let's call promptAsyncGoogle() if the user clicks on the Google login button:

// Handles the login via the Google Provider
const handleLoginGoogle = async () => {
  await promptAsyncGoogle();
};
<Button title={'Login'} onPress={handleLoginGoogle} />

After clicking on the button the Google prompt will appear and as the user logged in it will eventually update the googleResponse variable.

But this login is only for to request the id_token from the provider. This call is not yet the firebase login. So we need to log in to Firebase after this call is successful

Firebase login and user id_token

Let's watch our googleResponse variable and create an effect when it changes.

useEffect(() => {
  if (googleResponse?.type === 'success') {
    // ...Firebase login will come here
  }
}, [googleResponse]);

Now the Firebase login needs a credential to patch us through. This credential can be created from the provider's id_token. To do this we'll use the GoogleAuthProvider that is provided for us by the firebase package.

import { GoogleAuthProvider } from 'firebase/auth/react-native';

And to use it to generate credentials:

const credentials = GoogleAuthProvider.credential(
  googleResponse.params.id_token
);

So far this is how our useEffect looks like:

useEffect(() => {
  if (googleResponse?.type === 'success') {
    const credentials = GoogleAuthProvider.credential(
      googleResponse.params.id_token
    );
    // ...Firebase login will come here
  }
}, [googleResponse]);

Now we need to call the Firebase login that is also coming from the firebase package. But before that we need to set up a firebase auth instance outside of our component like this

// create Firebase auth instace
const auth = initializeAuth(app);

Now we can write out login function

const signInResponse = await signInWithCredential(auth, credentials);

For better code separation this could be better to be moved into a useCallback.

The type OAuthCredential is also from the firebase package.

// Function that logs into firebase using the credentials from an OAuth provider
const loginToFirebase = useCallback(async (credentials: OAuthCredential) => {
  const signInResponse = await signInWithCredential(auth, credentials);
}, []);

If our request is successful, we should get the user into the signInResponse.

So this is how our component looks like now:

import { useEffect, useCallback } from 'react';
import { Button } from 'react-native';
import { useIdTokenAuthRequest as useGoogleIdTokenAuthRequest } from 'expo-auth-session/providers/google';
import {
  signInWithCredential,
  GoogleAuthProvider,
  initializeAuth,
  OAuthCredential,
} from 'firebase/auth/react-native';
import {
  androidClientId,
  app,
  expoClientId,
  iosClientId,
} from './firebaseConfig';

// create Firebase auth instace
const auth = initializeAuth(app);

export const Auth: React.FC = () => {
  // Hook that gives us the function to authenticate our Google OAuth provider
  const [, googleResponse, promptAsyncGoogle] = useGoogleIdTokenAuthRequest({
    selectAccount: true,
    expoClientId,
    iosClientId,
    androidClientId,
  });

  // Handles the login via the Google Provider
  const handleLoginGoogle = async () => {
    await promptAsyncGoogle();
  };

  // Function that logs into firebase using the credentials from an OAuth provider
  const loginToFirebase = useCallback(async (credentials: OAuthCredential) => {
    const signInResponse = await signInWithCredential(auth, credentials);
  }, []);

  useEffect(() => {
    if (googleResponse?.type === 'success') {
      const credentials = GoogleAuthProvider.credential(
        googleResponse.params.id_token
      );
      loginToFirebase(credentials);
    }
  }, [googleResponse]);

  return <Button title={'Login'} onPress={handleLoginGoogle} />;
};

Save user to state and setup persistence with SecureStorage

It's nice that we are able to authenticate but let's save our user to the component and make sure that it's persisted even if the app gets closed.

Let's add a state variable

(The User type comes from the firebase package)

const [user, setUser] = useState<User | null>(null);

To save the user to this state we have to add a listener on the onAuthStateChanged property of our initialized auth instance.

useEffect(() => {
  auth.onAuthStateChanged((user) => {
    setUser(user);
  });
}, []);

This way we can easily add the user data to our screen like this

return (
  <>
    <Button title={'Login'} onPress={handleLoginGoogle} />
    {user && (
      <>
        <Text>Logged in as:</Text>
        <Text>{user.displayName}</Text>
        <Text>{user.email}</Text>
      </>
    )}
  </>
);

Let's extend the logic with a logout functionality that just basically signs the user out. Fortunately we only have to call one function because our previous listener will take care of the state change for the logout as well.

const handleLogoutGoogle = useCallback(() => {
  auth.signOut();
}, []);

Add the visuals too using a ternary:

return (
  <>
    {user ? (
      <>
        <Button title={'Logout'} onPress={handleLogoutGoogle} />
        <Text>Logged in as:</Text>
        <Text>{user.displayName}</Text>
        <Text>{user.email}</Text>
      </>
    ) : (
      <Button title={'Login'} onPress={handleLoginGoogle} />
    )}
  </>
);

The last thing we have to do is to persist our user even when the app gets closed. We are going to do it with SecureStorage.

Import it on the top

import * as SecureStore from 'expo-secure-store';

And the way we are able to set it up is to create our own persistor for the firebase auth instance.

// Persistor for React Native using Secure Storage
const secureStoragePersistor = getReactNativePersistence({
  getItem(key) {
    // ...Get an item by key from storage
  },
  setItem(key, value) {
    // ...Set value to an item by key into storage
  },
  removeItem(key) {
    // ...Remove an item by key from the storage
  },
});

SecureStorage has getItemAsync, setItemAsync, deleteItemAsync functions and these correspond in their typing with the ones above extending the getReactNativePersistence.

There is only one issue, that SecureStorage can only save keys that contain alphanumeric values. So we need to transform our keys from these functions to be compatible with this rule.

Here is a small regex replace that will only keep characters that are compatible with SecureStorage

const replaceNonAlphaNumericValues = (key: string) =>
  key.replaceAll(/[^a-zA-Z\d\s]/g, '');

So finally the persistor looks like this:

// Persistor for React Native using Secure Storage
const secureStoragePersistor = getReactNativePersistence({
  async getItem(key) {
    return SecureStore.getItemAsync(replaceNonAlphaNumericValues(key));
  },
  setItem(key, value) {
    return SecureStore.setItemAsync(replaceNonAlphaNumericValues(key), value);
  },
  removeItem(key) {
    return SecureStore.deleteItemAsync(replaceNonAlphaNumericValues(key));
  },
});

Right, let's add it to our auth instance via filling the deps object that is the second argument during initialization.

const auth = initializeAuth(app, {
  persistence: secureStoragePersistor,
});

TLDR.:

Let's see how our component looks like in it's final form.

import { useState, useEffect, useCallback } from 'react';
import { Button, Text } from 'react-native';
import { useIdTokenAuthRequest as useGoogleIdTokenAuthRequest } from 'expo-auth-session/providers/google';
import {
  signInWithCredential,
  GoogleAuthProvider,
  User,
  initializeAuth,
  getReactNativePersistence,
  OAuthCredential,
} from 'firebase/auth/react-native';
import {
  androidClientId,
  app,
  expoClientId,
  iosClientId,
} from './firebaseConfig';
import * as SecureStore from 'expo-secure-store';

// Util to remove non alphanumeric values from a string
const replaceNonAlphaNumericValues = (key: string) =>
  key.replaceAll(/[^a-zA-Z\d\s]/g, '');

// Persistor for React Native using Secure Storage
const secureStoragePersistor = getReactNativePersistence({
  async getItem(key) {
    return SecureStore.getItemAsync(replaceNonAlphaNumericValues(key));
  },
  setItem(key, value) {
    return SecureStore.setItemAsync(replaceNonAlphaNumericValues(key), value);
  },
  removeItem(key) {
    return SecureStore.deleteItemAsync(replaceNonAlphaNumericValues(key));
  },
});

// create Firebase auth instace
const auth = initializeAuth(app, {
  persistence: secureStoragePersistor,
});

export const Auth: React.FC = () => {
  const [accessToken, setAccessToken] = useState('');
  const [user, setUser] = useState<User | null>(null);

  // Sets the user to the state to be accessible in the component
  // @NOTE: This can be moved to a context so this component can act as a provider
  useEffect(() => {
    auth.onAuthStateChanged((user) => {
      setUser(user);
    });
  }, []);

  // Hook that gives us the function to authenticate our Google OAuth provider
  const [, googleResponse, promptAsyncGoogle] = useGoogleIdTokenAuthRequest({
    selectAccount: true,
    expoClientId,
    iosClientId,
    androidClientId,
  });

  // Handles the login via the Google Provider
  const handleLoginGoogle = async () => {
    await promptAsyncGoogle();
  };

  // Function that logs into firebase using the credentials from an OAuth provider
  const loginToFirebase = useCallback(async (credentials: OAuthCredential) => {
    const signInResponse = await signInWithCredential(auth, credentials);
    const token = await signInResponse.user.getIdToken();
  }, []);

  useEffect(() => {
    if (googleResponse?.type === 'success') {
      const credentials = GoogleAuthProvider.credential(
        googleResponse.params.id_token
      );
      loginToFirebase(credentials);
    }
  }, [googleResponse]);

  // Handles the logout using Google Provider
  const handleLogoutGoogle = useCallback(() => {
    auth.signOut();
  }, []);

  return (
    <>
      {user ? (
        <>
          <Button title={'Logout'} onPress={handleLogoutGoogle} />
          <Text>Logged in as:</Text>
          <Text>{user.displayName}</Text>
          <Text>{user.email}</Text>
        </>
      ) : (
        <Button title={'Login'} onPress={handleLoginGoogle} />
      )}
    </>
  );
};

Scriptide is a highly skilled software development company that specializes in custom, complex B2B software solutions. We offer a wide range of services, including digital transformation, web and mobile development, AI, blockchain, and more.

Get a free IT consultation. We are excited to hear from you.

You might also like these articles!

Expo with React Native in 2024

Click for details

Should I Use Expo for React Native in 2024?

The seemingly never-ending debate about whether you should use Expo for your next React Native project is coming to an end. Spoiler: both Expo and React Native are winners.

#dev

APR 10, 2024

9 min read

Objective-C code in a VS Code editor.

Click for details

Tracking active window on MacOS using Objective-C in an Electron app

In this article we'll show to track active windows on MacOS in an Electron app quite painlessly with Objective-C++. It can also be used natively without Electron.

#dev

OCT 05, 2022

4 min read

By clicking 'Accept' you agree to the use of all cookies as described in our Privacy Policy.

© 2024 Scriptide Ltd.

All rights reserved

D-U-N-S® Nr.: 40-142-5341

VAT ID (HU): HU27931114

Registration Number (HU): 01 09 357677

Privacy