Chapter 30無料公開

 02.Authentication

kazutxt
kazutxt
2023.04.15に更新

Firebase Authentication(以下Authentication)の概要を確認した上で、メール/パスワード認証とGoogle認証の2つの実装方法を解説します。

概要

Authenticationは名前の通りユーザの認証機能を提供します。
一般的に認証は非常に取り扱いが難しい問題です。なぜならユーザの重要な個人情報であるユーザ名やメールアドレス、パスワードをセキュアに取り扱う必要があるためです。
ユーザ情報は極めて厳重に管理をする必要があり、ユーザ情報が流出すると大きな責任を問われかねません。

Authenticationはユーザ認証に関するこれらの問題を簡単に解決してくれます。
まず、Flutterから認証機能を利用する方法は、メソッドに適切な引数を渡して呼び出すだけで実現できます。この時、ユーザ情報は開発者が管理するわけではなく、Firebaseが管理しUIDと紐付けてくれます。
そのため、DB格納時の暗号化や暗号鍵のメンテナンスなどを検討する必要はなく、Firebaseの画面から利用者のパスワードを見ることもできないため、関係者による盗聴リスクもありません。開発者はこのUIDを使ってユーザに紐づく情報を取り出せます。
加えて、Authenticationは多くの認証方式に対応しています。メール/パスワードによる認証を始め、Google/Twitter/Facebook/GitHubなどのソーシャルアカウントによる認証にも対応しています。

メールアドレスとパスワードによる認証

まず、一番基本となるメールアドレスとパスワードによる認証を解説します。

有効化手順

最初に、Firebaseのサイドメニューから「構築」 > 「Authenctication」を選択し「始める」をクリックします。

次に「Sign-in method」のタブから「メール/パスワード」を選択し、有効にします。

パッケージのインストール

今回は、firebase_authのパッケージを利用します。詳細はパッケージの公式サイトを参照してください。
https://pub.dev/packages/firebase_auth

まず、必要なパッケージをインストールします。

pubspec.yaml
# 必要部分のみ抜粋/バージョンは各自の環境に合わせたものを使う
dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^1.19.1
+ firebase_auth: ^3.4.1

実装例

このアプリでは、メールアドレスとパスワードによるユーザの登録や認証、パスワードリセットを行います。

今回は、先に画面と機能を説明してから、ソースコード例を解説します。
画面には、メールアドレス/パスワードの2つの入力欄と、ユーザ登録/ログイン/パスワードリセットの3つのボタンが並んでいます。

機能は、ボタンに対応して3つあります。

  1. メールアドレスとパスワードを入力して、ユーザ登録ボタンを押すと、新規にユーザ登録ができる。
  2. メールアドレスとパスワードを入力して、ログインボタンを押すと、ユーザの認証ができる。
  3. メールアドレスを入力して、パスワードリセットボタンを押すと、パスワードのリセットの案内をメールアドレス宛に送れる。

ソースコードは、下記のようになっています。

lib/main.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

// MyApp、MyHomePageはデフォルトから変更がないため省略

class _MyHomePageState extends State<MyHomePage> {
  // 入力したメールアドレス・パスワード
  String _email = '';
  String _password = '';

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              // 1行目 メールアドレス入力用テキストフィールド
              TextFormField(
                decoration: const InputDecoration(labelText: 'メールアドレス'),
                onChanged: (String value) {
                  setState(() {
                    _email = value;
                  });
                },
              ),
              // 2行目 パスワード入力用テキストフィールド
              TextFormField(
                decoration: const InputDecoration(labelText: 'パスワード'),
                obscureText: true,
                onChanged: (String value) {
                  setState(() {
                    _password = value;
                  });
                },
              ),
              // 3行目 ユーザ登録ボタン
              ElevatedButton(
                child: const Text('ユーザ登録'),
                onPressed: () async {
                  try {
                    final User? user = (await FirebaseAuth.instance
                            .createUserWithEmailAndPassword(
                                email: _email, password: _password))
                        .user;
                    if (user != null)
                      print("ユーザ登録しました ${user.email} , ${user.uid}");
                  } catch (e) {
                    print(e);
                  }
                },
              ),
              // 4行目 ログインボタン
              ElevatedButton(
                child: const Text('ログイン'),
                onPressed: () async {
                  try {
                    // メール/パスワードでログイン
                    final User? user = (await FirebaseAuth.instance
                            .signInWithEmailAndPassword(
                                email: _email, password: _password))
                        .user;
                    if (user != null)
                      print("ログインしました ${user.email} , ${user.uid}");
                  } catch (e) {
                    print(e);
                  }
                },
              ),
              // 5行目 パスワードリセット登録ボタン
              ElevatedButton(
                  child: const Text('パスワードリセット'),
                  onPressed: () async {
                    try {
                      await FirebaseAuth.instance
                          .sendPasswordResetEmail(email: _email);
                      print("パスワードリセット用のメールを送信しました");
                    } catch (e) {
                      print(e);
                    }
                  }),
            ],
          ),
        ),
      ),
    );
  }
}

まず、firebase関連のパッケージをimportしています。そして、main関数の中でFirebase.initializeAppを行い、Firebaseに関する初期化を行っています。

続いて、メールアドレスとパスワード用のTextFormFieldを用意し、入力があるたびに_email、_passwordを更新しています。
ユーザの登録と認証の部分は、それぞれ対応するFirebaseAuthのメソッドを呼び出して実施しています。

  • 登録 : FirebaseAuth.instance.createUserWithEmailAndPassword
  • 認証 : FirebaseAuth.instance.signInWithEmailAndPassword

どちらも、メールアドレスとパスワードを引数に与えると、User型のインスタンスが返却されます。

最後に、パスワードリセット機能です。FirebaseAuth.instance.sendPasswordResetEmailにメールアドレスを渡して呼び出すと、メールサーバなどを準備しなくても、自動でそのメールアドレスにパスワードリセット用のメールを送れます。なお、このリセット用のメールの文面は「Authentication」 > 「Templates」からカスタマイズできます。

動作イメージ

適切なメールアドレスとパスワードを入力するとユーザ登録ができ、登録後に同じメールアドレスとパスワードを使ってログインボタンを押すと、認証が行われます。

ターミナル
I/flutter (21407): ユーザ登録しました kazutxt@test.com , ggF8ODiDWmcoSpKfSoHUWtf93Us1
I/flutter (21407): ログインしました kazutxt@test.com , ggF8ODiDWmcoSpKfSoHUWtf93Us1

また、「Authentication」>「Users」に登録したユーザの一覧が表示されます。
この画面からは、ログイン日の確認や登録ユーザの無効化や削除もできます。

パスワードリセットは、メールアドレスを入力しパスワードリセットボタンを押すと、入力したメールアドレスにパスワードリセット用のメールが送られてきますので、こちらを使ってパスワードをリセットできます。

Googleアカウントによる認証

Authenticationは、メールとパスワードによる認証以外にも、様々なソーシャルアカウントと連携ができるようになっています。
今回は一例として、Googleアカウントによる認証を解説します。
この認証方法を使えば、メールアドレスを登録することなく、Googleアカウントさえ持っていれば認証が行えます。しかも、UIDは「メールアドレスとパスワードによる認証」でも「Googleアカウントによる認証」でも、同じように払い出されますので、認証後は統一したロジックで処理を行えます。

有効化手順

まず、FirebaseでGoogle認証を有効化します。
Authenticationの「Sign-in method」タブで、追加のプロバイダからGoogleを選択します。


プロジェクトの公開名はデフォルトのままでも構いませんが、認証時にユーザに表示されます。サポートメールアドレスも選択します。


事前準備

Google認証では事前準備が必要になります。

Androidの設定

Androidでは証明書のSHA1のフィンガープリント(証明書の同一性を保証するための短いバイト列)が必要になります。
今回はデバッグ用のものを利用します。下記のコマンドを実施し、パスワードに「android」と入力します。
画面に表示されるSHA1をFirebaseのプロジェクト設定画面にあるAndroidアプリのフィンガープリントに設定します。

ターミナル
{プロジェクトのルート} % keytool -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore
キーストアのパスワードを入力してください:  
別名: androiddebugkey
# 中略
証明書のフィンガプリント:
	 SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
# 中略

SHA1を設定した後に「google-services.json」をダウンロードし「android/app」に置かれているものに上書きします。

iOSの設定

Firebaseのプロジェクト設定画面にあるiOSアプリの画面から「GoogleService-Info.plist」をダウンロードします。
Xcodeを起動し「Runner」を右クリックして「Add Files to "Runner"」を選択し、上記ファイルを選択します。


VSCode に戻り、下記のスニペットとイメージの通り、「info.plist」にCFBundleURLTypesを追記します。CFBundleURLSchemesには、「GoogleService-Info.plist」に記載されているREVERSED_CLIENT_IDを設定します。

ios/Runner/info.plist
<!-- dictタグ内の追記する部分のみ抜粋-->
+<key>CFBundleURLTypes</key>
+<array>
+   <dict>
+       <key>CFBundleTypeRole</key>
+       <string>Editor</string>
+       <key>CFBundleURLSchemes</key>
+       <array>
+           <string>{REVERSED_CLIENT_ID}</string>
+       </array>
+   </dict>
</array>

パッケージのインストール

Google認証用のgoogle_sign_inのパッケージを追加でインストールします。こちらも詳細はパッケージの公式サイトを参照してください。
https://pub.dev/packages/google_sign_in

まず、必要なパッケージをインストールします。

pubspec.yaml
# 必要部分のみ抜粋/バージョンは各自の環境に合わせたものを使う
dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^1.19.1
  firebase_auth: ^3.4.1
+ google_sign_in: ^5.4.0

実装例

このアプリでは、Google認証を行います。

lib/main.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:google_sign_in/google_sign_in.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

// MyApp、MyHomePageはデフォルトから変更がないため省略

class _MyHomePageState extends State<MyHomePage> {
  // Googleアカウントの表示名
  String _displayName = "";
  static final googleLogin = GoogleSignIn(scopes: [
    'email',
    'https://www.googleapis.com/auth/contacts.readonly',
  ]);

  
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
      child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
        Text("Hello $_displayName", style: TextStyle(fontSize: 50)),
        TextButton(
          // ボタンを押した時のイベント
          onPressed: () async {
            // Google認証
            GoogleSignInAccount? signinAccount = await googleLogin.signIn();
            if (signinAccount == null) return;
            GoogleSignInAuthentication auth =
                await signinAccount.authentication;
            final OAuthCredential credential = GoogleAuthProvider.credential(
              idToken: auth.idToken,
              accessToken: auth.accessToken,
            );
            // 認証情報をFirebaseに登録
            User? user =
                (await FirebaseAuth.instance.signInWithCredential(credential))
                    .user;
            if (user != null) {
              setState(() {
                // 画面を更新
                _displayName = user.displayName!;
              });
            }
          },
          child: const Text(
            'login',
            style: const TextStyle(fontSize: 50),
          ),
        ),
      ]),
    ));
  }
}

「login」のテキストボタンをタップするとgoogleLogin.signIn()でGoogle認証を行います。
認証が正常に行われればsigninAccount.authenticationGoogleAuthProvider.credentialで認証情報を取得します。
Firebaseへの認証はFirebaseAuth.instance.signInWithCredential(credential)で行っています。

動作イメージ

Googleアカウントによる認証が正常に行われると、ユーザのアカウント表示名がHelloに続けて画面に表示されます。
初回は認証・承認作業が必要となりますが、これらの情報が記録されていれば2回目以降はログイン情報の確認のみになるため、スムーズなログインが行えます。

Firebaseにも、プロバイダがGoogleの登録ユーザが表示されています。