Firebase Authentication を調べてみた

# 概要

FirebaseのAuthenticationに関して調べてみました。
UIに関しては基本[FirebaseUI][1]のAndroid版を利用しています。
iOSやWEBに関しても提供されているので、他のプラットフォームでも利用できそうです。

# 特徴

## 認証の種類

- Password
  - メールアドレスとパスワードでの認証
- Provider
  - 下記のプロバイダでのユーザー認証
    - Google
    - Facebook
    - Twitter
    - GitHub
  - 各アカウントでのDeveloper登録等は必要
- Phone Number
  - SMSを利用した認証
- [Custom][13]
  - `Firebase Console`で鍵を生成し、それを使用した認証方式
- [Anonymous][12]
  - 一時的な匿名の認証
  - 匿名アカウントから通常アカウントへのアップグレードは可能

## アカウントの適用範囲について

- 作成したアカウントはプロジェクト単位で共有される
  - 各プラットフォームで共通利用する場合は、同じプロジェクトにアプリを登録する必要がある

## プロフィールの拡張について

- [ユーザー プロパティ][9]
  - 固定の基本プロパティ以外を設定することはできない
  - 拡張したい場合は`Realtime Database`等を利用する必要がある

## ユーザー管理について

- `Firebase Console`では以下の機能が利用できる
  - ユーザー追加
  - パスワードリセットメールの送信
  - アカウントの停止
  - アカウントの削除
- 上記以外の操作をする場合は`Firebase Admin SDK`の[Admin Auth API][10]を利用する必要がある
  - もちろん上記内容も実行できる
  - `Node.js`、`Java`、`Python`で実行できる

## Cloud Functions との連携

- ユーザーの作成および削除をトリガーにできる
  - 登録、削除の完了メールを送るとかができる
  - 削除時は`Realtime Database`に保存した個人情報を削除するなどもできそう
  - [Cloud Functions で Firebase Authentication を拡張する][11]

## Authentication Flow Chart

- [FirebaseUI][1]に[Flow Chart][14]が記載されています。参考までに。

# 機能

## サインイン / サインアップ

```java
private static final int REQUEST_CODE_AUTH = 100;

private void signIn() {
    startActivityForResult(
            AuthUI.getInstance()
                    .createSignInIntentBuilder()
                    .setAvailableProviders(
                            Arrays.asList(
                                    new AuthUI.IdpConfig.Builder(AuthUI.GOOGLE_PROVIDER).build(),
                                    new AuthUI.IdpConfig.Builder(AuthUI.FACEBOOK_PROVIDER).build(),
                                    new AuthUI.IdpConfig.Builder(AuthUI.TWITTER_PROVIDER).build(),
                                    new AuthUI.IdpConfig.Builder(AuthUI.EMAIL_PROVIDER).build()))
                    .setPrivacyPolicyUrl(getString(R.string.url_privacy_policy))
                    .setAllowNewEmailAccounts(true)
                    .build(),
            REQUEST_CODE_AUTH);
}
```

- パスワード認証
  - メールアドレス入力後、既存ユーザーか判定し既存ユーザーの場合パスワード入力、新規ユーザーは登録画面に遷移する

- `setAvailableProviders()`
  - `IdpConfig`のリストを渡すことで対応するAuth機能が使える
  - 上記例だと以下ようになる

![sign_in_select_provider.png](https://qiita-image-store.s3.amazonaws.com/0/189865/c48bfd9f-a803-03e0-b5b4-a29612d9665f.png)


- `setPrivacyPolicyUrl()`
  - 設定することで、以下の画像のようにパスワード認証の新規登録画面でプライバシーポリシーのリンクが表示される

![sign_up_password.png](https://qiita-image-store.s3.amazonaws.com/0/189865/3ea978e0-e551-c889-d59d-b0da2ab1f2fd.png)


- `setAllowNewEmailAccounts()`
  - 新規メールアドレスの入力を許可するか否か

## サインアウト

```java
private void signOut() {
    AuthUI.getInstance()
            .signOut(this)
            .addOnCompleteListener(new OnCompleteListener<Void>() {
                public void onComplete(@NonNull Task<Void> task) {
                    if (task.isSuccessful()) {
                        Toast.makeText(AuthenticationActivity.this,
                                R.string.success_sign_out, Toast.LENGTH_SHORT).show();
                    }
                }
            });
}
```

- サインインされている状態で実行すれば問題なし
- 他のAPIも同様だが、`OnCompleteListener`や`OnFailureListener`を登録することで結果をハンドリングできる

## アカウント削除

```java
private void deleteAccount() {
    FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
    if (user == null) {
        return;
    }

    user.delete()
            .addOnCompleteListener(new OnCompleteListener<Void>() {
                @Override
                public void onComplete(@NonNull Task<Void> task) {
                    if (task.isSuccessful()) {
                        Toast.makeText(AuthenticationActivity.this,
                                R.string.success_delete_account, Toast.LENGTH_SHORT).show();
                    } else {
                        Exception ex = task.getException();
                        if (ex instanceof FirebaseException) {
                            Toast.makeText(AuthenticationActivity.this,
                                    R.string.do_re_authentication, Toast.LENGTH_SHORT).show();
                            reAuthSignIn(REQUEST_CODE_DELETE_RE_AUTH);
                        } else {
                            Toast.makeText(AuthenticationActivity.this,
                                    R.string.fail_delete_account, Toast.LENGTH_SHORT).show();
                        }
                    }
                }
            });
}
```

- 上記は`FirebaseUser`のインスタンスから削除を実行する実装方法
  - `FirebaseUI`の`AuthUI.getInstance().delete(this);`でも削除可能
- 注意点
  - アカウント削除のようなセキュリティ上重要な操作を行う場合は、ユーザーが直近でサインインしている必要がある
  - サインインから時間が立っている場合は再認証の必要がある（`reAuthSignIn()`）

## 再認証

```java
private void reAuthSignIn() {
    FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
    if (user == null) {
        return;
    }

    List<AuthUI.IdpConfig> idpConfigs = new ArrayList<>();
    for (UserInfo info : user.getProviderData()) {
        String provider = info.getProviderId();
        if (provider.equals(FirebaseAuthProvider.PROVIDER_ID)) {
            continue;
        }
        idpConfigs.add(new AuthUI.IdpConfig.Builder(provider).build());
    }

    startActivityForResult(
            AuthUI.getInstance()
                    .createSignInIntentBuilder()
                    .setAvailableProviders(idpConfigs)
                    .setAllowNewEmailAccounts(false)
                    .build(),
            REQUEST_CODE_AUTH);
}
```

- `AuthUI`には再認証のUIは提供されていない
  - そのため、ユーザーが利用しているプロバイダ名からサインインページへ遷移するようにした
- [公式ドキュメント][2]には`FirebaseUser`の`reauthenticate()`を利用したサンプルもある

## ユーザープロフィール取得

- サンプルコードは公式ドキュメントの通りなので省略
  - [ユーザーのプロフィールを取得する][3]
  - [ユーザーのプロバイダ別のプロフィール情報を取得する][4]

- 取得できる情報は以下の通り
  - 表示ユーザー名
  - Eメールアドレス
  - Eメールでの確認ステータス
  - UID
  - プロフィール写真のURL

## ユーザープロフィール更新

- こちらもサンプルは公式ドキュメントの通りなので省略
- [ユーザー再認証のドキュメント][2]に記載があるが、再認証が必要なものは簡単に更新できないため、更新できるのは以下の項目のみとなる
  - 表示ユーザー名
  - プロフィール写真のURL

## 確認メール送信

- Eメールアドレスへ確認メールを送信することができる
  - その結果はプロフィールに反映される
  - サンプルは[公式ドキュメント][6]を参照

## パスワードリセットメール送信

- パスワード再設定用のメールを送信する
- 自前で実装する場合は以下の公式ドキュメントを参照
  - [パスワードの再設定メールを送信する][5]
- `FirebaseUI`ではOSS側で実装されているので問題ない

![sign_in_input_password.png](https://qiita-image-store.s3.amazonaws.com/0/189865/bbcf91d3-2148-3a89-25a8-7f7a0300a628.png) ![send_password_rest_mail.png](https://qiita-image-store.s3.amazonaws.com/0/189865/00280599-4989-1511-1f8d-02f7ee8f2734.png)


## メールアドレスとパスワードの設定

- サンプルは公式ドキュメントの通り
  - [ユーザーのメールアドレスを設定する][7]
  - [ユーザーのパスワードを設定する][8]
- アカウント削除同様、再認証が必要になる

# まとめ

- サンプルを見ての通りかなりお手軽に認証が組み込めます
- `Realtime Database`や`Cloud Functions`とも連携しやすいので、サーバーレスでアプリを作るならかなり有用と感じました


[1]:https://github.com/firebase/FirebaseUI
[2]:https://firebase.google.com/docs/auth/android/manage-users?hl=ja#re-authenticate_a_user
[3]:https://firebase.google.com/docs/auth/android/manage-users?hl=ja#get_a_users_profile
[4]:https://firebase.google.com/docs/auth/android/manage-users?hl=ja#get_a_users_provider-specific_profile_information
[5]:https://firebase.google.com/docs/auth/android/manage-users?hl=ja#send_a_password_reset_email
[6]:https://firebase.google.com/docs/auth/android/manage-users?hl=ja#send_a_user_a_verification_email
[7]:https://firebase.google.com/docs/auth/android/manage-users?hl=ja#set_a_users_email_address
[8]:https://firebase.google.com/docs/auth/android/manage-users?hl=ja#set_a_users_password
[9]:https://firebase.google.com/docs/auth/users?hl=ja#user_properties
[10]:https://firebase.google.com/docs/auth/admin/?hl=ja
[11]:https://firebase.google.com/docs/auth/extend-with-functions?hl=ja
[12]:https://firebase.google.com/docs/auth/android/anonymous-auth?hl=ja
[13]:https://firebase.google.com/docs/auth/android/custom-auth?hl=ja
[14]:https://github.com/firebase/FirebaseUI-Android/blob/master/auth/README.md#authentication-flow-chart