Flutter×FirebaseのGoogle認証(2021年4月版)

---
# :book: Flutterの記事を整理し本にしました :book:
https://zenn.dev/kazutxt/books/flutter_practice_introduction

* <font color="red"><b>本稿の記事を含む様々な記事を体系的に整理し本にまとめました</b></font>
* 今後はこちらを最新化するため、<font color="red"><b>最新情報はこちらをご確認くださ</b>い</font>
* 10万文字を超える超大作になっています(笑)

---

# はじめに
* FlutterのFirebaseパッケージのメソッドとかがコロコロ変わる印象があり、先人方のコピペで動かない事があったので、最新版で動くものを整理しました。
 
## まとめ
* [Flutter系の記事のまとめ](https://qiita.com/kazutxt/items/524acb1c134015682ccf)

# 概要
FirebaseのAuthenticationの基本については、[Firebaseの機能1-1:Authentication(メール)](https://zenn.dev/kazutxt/books/flutter_practice_introduction/viewer/firebase-authentication)をご参照ください。

Firebase Authentication(以下Authentication)には、メールとパスワード以外にも、様々なSNSとの連携が実現できるようになっています。

本チャプターでは、一番代表的なGoogleアカウントとの連携の実装を解説します。

この認証方法を使えば、メールアドレスを登録することなく、Googleアカウントさえ持っていれば、認証を通ることができます。

開発者側は、ユーザのUIDがメールでもGoogle認証でも同じように払い出されますので、認証後は統一したロジックで処理を行うことができます。


# 事前準備
## 共通の設定
まずは、FirebaseでGoogleAuthを有効化します。

![pic3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/a314b4f9-07db-0c48-0628-5ef69cdb8a8f.png)


Authenticationから、プロバイダーのGoogleを有効にします。
プロジェクトの公開名はデフォルトでも構いませんが、認証時にユーザに表示されます。

## Androidの設定
AndroidではSHA1のフィンガープリントが必要になります。

```sh:keygen.sh
% keytool -list -v-alias androiddebugkey -keystore ~/.android/debug.keystore
キーストアのパスワードを入力してください:  
別名: androiddebugkey
作成日: 2020/08/09
エントリ・タイプ: PrivateKeyEntry
証明書チェーンの長さ: 1
証明書[1]:
所有者: C=US, O=Android, CN=Android Debug
発行者: C=US, O=Android, CN=Android Debug
シリアル番号: 1
有効期間の開始日: Sun Aug 09 19:24:48 PWT 2020終了日: Tue Aug 02 19:24:48 PWT 2050
証明書のフィンガプリント:
	 MD5: ********
	 SHA1: ********
	 SHA256: ********
	 署名アルゴリズム名: SHA1withRSA
	 バージョン: 1
```
まず、コマンドでSHA1のフィンガプリントを作成し、下記のアプリの設定部分に登録します。
パスワードは特にありませんので、そのままEnterを入れてください。

![pic4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/098a90b9-3b5f-a07c-cb16-9bc6c66858ea.png)

続いて、SHA1を設定した後は、再度`google-services.json`をダウンロードし配置します。

`google-services.json`の設定方法については、[Firebase概要](http://localhost:8000/books/flutter_practice_introduction/firebase-overview)をご確認ください。

## iOSの設定
```xml:info.plist
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>{REVERSED_CLIENT_ID}</string>
        </array>
    </dict>
</array>
```

info.plistに設定を追加します。
下記イメージの通り、CFBundleURLSchemesにREVERSED_CLIENT_IDを設定します。

![pic5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/c9b48480-aa6b-abbd-621c-283822275788.png)


# 実装

ここ数年でメソッド名が変わったりしていますので、ご注意ください。
(FirebaseUserがUserに変更されたり、getCredentialがcredentialに変更されたりしています
本チャプターは下記のパッケージバージョンで動作確認をしています。

まず、必要なパッケージをインストールします。
必要に応じて、最新のバージョンをお使いください。

```yaml:pubspec.yaml
dependencies:
  firebase_core: ^1.0.4
  firebase_auth: ^1.1.1
  google_sign_in: ^5.0.2
```
続いて、検証に用いる画面のソースコードになります。
ログインのページとログアウトのページを準備し、それぞれの動作を確認していきます。


```dart:main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:firebase_test/LoginPage.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: LoginPage(),
    );
  }
}
```
メイン画面は、LoginPageを呼び出すだけですが、Firebaseを初期化するために、main関数を非同期にした上で、初期化用のメソッドを呼んでいます。


```dart:LoginPage.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:firebase_test/LogoutPage.dart';

class LoginPage extends StatelessWidget {
  static final googleLogin = GoogleSignIn(scopes: [
    'email',
    'https://www.googleapis.com/auth/contacts.readonly',
  ]);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Demo'),
      ),
      body: Center(
        child: TextButton(
          onPressed: () async {
            GoogleSignInAccount signinAccount = await googleLogin.signIn();
            if (signinAccount == null) return;
            GoogleSignInAuthentication auth =
                await signinAccount.authentication;
            final GoogleAuthCredential credential =
                GoogleAuthProvider.credential(
              idToken: auth.idToken,
              accessToken: auth.accessToken,
            );
            User user =
                (await FirebaseAuth.instance.signInWithCredential(credential))
                    .user;
            if (user != null) {
              await Navigator.of(context).pushReplacement(
                MaterialPageRoute(builder: (context) {
                  return LogoutPage(user);
                }),
              );
            }
          },
          child: Text(
            'login',
            style: TextStyle(fontSize: 50),
          ),
        ),
      ),
    );
  }
}

```
続いて、ログイン画面です。
画面には、ボタンが1つあるだけで、ログインボタンを押すと認証を行います。
処理は、`GoogleSignIn`でアカウントを取得したあとに、`GoogleSignInAuthentication,`と`GoogleAuthCredential`で認証を行います。

Google認証が問題なく行われたあとに、この情報を使って、Firebaseに認証情報を登録します。
該当部分は、`FirebaseAuth.instance.signInWithCredential(credential))`になります。

返ってきたユーザ情報を持って、ログアウトページに遷移しています。


```dart:Logout.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:firebase_test/LoginPage.dart';

class LogoutPage extends StatelessWidget {
  User user;
  LogoutPage(this.user);
  static final googleLogin = GoogleSignIn(scopes: [
    'email',
    'https://www.googleapis.com/auth/contacts.readonly',
  ]);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("Hello ${user.displayName}", style: TextStyle(fontSize: 50)),
            SizedBox(height: 20),
            TextButton(
              onPressed: () async {
                FirebaseAuth.instance.signOut();
                await googleLogin.signIn();
                await Navigator.of(context)
                    .pushReplacement(MaterialPageRoute(builder: (context) {
                  return LoginPage();
                }));
              },
              child: Text('Logout', style: TextStyle(fontSize: 50)),
            )
          ],
        ),
      ),
    );
  }
}
```
最後は、ログアウト画面です。
ログアウト画面は、認証された`user`の`displayName`を使ってユーザ名を表示しています。
その下にログアウトのボタンを配置し、ボタンを押すとログアウトが行われます。

処理は、まず`FirebaseAuth.instance.signOut();`でFirebaseのログアウトを行います。
次に`googleLogin.signIn();`でGoogleのログアウトを行います。
最後に、ログインページに遷移しています。


# 動作イメージ

実際の動作イメージは下記のようになります。

![pic1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/e84ce0f6-2913-e01b-585f-08e34121fef5.png)


初回のみ設定が必要となりますが、2回目からは問い合わせのみになるため、スムーズなログイン/ログアウトが行えます。

AndroidとiPhoneで表示される内容やステップが若干異なります。
また、2要素認証の有無によっても初回の動作が異なります。

Firebase側にもプロバイダーがGoogleの認証情報が登録されています。
![pic2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/5c8b6c08-0fb2-cfb2-4164-0631464853ed.png)