Flutterアプリで生体認証を活用し、サーバサイドのユーザと紐付けたい

:::note info
この記事はLITALICO Engineers Advent Calendar 2023 カレンダー1 の 3日目の記事です
:::

## はじめに

* Flutterで生体認証を使うパッケージとしては、[local_auth](https://pub.dev/packages/local_auth)が有名
    * [記事](https://qiita.com/search?q=local_auth+flutter)もいっぱい書かれている
        * 参考になりました、ありがとうございます
* ただ、local_authは生体認証で認証できた/できなかったまでしか提供してくれない
    * サーバサイドと連携するアプリで活用することを考えると、サーバサイド側で保持しているユーザ情報とどう紐づけるか考える必要がある
    * 調べてみたところ、そこまで書かれている記事がないなと思ったので、まとめてみようと思った

## 概要図

![Flutter生体認証.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277180/260ce05d-a9c1-4263-81a8-5d53ce4aea5b.png)

**注）今回のサンプルでは概要を説明するために、passwordをflutter_secure_storageに保持しているが、トークンやその他、資格情報を保持し、passowrdを直接保持しないなどを適宜検討してください**

## 使用するパッケージ

* local_auth
    * 生体認証の利用、これを使うと、スマホの指紋認証、FaceIDなどを使った認証ができる
* flutter_secure_storage
    * アプリ側でのID/パスワードの保持用
* firebase_auth
    * サーバサイドの認証サンプル用


## コードサンプル

### Flutter　PJ全体の環境設定
``` yaml: pubspec.yaml
dependencies:
  local_auth: ^2.1.7
  flutter_secure_storage: ^9.0.0
  firebase_auth: ^4.14.0
```

### iOS向けの設定
```plist: info.plist
<key>NSFaceIDUsageDescription</key>
<string>生体認証を使用する目的を記述</string>
```

### Android向けの設定
```kotlin: MainActivity.kt
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant

class MainActivity: FlutterFragmentActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
    }
}
```

```xml: AndroidManifest.xml
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.app">
        <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
    <manifest>
```

### ユーザ登録

#### 必要なパッケージの読み込み

```dart: regist_user.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
```

#### ユーザ登録

```dart: regist_user.dart
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
final _storage = new FlutterSecureStorage();
/** 省略 **/

Future<void> registUser(String email, String password) async {
    // まず、Firebaseでユーザ登録を行う
    try {
        UserCredential _userCredential = await _firebaseAuth.createUserWithEmailAndPassword(
            email: email,
            password: password,
        );
    } on FirebaseAuthException catch (e) {
        debugPrint('debug: cannot regist: $e');
        //ToDo:エラーハンドリング
        return
    }

    // 次に、Firebaseで作成したユーザをSecureStorageに保存する
    await _storage.write(key: 'email', value: email);
    await _storage.write(key: 'password', value: password);
}
```

### 生体認証

#### 必要なパッケージの読み込み

```dart: authenticate_user.dart
import 'package:local_auth/local_auth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
```

#### 利用可能な生体認証の確認

```dart: authenticate_user_with_biometric.dart
LocalAuthentication _localAuth = LocalAuthentication();
/** 省略 **/

Future<List<BiometricType>> _getAvailableBiometricTypes() async {
    List<BiometricType> availableBiometricTypes;
    try {
        availableBiometricTypes = await _localAuth.getAvailableBiometrics();
    } on PlatformException catch (e) {
        debugPrint('debug: cannot get availavle biometrics: $e');
    }
    return availableBiometricTypes;
}
```

#### 生体認証を実地する

```dart: authenticate_user_with_biometric.dart
LocalAuthentication _localAuth = LocalAuthentication();
/** 省略 **/

Future<bool> _authenticateWithBiometric() async {
    bool authenticate = false;

    List<BiometricType> availableBiometricTypes = await _getAvailableBiometricTypes();

    try {
      if (availableBiometricTypes.contains(BiometricType.face)
          || availableBiometricTypes.contains(BiometricType.fingerprint)) {
        authenticate = await _localAuth.authenticateWithBiometrics(localizedReason: "認証してください");
      }
    } on PlatformException catch (e) {
        debugPrint('debug: cannot authenticate with biometrics: $e');
    }
    return authenticate;
  }
```

#### 生体認証を通過後、保持していたID/パスワードで認証する

```dart: authenticate_user_with_biometric.dart
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
final _storage = new FlutterSecureStorage();
/** 省略 **/

Future<void> _loginWithBiometric() async {
    bool authenticate = await _authenticateWithBiometric();

    if (authenticate) {
        String email = await _storage.read(key: 'email');
        String password = await _storage.read(key: 'password');

        try {
            final credential = await _firebaseAuth.signInWithEmailAndPassword(
                email: email,
                password: password
              );
        } on FirebaseAuthException catch (e) {
            debugPrint('debug: cannot login: $e');
        }

        // ToDo: ログイン後処理
    }
}
```

# あとがき

配布するアプリで考えると、他にも下記のように色々やらないといけないことがあります

* View（ユーザ登録画面、ログイン画面）の作成
* エラーハンドリング
* 認証情報の保持方法（JWTやセッションなど）

ただ、大まかな流れは確認いただけるかなと思います

簡単な動作検証しかしておりませんが、2時間くらいでやりたかったことが概ね確認できるのは、Flutter本当に良いなと思います

明日は、@tjinjinさんの記事です！おたのしみに〜！