Flutter Web x Firebase Auth x Google認証を実装してみる

この記事は [Flutter Advent Calendar 2019](https://qiita.com/advent-calendar/2019/flutter) 16日目の記事です。

Flutter使ってみたいなーと思っていたので、まずはFirebase Auth + Google認証を組み込んだ上で、Flutter Webでの挙動を確認してみたよ。という内容になっています。主にWebでの挙動確認が主目的です。参考にしたサンプルの品質が若干怪しいので、それだけ気をつけてください。

## Firebase Auth + Google認証の組み込み

Flutter Webは直近ベータになったようですが、まだ知見も少ないので、まずはモバイル（Android）でちゃんと動くかどうかを確認してから進めました。まずは下準備として、以下の記事あたりを参考に、Androidで認証フローを一通り確認できるようにしています。

 * https://blog.codemagic.io/firebase-authentication-google-sign-in-using-flutter/
 * https://medium.com/flutter-community/flutter-implementing-google-sign-in-71888bca24ed
 * https://levelup.gitconnected.com/using-firebase-in-flutter-web-4b99952180aa

認証部分のコードは以下のようになっています（記事からの引用）

```dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';

final FirebaseAuth _auth = FirebaseAuth.instance;
final GoogleSignIn googleSignIn = GoogleSignIn();

Future<String> signInWithGoogle() async {
  final GoogleSignInAccount googleSignInAccount = await googleSignIn.signIn();
  final GoogleSignInAuthentication googleSignInAuthentication =
  await googleSignInAccount.authentication;

  final AuthCredential credential = GoogleAuthProvider.getCredential(
    accessToken: googleSignInAuthentication.accessToken,
    idToken: googleSignInAuthentication.idToken,
  );

  final AuthResult authResult = await FirebaseAuth.instance.signInWithCredential(credential);
  final FirebaseUser user = authResult.user;

  assert(!user.isAnonymous);
  assert(await user.getIdToken() != null);

  final FirebaseUser currentUser = await _auth.currentUser();
  assert(user.uid == currentUser.uid);

  return 'signInWithGoogle succeeded: $user';
}

void signOutGoogle() async{
  await googleSignIn.signOut();

  print("User Sign Out");
}
```

**GoogleからIDトークン取得 → FirebaseへIDトークンを投げて認証** が大きな流れになっております。このサンプルではGoogleからトークンを取得するフローがImplicitっぽいのが気になりました。あとFirebase Authってリプレイアタックし放題なんですっけ。

...チョット本筋とズレてしまうので今回はこのまま進めます。

## Flutter Web をビルドする準備

Webをビルドできるようにします。
ベータ版を有効にする必要があります。

```
flutter channel beta
flutter upgrade
flutter config --enable-web
```

flutter devicesを叩いて Chrome が確認できればOK

```
$ flutter devices

Android SDK built for x86 • emulator-5554 • android-x86    • Android 9 (API 28) (emulator)
Chrome                    • chrome        • web-javascript • Google Chrome 78.0.3904.108
Web Server                • web-server    • web-javascript • Flutter Tools
```

既存のプロジェクトにWeb版を追加する場合は以下コマンドでOKです。

```
flutter create .
```

実行すると、各種設定ファイルが生成されると思います。分かりやすいところだろ web/index.html ですね。

```web/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>flutter_auth_playground</title>
</head>
<body>
  <script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
```

packages getしてパッケージを最新にしておきましょう。
次のコマンドWebを起動します。

```
flutter run -d chrome
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66417/a7378603-36b8-f13f-8db9-845f0a585d7b.png" width="400">

動きました。やったね。

## 起動時のポートの固定方法

前述した起動方法だと、毎回ポートが変わります。
ポートが毎回変わるとOAuth 2.0のオリジン検証で弾かれるのが面倒なので、起動時のポートを固定しておくと捗ります。

```
flutter run -d chrome --web-port=3333
```

Android Studioの場合は下記から設定できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66417/90125db2-d199-fe1f-c955-fa0f77a08204.png" width="400">
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66417/25aed970-1eb3-e442-703c-54b01ef80f3d.png)

## Google OAuth と Firebaseの接続設定

Androidだと android/app/google-services.json をぶっこむだけでしたが、Web版の場合はindex.htmlのヘッダに接続情報を記述します。また、firebaseのスクリプトを読ませる必要があります。

```web/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="google-signin-client_id" content="0123456789-xxxxxxxxxxxxxxxx.apps.googleusercontent.com" />
  <title>flutter_auth_playground</title>
</head>
<body>
  <script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-auth.js"></script>
  <script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
```

google-signin-client_id はGCPのOAuthクライアント作成で取得したクライアントIDを使います。
作成時に承認済みのURLに、先程ポートを固定したURIを設定しておきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66417/0aef1fa7-073e-7d69-c8ed-d666541c695d.png" width="600">

次に、Firebase側でWeb用アプリを追加し、その際に発行されたfirebaseConfigを覚えておきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66417/a15f39e3-ae5e-8b1f-c79c-a122777c3fd3.png" width="600">


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66417/5c855f91-a2a8-bbf0-093f-b8148610543e.png" width="600">

firebaseConfigを参考に、以下のような形でmain.dartに記述します。

```main.dart
import 'package:firebase/firebase.dart' as firebase;

void main() {
  firebase.initializeApp(
    apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    authDomain: "xxxxxxxxxxx.firebaseapp.com",
    databaseURL: "https://xxxxxxxxxxx.firebaseio.com",
    projectId: "xxxxxxxxxxxx",
    storageBucket: "xxxxxxxxxxx.appspot.com"
  );

  runApp(MyApp());
}
```

設定はこれで終わりです。


## 動かしてみる

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66417/a7378603-36b8-f13f-8db9-845f0a585d7b.png" width="400">

（Sign in with Google） をクリックしてGoogle認証を通すと...

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66417/f6cdf263-bddf-a9e2-4318-5f35402dcf47.png" width="400">

動きました :tada: 


気になっていたトークンの格納場所ですが、IndexedDBの中を掘っていったところのstsTokenManagerにアクセストークンもリフレッシュトークンも保存されていました。まあWeb Storageに入れるしかないよなーという感じですね。

<img width="809" alt="token.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66417/e08a8012-3ae2-9289-86af-1c115296ef4e.png">

## まとめ

Flutter Web に Firebase AuthとGoogle認証を組み込んでみました。ほとんどコードを書き換えることなく各プラットフォームのビルドができるのは素晴らしい体験ですね。DOMではなくcanvasに描画されるので動きに若干の違和感はあるなーという感想でした。

明日はmirockさんがDesktop Embedding for Flutterについて書いてくれます！楽しみ！