🔒

【Expo】react-native-firebase でユーザ認証を実装する

2023/12/04に公開1

Firebase Authentication では Google、Facebook、Eメール、SMS などのプロバイダを用いてユーザ認証を行う便利な機能が提供されています。

今回は Expo アプリ上で react-native-firebase を利用したユーザ認証機能を実装したいと思います。

この記事は React Native Advent Calendar 2023 4日目で執筆しました。

前提知識

react-native-firebase は Firebase のさまざまなサービスを React Native に実装できるライブラリです。

しかしネイティブコードを含むため残念ながらそのままの Expo プロジェクトでは動作しません。

ここで以前まではプロジェクトを eject して Bare Workflow に移行しなければならなかったのですが、現在では Expo 管理下のままネイティブコードを実行する機能があるためこれを使います。(詳細は後述)

捕捉として JS SDK を利用すれば環境を移行せず Firebase を実装できますが、対応するサービスが限られるため要注意です。

セットアップ

1. Expo

prebuild について

先述の expo eject コマンドですが現在は非推奨になっており、その代わりに prebuild という機能が提供されています。

prebuild を行うとプロジェクトのルートに android ios ディレクトリが作成され、その中にネイティブコードを自動生成します。

これによって Expo 上でもネイティブコードを含むライブラリを利用できるようになります。

prebuild のメリットとしてはこれらが挙げられます。

  • Expo 管理下で開発を続けられる
  • ネイティブコードの面倒な構成やパッケージのリンキングを自動で行ってくれる
  • android ios ディレクトリを削除すればネイティブコードを利用しない Expo プロジェクトに復帰できる

プロジェクトのセットアップ

まずは prebuild を実行して android ディレクトリが生成されることを確認しましょう。

$ npx expo prebuild

Expo アプリの構成ファイル ( app.json / app.config.json ) を変更した際は --clean オプションと一緒に prebuild します。

$ npx expo prebuild --clean

Authentication を利用するには以下のパッケージをインストールし、Autolinking が働くように app.jsonexpo.plugins 配列にインストールしたネイティブパッケージを追加します。

どの認証プロバイダでも共通して必要
$ npx expo install @react-native-firebase/app
$ npx expo install @react-native-firebase/auth

Google で認証する場合 (+α)
$ npx expo install @react-native-google-signin/google-signin

メールリンクで認証する場合 (+α)
$ npx expo install @react-native-firebase/dynamic-links
$ npx expo install @react-native-async-storage/async-storage # あると便利
app.json
{
    "expo": {
        ...
        "plugins": [
            "@react-native-firebase/app",
            "@react-native-firebase/auth",
            "@react-native-firebase/dynamic-links",
            "@react-native-google-signin/google-signin"
        ]
    }
}

Google もしくはメールリンクで認証する場合は後ほどデバッグ用署名を Firebase に設定する必要があるため、事前に SHA-1 を取得&コピーしておきます。

デバッグ用のキーストアファイルは <Expoプロジェクト>/android/app/debug.keystore にあります。

$ keytool -list -v -keystore ./android/app/debug.keystore
Enter keystore password: (空のパスワード)

2. Firebase プロジェクト

まだプロジェクトを作成していない場合は Firebase コンソール からプロジェクトを作成してください。

プロジェクトが用意できたら Firebase に Android アプリを登録します。
※トップ画面の Android アイコンから登録できます。

先ほどの手順でデバッグ用の署名証明書を取得した場合はそちらも忘れず入力しましょう。

Android アプリの登録

次のステップで構成ファイル google-services.json がダウンロードできたら残りの手順はスキップしてしまって構いません。

3. Firebase Authentication

これでプロジェクトの準備が完了したので次は Authentication をセットアップしていきます。

ナビゲーションメニューの「プロジェクトのカテゴリ」から 構築 > Authentication を選択した後に「始める」から Sign-in method 画面に進みます。

Google 認証

プロバイダの一覧から「Google」を選択し、プロジェクトの表示名と連絡用のメールアドレスを入力します。

Google 認証のセットアップ

初めて OAuth プロバイダを追加すると構成ファイルに OAuth 構成が追加されるため、次のステップで新しい google-services.json をダウンロードしておきます。

メール/パスワード認証

メールアドレスとパスワードの組み合わせを使って認証する方法です。

プロバイダの一覧から「メール / パスワード」を選択して「メール / パスワード」を有効にします。

メール/パスワード認証のセットアップ

メールリンク認証

ユーザのメールアドレスに認証専用のリンクを送信して認証する方法です。

プロバイダの一覧から「メール / パスワード」を選択して「メールリンク(パスワードなしでログイン)」を有効にします。
※「メール / パスワード」も自動的に有効になります。

メールリンク認証のセットアップ

メールリンク認証を利用する場合のみ必要なセットアップです。

コンソール内のナビゲーションメニューにある「プロジェクトのカテゴリ」から エンゲージメント > Dynamic Links を選択したら「続行」からセットアップを開始します。
※サービスの非推奨に関するメッセージは無視してしまって構いません。

URL 接頭辞の追加ではユーザに送信する認証リンクのドメインを指定します。

独自ドメインを設定することもできますが今回は Google が無料で提供する page.link ドメインを使うことにします。

URL 接頭辞の追加

指定したサブドメインが既存のものと重複していなければセットアップを完了できます。

実装

プロジェクトのルートに先ほどダウンロードしてきた google-services.json を追加し、app.jsonexpo.android.googleServicesFile にその相対パスを記述します。

app.json
{
    "expo": {
        "android": {
	    "googleServicesFile": "./google-services.json"
	}
    }
}

構成ファイルを変更したので prebuild も済ませておきましょう。

$ npm run android

これで必要最低限の準備が完了したためコーディングに進みます。
※サンプルコードのエラーハンドリングは省いています。

Google 認証

GoogleSignin で取得した ID トークンを Firebase に渡すことで認証を行います。

webClientIdgoogle-services.jsonclient.oauth_client 内にある client_id をコピーして指定します。

Social Authentication | React Native Firebase

SignInWithGoogle.ts
import { useEffect, useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import FirebaseAuth from '@react-native-firebase/auth';
import { GoogleSignin } from '@react-native-google-signin/google-signin';

const auth = FirebaseAuth();

// Google サインインに必須
GoogleSignin.configure({
  // 自身の Web Client ID に置き換える
  webClientId: 'xxxxx.apps.googleusercontent.com',
});

export default function SignInWithGoogle() {
  const [email, setEmail] = useState<string | null>(null);

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged(() => {
      if (auth.currentUser) {
        setEmail(auth.currentUser.email);
      }
    });

    return () => unsubscribe();
  }, []);

  const signIn = async () => {
    // Google のログイン画面を表示して認証用の ID トークンを取得する
    const user = await GoogleSignin.signIn();
    const idToken = user.idToken;

    if (idToken === null) {
      return;
    }

    // 取得した認証情報 (ID トークン) を元にサインインする
    const credential = FirebaseAuth.GoogleAuthProvider.credential(idToken);
    await auth.signInWithCredential(credential);
  };

  const signOut = async () => {
    auth.signOut().then(() => setEmail(null));
  };

  return email ? (
    <View style={styles.container}>
      <Text style={styles.text}>
        {`${email} でサインインしています`}
      </Text>
      <Pressable style={styles.button} onPress={signOut}>
        <Text style={styles.buttonText}>
          サインアウト
        </Text>
      </Pressable>
    </View>
  ) : (
    <View style={styles.container}>
      <Pressable style={styles.button} onPress={signIn}>
        <Text style={styles.buttonText}>
          Google でサインイン
        </Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({ /* styles */ });

<サインイン前>

サインイン前

<サインイン中>

サインイン中

<サインイン後>

サインイン後

メール / パスワード認証

パスワードで認証するにはサインイン前に新しいメールアドレスとパスワードでアカウントを作成する必要があります。

Authentication | React Native Firebase

SignInWithEmailAndPassword.ts
import { useEffect, useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import FirebaseAuth from '@react-native-firebase/auth';

const auth = FirebaseAuth();

// 通常はユーザが入力したメールアドレス・パスワードを使用する
const userEmail = 'info@example.com';
const userPassword = 'password';

export default function SignInWithEmailAndPassword() {
  const [email, setEmail] = useState<string | null>(null);

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged(() => {
      if (auth.currentUser) {
        setEmail(auth.currentUser.email);
      }
    });

    return () => unsubscribe();
  }, []);

  const createAccount = async () => {
    const credential = await auth.createUserWithEmailAndPassword(userEmail, userPassword);
    const user = credential.user;
  };

  const signIn = async () => {
    const credential = await auth.signInWithEmailAndPassword(userEmail, userPassword);
    const user = credential.user;
  };

  const signOut = async () => {
    auth.signOut().then(() => setEmail(null));
  };

  return email ? (
    <View style={styles.container}>
      <Text style={styles.text}>
        {`${email} でサインインしています`}
      </Text>
      <Pressable style={styles.button} onPress={signOut}>
        <Text style={styles.buttonText}>
          サインアウト
        </Text>
      </Pressable>
    </View>
  ) : (
    <View style={styles.container}>
      <Pressable style={styles.button} onPress={createAccount}>
        <Text style={styles.buttonText}>
          アカウント作成
        </Text>
      </Pressable>
      <Pressable style={styles.button} onPress={signIn}>
        <Text style={styles.buttonText}>
          メールでサインイン
        </Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({ /* styles */ });

<アカウント作成前/サインイン前>

アカウント作成前/サインイン前

<アカウント作成後/サインイン後>

アカウント作成後/サインイン後

メールリンク認証

ユーザのメールにサインイン用のディープリンクを送信し、ユーザがリンクを踏んだ後は Dynamic Links 経由でアプリに復帰してもらいます。

アプリ復帰時は意図しないデバイスでサインインされていないか検証するため、送信先メールアドレスとリンク元のメールアドレスが一致するか判断しなければなりません。

その際ユーザにメールアドレスを再入力してもらっては二度手間になってしまうため、メールリンクを送る際に AsyncStorage でメールアドレスを保存して再利用する方法がおすすめです。

Android でメールリンクを使用して Firebase 認証を行う

SignInWithEmailLink.ts
import { useEffect, useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import FirebaseAuth from '@react-native-firebase/auth';
import FirebaseDynamicLinks from '@react-native-firebase/dynamic-links';
import AsyncStorage from '@react-native-async-storage/async-storage';

const auth = FirebaseAuth();

// 通常はユーザが入力したメールアドレスを使用する
const userEmail = 'info@example.com';

export default function SignInWithEmailLink() {
  const [email, setEmail] = useState<string | null>(null);

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged(() => {
      if (auth.currentUser) {
        setEmail(auth.currentUser.email);
      }
    });

    return () => unsubscribe();
  }, []);

  // メールからのリンクを検知してサインインを試みる
  useEffect(() => {
    const dynamicLinks = FirebaseDynamicLinks();

    const tryToSignIn = async (link: string) => {
      if (auth.isSignInWithEmailLink(link)) {
        const email = await AsyncStorage.getItem('email-for-link-auth');

        if (email === null) {
          return;
        }

        // 検証用メールアドレスとダイナミックリンクを渡して認証する
        const credential = await auth.signInWithEmailLink(email, link);
        const user = credential.user;

        // 検証用メールアドレスはもう必要ないためストレージから削除する
        await AsyncStorage.removeItem('email-for-link-auth');

        setEmail(user.email);
      }
    };

    // アプリ未起動時にリンクが踏まれるケース
    dynamicLinks.getInitialLink().then((link) => {
      if (link) {
        tryToSignIn(link.url);
      }
    });

    // アプリ起動中にリンクが踏まれるケース
    const unsubscribe = dynamicLinks.onLink((link) => tryToSignIn(link.url));
    return () => unsubscribe();
  }, []);

  const sendSignInLink = async () => {
    const actionCodeSettings = {
      // メールリンクのリダイレクト先 URL
      // ※ドメインを Firebase のホワイトリストに登録すること
      url: 'https://example.com',
      handleCodeInApp: true,
      android: {
        // Android アプリのパッケージ名
        packageName: 'com.example.myapp',
        // デバイスにアプリがインストールされていなかった際にインストールを要求するか
        installApp: true,
        // 要求するアプリの最小バージョン
        minimumVersion: '1',
      },
      // Dynamic Links のセットアップで用意したドメイン
      dynamicLinkDomain: 'example.page.link',
    };

    // ユーザにメールリンクを送信する
    await auth.sendSignInLinkToEmail(userEmail, actionCodeSettings);
    // メールアドレス検証のために送信先メールアドレスを保存する
    await AsyncStorage.setItem('email-for-link-auth', userEmail);
  };

  const signOut = async () => {
    auth.signOut().then(() => setEmail(null));
  };

  return email ? (
    <View style={styles.container}>
      <Text style={styles.text}>
        {`${email} でサインインしています`}
      </Text>
      <Pressable style={styles.button} onPress={signOut}>
        <Text style={styles.buttonText}>
          サインアウト
        </Text>
      </Pressable>
    </View>
  ) : (
    <View style={styles.container}>
      <Pressable style={styles.button} onPress={sendSignInLink}>
        <Text style={styles.buttonText}>
          認証リンクを送信
        </Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({ /* styles */ });

ActionCodeSettings に指定するリダイレクト先のドメインをホワイトリストに登録します。

Firebase コンソールの Authentication から Settings に進み、承認済みドメインを開くと登録することができます。

承認済みドメイン

<サインイン前>

サインイン前

<メールリンク受信>
私が個人開発している作業記録アプリのサンプルメールです。

メールリンク受信

<サインイン後>

サインイン後

実行する

npm run android を実行するとエミュレータが起動してビルドが開始されます。

$ npm run android

ユーザ認証時に DEVELOPER_ERROR と怒られる場合

Firebase の Android アプリに設定した SHA-1 が間違っている可能性があります。

先述の手順でデバッグ用の署名を確認してみてください。

Execution failed for task ':expo-modules-core:compileDebugKotlin'. と怒られて起動できない

Firebase に直接関係はないのですがエミュレータ起動中にこのエラーに遭遇しました。

原因としては Expo の prebuild が 8.0.1 の Gradle を使おうとすることで Gradle と Java のバージョン不一致が起きてしまったようです。

お互いのバージョンは完全に一致する必要があるとのことなので Compatibility Matrix は要チェックかと思います。

現状 prebuild するごとに毎度 gradle-wrapper.propertiesdistributionUrl で Gradle バージョンを変えてその場をしのいでいるのですが、もし楽な解決方法があればご教授ください...

最後に

今回は Expo アプリで Firebase Authentication の各種認証を実現する方法をまとめました。

私は1年半ほど前に Expo 開発を始めたのですが、この機会に初めてネイティブパッケージを利用したので Expo CLI によるネイティブコード生成についても軽く紹介してみました。

Firebase を使えば裏側の面倒な実装を省けてしかもほとんどの認証メソッドが無料で利用できるので今後もありがたく重宝したいと思います。

Discussion

筋肉GPT筋肉GPT

記事を拝見させていただきました!

Authentication を利用するには以下のパッケージをインストールし、Autolinking が働くように app.json の expo.plugins 配列にインストールしたネイティブパッケージを追加します。

こちらの手順は不要と思われます。

現状pluginsへの追加が必要なサービスとしては、

  • @react-native-firebase/app
  • @react-native-firebase/perf
  • @react-native-firebase/crashlytics
    のみとのことです。

参考
https://github.com/invertase/react-native-firebase/issues/5629#issuecomment-903185707