13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Expo AppleAuthentication+Firebaseで「Sign in with Apple」を導入

Last updated at Posted at 2020-03-21

はじめに

「Sign in with Apple」をFirebaseのAuthenticationと連携させてExpoアプリに導入してみます。
すでにyosukesuzukiさんの下記の記事があるので恐縮ですが、こちらを参考にしたうえで、この記事では補足的な事柄を含めて簡単にまとめます。

expo+firebaseでSign in with Appleを実装する
https://qiita.com/yosukesuzuki/items/0a6590fc67ad587a693a

導入の準備

Apple DeveloperでAppのIdentifierに対して設定をするわけなので、まだ一度もExpoアプリをビルドしていない場合は事前にiOS向けのビルドをしておく必要があります。
まずはapp.jsonの編集を。

app.json
{
  "expo": {
    "ios": {
      "bundleIdentifier": "com.xxx.xxxxxxxxxxxx",
      "usesAppleSignIn": true
    }
  }
}

expo.ios.bundleIdentifierにバンドル名を設定し、expo.ios.usesAppleSignIn: trueを追加します。

iOSのビルドをしていない場合はこの後ビルドすることで、Expoにお任せでIdentifierを作ってもらいます。

$ expo build:ios

ビルドが完了したら、Apple DeveloperページでIdentifiersの中から該当するものを選択し、「Sign In with Apple」のCapabilityを有効にします。
Certificates__Identifiers___Profiles_-_Apple_Developer.png
ここでオプションとして既存のAppIDとグループ化することができるようですが、普通に「Enable as a primary App ID」を選択しておきます。

続いてFirebaseプロジェクト側の設定を行います。

「Authentication」の「Sign-in method」タブで「Apple」を有効にし、サービスIDにアプリのbundleIdentifierを入力します。プロジェクトにiOSアプリとして登録済みものがある場合はそのアプリのバンドルIDに対してデフォルトで許可が通りますが、ExpoアプリはFirebaseではWebアプリという扱いなので(iOSアプリとして登録していることはないと思います)、ここで入力が必要です。
Test_Project_–_Authentication_–_Firebase_console.png

参考記事にもありますが、Expo Clientで確認する際にはバンドルIDはhost.exp.Exponentとなるので、これをバンドルIDとするiOSアプリを追加しておくことで開発時にも認証が通るようになります。
Test_Project_–_Settings_–_Firebase_console.png

これで事前の準備は完了です。

Sign in with Appleボタンの実装

簡単にApp.jsにまとめて、ボタンだけ設置してみます。
expo-apple-authenticationexpo-cryptofirebaseをインストールしておいてください。

App.js
import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import * as AppleAuthentication from 'expo-apple-authentication';
import * as Crypto from 'expo-crypto';
import firebase from 'firebase';

// この時点でfirebase.initializeAppはしている想定

/**
 * ランダム文字列を生成
 * @param length
 * @returns {string}
 */
function nonceGen(length) {
  let result = '';
  let characters =
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let charactersLength = characters.length;
  for (let i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }
  return result;
}

export default function App() {
  const [appleCredentialState, setAppleCredentialState] = useState(null);
  const [isAvailableAppleAuth, setIsAvailableAppleAuth] = useState(null);

  useEffect(() => {
    AppleAuthentication.isAvailableAsync().then(setIsAvailableAppleAuth); // 使用可能かどうか判定してstateにセット
    firebase.auth().onAuthStateChanged((currentUser) => {
      // Firebase Authenticationの認証状況が呼ばれる
      console.log('currentUser', currentUser);
      if (currentUser) {
        const appleProviderData = firebase.auth().currentUser.providerData.find(data => (data.providerId === 'apple.com'));
        if (appleProviderData) {
          AppleAuthentication.getCredentialStateAsync(appleProviderData.uid).then((stateNum) => {
            const stateKey = Object.keys(AppleAuthentication.AppleAuthenticationCredentialState)
              .find((stateKey) => (AppleAuthentication.AppleAuthenticationCredentialState[stateKey] === stateNum));
            console.log('Credential state: ', stateKey || '');
            setAppleCredentialState(stateKey); // 認証状況を表示
            if (stateNum === AppleAuthentication.AppleAuthenticationCredentialState.REVOKED) { // AppleIDの使用を停止した場合
              firebase.auth().signOut().then(() => { // サインアウト
                setAppleCredentialState(null);
              });
            }
          });
        }
      }
    });
  }, []);

  return (
    <View style={styles.container}>
      {appleCredentialState && <Text>Apple Credential state: {appleCredentialState}</Text>}
      {isAvailableAppleAuth === false && <Text>この端末ではApple Authenticationは使用できません</Text>}
      {isAvailableAppleAuth && (
        <AppleAuthentication.AppleAuthenticationButton
          buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
          buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.WHITE_OUTLINE}
          cornerRadius={5}
          style={{ width: 200, height: 64 }}
          onPress={async () => {
            try {
              const nonce = nonceGen(32); // ランダム文字列(ノンス)を生成
              const digestedNonce = await Crypto.digestStringAsync(
                Crypto.CryptoDigestAlgorithm.SHA256,
                nonce
              ); // SHA256でノンスをハッシュ化
              const result = await AppleAuthentication.signInAsync({
                requestedScopes: [ // ユーザー情報のスコープを設定(名前とメールアドレスのみ可)
                  AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
                  AppleAuthentication.AppleAuthenticationScope.EMAIL
                ],
                nonce: digestedNonce // Apple側にはハッシュ化したノンスを渡す
              });
              console.log('Apple Sign In result: ', result);
              let provider = new firebase.auth.OAuthProvider("apple.com");
              let credential = provider.credential({
                idToken: result.identityToken,
                rawNonce: nonce // Firebase側には元のノンスを渡して検証させる
              });
              const firebaseResult = await firebase.auth().signInWithCredential(credential);
              console.log('Firebase Auth result: ', firebaseResult);
            } catch (e) {
              console.error(e);
            }
          }}
        />
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

機能自体がシンプルなので難しいことはないですが、ノンスの部分だけ理解が必要です。
AppleAuthentication.signInAsync時にリプレイ攻撃を防ぐためのオプションとしてnonceプロパティを渡すことができます。この時、ランダム文字列をSHA256でハッシュ化したものをApple側に送信し、Firebaseには元の文字列を送信することで、両者の照合によりセキュリティが向上するという仕組みです。

認証結果の内容

AppleAuthentication.signInAsyncによるPromiseはAppleAuthentication.AppleAuthenticationCredentialオブジェクトを返します。
このオブジェクトは下記のプロパティを含んでいます。

  • user
    ユーザー固有の値。以後AppleAuthentication.getCredentialStateAsyncで認証状態をチェックする際に使用する。
  • fullName
    ユーザーの名前に関する情報。requestedScopesオプションでFULL_NAMEを設定している場合にそのアプリで最初に認証したときのみ値が入り、この内容を含む。
  • email
    ユーザーのメールアドレス。ユーザーが認証時に非公開にすることができ、その場合は[ランダム文字列]@privaterelay.appleid.comという形式のメールアドレスが生成され、ここに送信するとユーザーの本来のメールアドレスに転送される。これも最初に認証したときのみ値が入る。
  • state
    signInAsyncのオプションとして設定できる任意の文字列。独自のリプレイ攻撃対策などに使用。
  • identityToken
    ユーザー&アプリごとに生成されるトークン。FirebaseのOAuthProviderにはこれを渡す。
  • authorizationCode
    セッションごとに生成されるトークン。

画面で確認

認証が完了するとこのように表示されます。
PNGイメージ 44.png
ボタンのデザインはAppleAuthentication.AppleAuthenticationButtonコンポーネントのbuttonStyleプロパティでWHITE(上画像)、BLACK(黒背景)、WHITE_OUTLINE(白背景に黒枠)から選択することもできます。このコンポーネント自体は単なるビューなので、デザインガイドに従えば独自実装で他のボタンと合わせたりしても問題ないはずです。

[2020/3/30 追記] スタンドアロンアプリで確認(TestFlight)

スタンドアロンアプリとしてビルドしてTestFlightを使って確認する過程で気づきましたが、Expo Clientでの確認するだけなら上記のApple DeveloperでのIdentifierに関する設定は必要ありません。
スタンドアロンアプリとしてビルドする際には上述した設定をした上で、下記のようにプロビジョニングプロファイルや証明書を新しくするオプションを付けてビルドしてください。

$ expo build:ios --clear-provisioning-profile --revoke-credentials

このオプションをつけてビルドするのは一回のみでOKです。
PNGイメージ_46_.png
このようにスタンドアロンアプリとしても問題なくモーダルが表示され、ログインできました。

下記のようなエラーが出る場合は、プロビジョニングプロファイルが古いものになっていたり、app.jsonのexpo.ios.usesAppleSignInが設定されていなかったりするようです。

com.apple.authenticationservices.authorizationerror error 1000

Androidで見ると

AndroidではAppleAuthenticationモジュール自体が対応していないので、AppleAuthentication.isAvailableAsyncの判定によりボタンを表示しないようにしました。

iOS 12以下でも同様になります。
ちなみにiOSのレビューガイドラインに準拠するという意味ではiOSのみ対応していれば問題ないはずですが、下記のドキュメントがあるので、AndroidでもネイティブではFirebaseと組み合わせて実装できるようです。

Android で Apple を使用して認証する
https://firebase.google.com/docs/auth/android/apple

認証をリセットしたい場合

認証時の名前やメールアドレスなどの情報は同一アプリで最初にサインインした時にしか取得できないため、リセットしたい場合もあると思います。
その場合は、iOSの「設定」→「パスワードとアカウント」→「iCloud」→「パスワードとセキュリティ」→「Apple IDを使用中のApp」(使用中のアプリがない場合はメニュー自体表示されない)→アプリを選択→「Apple IDの使用を停止する」でアプリに対するApple IDの紐付けをリセットすることができます。

その他

ユーザーがメールアドレスを非公開にした場合のために、Firebaseからprivaterelay.appleid.comドメインを通してメールを転送できるようにプライベートEメールリレーサービスの設定が必要です。
しかし、AppleAuthenticationによって作成されたfirebase.auth.Userでは最初からemailVerifiedtrueになっているため、お手頃にメールアドレス確認メールを送ってもらって検証したりができませんでした。

[2020/3/30 追記] プライベートEメールリレーサービスを確認

プライベートEメールリレーサービスによるメール転送についてもFirebaseを経由せずとも通常のメールアドレスで確認できました。
メールアドレスを非公開にした場合に生成されたメールアドレスにGmailから送ってみると、転送先に設定したメールアドレスに届きます。
gmail.png

メールの送信元はApple Developerユーザーごとにホワイトリストで設定する必要があります。(最大で個人32個・組織100個)
Certificates__Identifiers___Profiles_-_Apple_Developer.png
https://developer.apple.com/account/resources/services/configure
Certificates, Identifiers & Profiles > More > Configure

※ちなみに、Expo Clientでの検証時には当然ながらExpo Clientのプロビジョニングファイルが使用されるので、開発者のアカウントでこれをいくら設定しても反映されません。検証にはスタンドアロンアプリとしてビルドする必要があります。

13
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?