[iOS] Firebaseメールリンク認証を実装してみた

#はじめに
[Firebase Authentication](https://firebase.google.com/docs/auth?hl=ja)を使用すれば、アプリでの様々なユーザー認証 (例えば、パスワード、電話番号、Googleアカウントなど)を比較的容易に実装することができます。
パッと思いつく認証方法は`メール/パスワード認証`かなと思います。
しかし、このセキュリティはユーザー視点に立つとパスワードを覚えるのが面倒であるといった、アプリを使用する障壁となり得ます。
本記事では、パスワード不要の`メールリンク認証 (iOS)`についての実装手順をまとめてみました。

#環境
[Firebase SDK (iOS)] Version 7.7.0
[Xcode] Version 12.4
[Swift] Version 5.3.2
[iOS] 14.4
[MacOS] 10.15.7

#メールリンク認証とは
[メールリンク認証](https://firebase.google.com/docs/auth/ios/email-link-auth?hl=ja)とは、ログイン用のリンクを含むメールをユーザーに送信し、そのリンクからログインしてもらう認証方法です。
<img width=300" alt="1.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/0aab85a2-1eae-12f3-2d7a-ba217b5c6bf0.png">
メールリンク認証にはいくつもメリットがあります。

- 登録とログインが簡単
- ユーザーがメールアドレスの正当な所有者であることを確認できる
- パスワード不要 (覚える必要なし)で、安全にログインできる
などです。

#本記事で作成するサンプル
今回は、ユーザーのメールアドレスを受け取り、送信されたメールのリンクからアプリを起動してログインするというシンプルなアプリを想定します。
<img width="500" alt="2.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/66b03f7d-7704-daa7-d371-58e2e0ac882e.png">

#実装手順
[公式ドキュメント](https://firebase.google.com/docs/auth/ios/email-link-auth?hl=ja)に従い、以下の流れで実装していきます。
1. FirebaseをiOSプロジェクトに追加 (本記事では省略します)
2. Firebaseプロジェクトでメールリンクログインを有効にする
3. Dynamic Linksを構成する
4. ユーザーのメールアドレスに認証リンクを送信して、アプリでユーザーのログインを完了する

###1. FirebaseをiOSプロジェクトに追加 (省略)
本記事では省略します。(公式ドキュメント 「[FirebaseをiOSプロジェクトに追加する](https://firebase.google.com/docs/ios/setup?hl=ja)」を参照してください)
今回は、ドキュメントでも推奨されていますがCocoaPodsでFirebaseをインポートします。`Podfile`ファイルには以下の`Pod`を含めます。

```:Podfile
  pod 'Firebase/Analytics'
  pod 'Firebase/Auth'
  pod 'Firebase/DynamicLinks'
```

(21/09/04 追記)
Xcode12.5以降で、正式にSwift PackageManagerでFirebaseをインストールできるようになりました。
[Swift PackageManagerを使用してFirebaseをインストールする](https://firebase.google.com/docs/ios/swift-package-manager)

###2. Firebaseプロジェクトでメールリンクログインを有効にする
[Firebase コンソール](https://console.firebase.google.com/u/0/?hl=ja)で[`Authentication`]セクションを開き、[`始める`]を選択します。
<img width="350" alt="3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/5301868c-01fa-c8c2-67db-aeb7237d69e3.png">&emsp;&emsp;<img width="350" alt="4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/4572fb91-eb75-e1ee-8a87-51e3080f7ba8.png">
[`Sign-in method`]タブで[`メール/パスワード`]を編集します。
<img width="700" alt="5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/0182689c-0dfe-6138-5fa2-3784d8459f39.png">
[`メール/パスワード`]と[`メールリンク`]の両方を有効にして保存します。
<img width="700" alt="6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/1c546a2e-5e1d-23dd-d244-5713dd5cfecc.png">
これでメールリンクが有効となりました。

###3. Firebase Dynamic Linksを構成する

####3-1. Firebase Dynamic Linksとは
>Firebase Dynamic Linksとは、アプリのインストールの有無に関わらす、複数のプラットフォームで機能するディープリンクです。
Dynamic Links により、ユーザーがリンクを開いたプラットフォームで得られる最高のエクスペリエンスを提供できるようになります。
iOS または Android でダイナミックリンクを開くと、ネイティブアプリのリンク先のコンテンツに直接移動します。デスクトップ ブラウザでダイナミック リンクを開くと、ウェブサイト上の同じコンテンツに移動します。
さらに、ダイナミック リンクはアプリのインストールが必要な場面でも利用できます。
たとえば、ダイナミック リンクを開いたユーザーの iOS デバイスまたは Android デバイスにアプリがインストールされていない場合にインストールを促し、インストール完了後にアプリを起動してリンクを開くといったこともできます。

[Firebase Dynamic Links](https://firebase.google.com/docs/dynamic-links?hl=ja)より引用


####3-2. App Store IDとチームIDを取得する
ダイナミックリンクを作成する際に、「`App Store ID`」と「`チームID`」が必要となるので先に取得しておきます。
すでに取得済みの方はスキップしてください。

#####チームIDの取得
[Apple Developer](https://developer.apple.com/)アカウントの[`Certifications, IDs & Profiles`]セクションの[`Identifiers`]セクションで確認できます。**「`App ID Prefix`」**がチームIDです。
アプリを未登録の方は、以下の手順で登録できます。
[`Certifications, IDs & Profiles`]セクションをクリックします。
<img width="700" alt="7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/ec1a1a27-941a-6042-3f7a-62148f2f8251.png">
[`Identifiers`]セクションの`＋`ボタンをクリックします。
<img width="700" alt="8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/b7596f12-941d-d4b0-a5c4-53304455aaa1.png">
「`App IDs`」を選択して「`Continue`」で次に進みます。
<img width="700" alt="9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/ea172f80-3680-e506-17b2-7aca863228f8.png">
「`Select a type`」で「`App`」を選択して「`Continue`」で次に進みます。
<img width="700" alt="10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/06182719-31b5-583e-75f1-79037f6829a3.png">
「`Description`」にはアプリの簡単な説明を入力します。アプリ名でも良さそうです。
「`Bundle ID`」にはXcodeプロジェクトのBundle Identifierを正確に入力してください。
「`Capabilities`」では、アプリリリース時にはアプリの機能を漏れなく選択する必要がありますが、今回は最低限必要な「`Associated Domains`」を選択して次に進みます。
<img width="700" alt="11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/6db04f72-e5e1-ed27-b8b1-2623455fe6e1.png">
「`App ID Prefix`」がチームIDです。これでチームIDの確認を完了しました。
<img width="700" alt="12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/5bdb2580-a9e6-3cd4-33ff-c1e4299df0a2.png">

#####App Store IDの取得
[App Store Connect](https://appstoreconnect.apple.com/)の「`マイApp`」から自分の登録しているアプリをクリックして、[`App情報`]セクションにある**「`Apple ID`」**がApp Store IDです。
アプリを未登録の方は以下の手順で登録できます。
まず「`マイApp`」をクリックします。
<img width="600" alt="13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/36281a36-81e8-1483-fe5d-d54cd893ea92.png">
作成しているアプリを「`新規App`」から追加します。
<img width="600" alt="14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/e0945545-218b-39bd-14ce-1375d004b648.png">
「`アプリの名前`」、「`使用する主な言語`」、「`Bundle ID`」、「`SKU`」を設定します。
SKUはアプリを区別できる名前であれば何でも良いと思います。
今回はアプリ名にしました。
<img width="600" alt="15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/55145c4b-7f68-762c-501c-1479aafe84ec.png">
作成を終えたら、[`App情報`]セクションをクリックして**「`Apple ID`」**を確認します。これがApp Store IDに当たるので、確認完了です。
<img width="700" alt="16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/7022ea60-8f3b-e73a-de90-736c85f441d2.png">

####3-3. Dynamic Linksを作成する
[公式ドキュメント](https://firebase.google.com/docs/dynamic-links/create-links?hl=ja)によると、Dynamic Linksの作成方法は4つありますが、今回は[Firebase コンソール](https://console.firebase.google.com/u/0/?hl=ja)で作成します。
[`Dynamic Links`]セクションを開き、「`始める`」をクリックします。
<img width="350" alt="17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/8efb1988-1eac-f45f-8977-8a083e26569f.png">&emsp;&emsp;<img width="350" alt="18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/e82058f1-07ee-ba3d-d0b1-2cdf50601583.png">
Dynamic LinksのURLドメインを作成します。
ドメインは特に指定しなければGoogle提供の`.page.link`が与えられるので、それを使用します。
サブドメインはアプリ名などの分かりやすいものが良いです。(例えば、今回ならURLを`openapp.page.link`にするとか)
<img width="450" alt="19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/8106b162-838d-6e32-a1a8-4cee22fd2270.png">&emsp;<img width="450" alt="20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/dc16d1a0-f831-b7c7-65ac-bd7d48a718c8.png">
次に、「`新しいダイナミック リンク`」ボタンからダイナミックリンクを作成します。
<img width="700" alt="21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/cb980903-12cd-b541-2587-2d43637eec03.png">
短縮URLのリンク設定では、デフォルト値でも問題ありません。今回は分かりやすいように「open」としています。
<img width="700" alt="22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/e14b6af3-ef52-cd16-4a84-6d26d8f9d014.png">
次に、ダイナミックリンクの設定です。
ディープリンクURLによって特定のコンテンツに直接遷移することが可能となります。ディープリンクは、オリジナルのURLで良いようなので遷移先が分かるように設定します。
今回は、アプリ自身に移動するので、[`Authentication`]セクションの`承認済みドメインURL`をコピーして使用しています。
「`ダイナミックリンク名`」は処理が分かりやすいように、「Open App」のようにします。
<img width="700" alt="23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/149c198c-59dd-a428-b923-7d075e89ad7b.png">
<img width="700" alt="24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/81881745-9310-84f1-3cf7-412685a76fe7.png">
次に、iOSデバイスでリンクを開いたときの動作を設定します。
今回は「`ディープリンクでiOSアプリを開く`」を選択して、アプリを指定します。
このとき、Firebase コンソールの[`プロジェクト設定`]セクションにおいて、`App Store ID`と`チームID`を設定している必要があります。
設定していない場合、アプリを指定しようとすると、「`App Store IDとチームIDをここに追加してください`」という警告が表示されるので、未設定の方は入力してください。
<img width="700" alt="25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/1e179d2d-d42c-4aa6-e671-05658e58b956.png">
<img width="700" alt="26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/d6f0b531-dc37-016b-90d8-9ee8e0fb2f02.png">
次に、アプリがインストールされていないときの動作を設定します。
今回はApp Storeに移動します。
<img width="600" alt="28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/53d5cabf-ba53-195b-11eb-a95aba4bd981.png">
次にAndroidでの動作設定や、その他詳細オプションを設定できますが、今回は特に何も設定をせずに進めています。
<img width="450" alt="29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/414e73eb-4b4b-490c-d315-bee4a6eb8a59.png">&emsp;&emsp;<img width="450" alt="30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/f1d2322f-a880-ac24-85e1-0c11e99022b2.png">
以上で、ダイナミックリンクの作成は完了です。
リンクURLをコピーして、[`Authentication`]セクションの[`Sign-in method`]タブ内にある「`承認済みドメイン`」に、ダイナミックリンクURLのドメインを追加します。
<img width="600" alt="31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/9da9235e-6269-cd0c-e8ba-24569f532573.png">
<img width="600" alt="32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/1cac523e-adbf-f0c4-5d40-ddd4ede0e77c.png">
下記のように、追加したURLドメインが一覧に表示されていればOKです。
<img width="700" alt="33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/a20cbe87-1992-fd9f-0b27-c4d4f8da7785.png">
以上で、コンソールにおけるDyanmic Linksの構成は完了です。
次にXcodeプロジェクトの設定を行います。

####3-4. Xcodeプロジェクトの設定
「`TARGETS`」の[`Signing & Capabilities`]タブにある「`+Capability`」をクリックして「`Associated Domains`」を追加します。
<img width="800" alt="34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/545bda3b-2553-c0a4-664f-58735ee49717.png">
「`Domains`」に「`applinks:example.page.link`」を入力してください。「`example.page.link`」はFirebaseコンソールで作成したダイナミックリンクURLのドメインです。
<img width="800" alt="35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/c92f8aa9-7878-22b7-1924-9b2d405fecb7.png">

以上でXcodeプロジェクトでの設定も完了です。

###4. メールアドレスに認証リンクを送信して、アプリでユーザーのログインを完了する
####4-1. ActionCodeSettingsオブジェクトを作成する
メールリンクの作成方法をFirebaseに伝える`ActionCodeSettings`オブジェクトを作成します。

```Swift:SendEmailViewController

    //・・・
    import Firebase

    @IBOutlet private weak var emailTextField: UITextField! //ユーザーからメールアドレスを受け取るUIを準備しておきます

    //・・・

    let email = emailTextField.text ?? "" //ユーザーのメールアドレス

    let actionCodeSettings = ActionCodeSettings() //メールリンクの作成方法をFirebaseに伝えるオブジェクト
    actionCodeSettings.handleCodeInApp = true //ログインをアプリ内で完結させる必要があります
    actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!) //iOSデバイス内でログインリンクを開くアプリのBundle ID
    //リンクURL
    var components = URLComponents()
    components.scheme = "https"
    components.host = "maillinkdemo.page.link" //Firebaseコンソールで作成したダイナミックリンクURLドメイン
    let queryItemEmailName = "email" //URLにemail情報(パラメータ)を追加する
    let emailTypeQueryItem = URLQueryItem(name: queryItemEmailName, value: email)
    components.queryItems = [emailTypeQueryItem]
    guard let linkParameter = components.url else { return }
    actionCodeSettings.url = linkParameter  
    //actionCodeSettingsの作成完了

    //・・・

```
####4-2. メールアドレスに認証リンクを送信する
ユーザーのメールアドレス`email`と作成した`actionCodeSettings`を使用して、ユーザーのメールアドレスに認証リンクを送信します。
ログインメールの送信時に、メールアドレスをローカルに保存しておきます。
Firebase Authでは、認証フロー完了時にユーザーのメールアドレスとログインリンクの送信アドレスが一致するかどうかを確認するため、ローカルに保存しておくことで、ユーザーに再度メールアドレスを入力してもらわなくても認証フローを完了できます。

[メールリンクログインを完了する | iOS でメールリンクを使用して Firebase 認証を行う](https://firebase.google.com/docs/auth/ios/email-link-auth?hl=ja#complete_sign_in_with_the_email_link)

```Swift:SendEmailViewController
    //・・・

    //ユーザーのメールアドレスに認証リンクを送信
    Auth.auth().sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings) { (err) in
        if let err = err {
            print("送信失敗")
            return
        }
        print("送信完了")

        //後で認証に使用するのでローカルにメールアドレスを保存しておく
        UserDefaults.standard.set(email, forKey: "email")

        //・・・
        //アラートを表示するなど、ユーザーにメールの確認を促す処理
           
    }
```
####4-3. リンクを確認して、ログインを完了する
メールを受信したら、それがメールリンク認証用であることを確認してログインを完了します。
ユーザーがメールに含まれたリンクをタップすると、次のようなページに遷移します。
<img width="300" alt="37.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/3eaa8077-fc7a-59d4-0ab8-1e550d1016b4.png">
「`OPEN`」ボタンをタップするとアプリが開きます。 (アプリがインストールされていない場合は、リリースしていればAppStoreを開きます)
アプリを開くと、`SceneDelegate`クラスの`scene(_:continue:)`メソッドが呼ばれるので、この中にログイン完了の処理を書きます。今回は、サインインしたら画面を切り替えるというシンプルな処理にしています。
SceneDelegateはiOS13から導入されたようで、[公式ドキュメント](https://firebase.google.com/docs/dynamic-links/ios/receive?hl=ja)ではAppDelegateクラスでの処理を紹介しているので、SceneDelegateクラスが不要で削除されている方はそちらを参考にしてください。
SceneDelegateクラスでの実装は、下記の記事が分かりやすく参考にさせていただきました。
参考: [[iOS13] SceneDelegateでDynamicLinksからのURLをハンドリングする](https://qiita.com/water_bath/items/83814de07f561d57a74d)


```Swift:SceneDelegate
import Firebase

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    //・・・
    
    var window: UIWindow?

    //ユーザーのアクティビティオブジェクトの情報を受信したときに呼ばれる
    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {

        //userActivityプロパティからリンクURLを取得
        guard let url = userActivity.webpageURL else { return }
        let link = url.absoluteString

        if Auth.auth().isSignIn(withEmailLink: link) {
            //ローカルに保存していたメールアドレスを取得
            guard let email = UserDefaults.standard.value(forKey: "email") as? String else {
                print("メールアドレスが存在しません")
                return
            }
            //ログイン処理
            Auth.auth().signIn(withEmail: email, link: link) { (auth, err) in
                if let err = err {
                    print("ログイン失敗")
                    return
                }
                print("ログイン成功")
                
                //ログイン成功時の処理 (例えば、今回は画面切り替えなどの処理)
                guard let scene = (scene as? UIWindowScene) else { return }
                let window = UIWindow(windowScene: scene)
                let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
                let didSignInVC = storyboard.instantiateViewController(withIdentifier: "DidSignInViewController")
                window?.rootViewController = didSignInVC
                self.window = window
                window.makeKeyAndVisible()
            }
        }
    }

    //・・・
}
```
####4-4. [補足] サインアウトする
サインアウト処理は以下のように簡単に書けます。

```Swift:DidSignViewController

import UIKit
import Firebase

class DidSignInViewController: UIViewController {
    
    //・・・

    //サインアウトボタンを準備しておく
    @IBAction func signButtonTapped(_ sender: UIButton) {
        //ログイン状態ならば、サインアウト処理を行う
        if let user = Auth.auth().currentUser {            
            do {
                try Auth.auth().signOut()
                print("サインアウト完了")
                //画面切り替えなど、サインアウト後の処理
                //・・・
                
            } catch let signOutError as NSError {
                print("失敗")
                return
            }
        }
    }
}
```

signOut()メソッドは完了のタイミングが分からないので、少し注意が必要そうです。

#少しハマったこと
これまでに何回かDynamicLinksを導入してみましたが、毎回実装した直後にはメールのリンクから**「Open link in app?」**のページに上手く遷移されない現象が起きました。
これは断言できるわけではないのですが、DynamicLinksを設定してからFirebaseのシステムに反映されるまでに時間がかかっているのかもしれません。
これといった対策をしないまま、次の日あたりに上手く機能することがあり、確実な原因が分からないままになっています。
同じようなことが起きた方、理由がわかる詳しい方がいればぜひコメントで共有していただけると幸いです！(笑)

#最後に
Firebaseを利用したメールリンク認証の実装手順をまとめてみました。
主観ですが、パスワードを用いたログインに比べてユーザー視点でのメリットが多いと感じました。
今回実装してみて、ダイナミックリンクはFirebaseコンソールで簡単に作成できるものの、細かい設定などに少し手こずりました。
**「参考文献」**に記載しましたが、iOSでのFirebase Dynamic Linksの実装について分かりやすい記事をいくつも参考にさせていただきました。
しかし、iOSアップデートなどにより、そのまま手順通りにやっても上手くいかない箇所もあるため、合わせてこの記事がほんの少しでも役に立てば幸いです。
初めて記事を投稿したため、分かりづらい点が多いと思いますが、今後もアップデートしていければと思います。

#参考文献
この記事は以下の情報を参考にしました。

- [iOS でメールリンクを使用して Firebase 認証を行う](https://firebase.google.com/docs/auth/ios/email-link-auth?hl=ja)
- [iOS でダイナミック リンクを受信する](https://firebase.google.com/docs/dynamic-links/ios/receive?hl=ja)
- [[Firebase]メールリンク認証の実装[iOS]](https://qiita.com/shiba1014/items/444c21ef009c2b2514af)
- [「アプリで開く」を実現する、Firebase Dynamic Linksの実装と運用Tips](https://logmi.jp/tech/articles/320720)
- [[iOS13] SceneDelegateでDynamicLinksからのURLをハンドリングする](https://qiita.com/water_bath/items/83814de07f561d57a74d)
- [Handling Firebase Dynamic Links in iOS 13 with Scene Delegate](https://prograils.com/firebase-dynamic-links-in-ios13-with-scene-delegate)