システム開発部のTです。
普段はAndroid、iOSなどのネイティブアプリを開発や、フロントエンドアプリの開発に携わっています。今回も前回の記事同様、デバイス認証の記事になります。

前編の記事の内容については、以下をご参照ください。

https://gaprot.jp/?p=17372

前編からのあらすじ

Flutterのlocal_authプラグインが公式から出たので、実装しながらどういうものかを前編で記事にしてみました。今回は、認証失敗時のエラーハンドリングや、その他オプションパラメータなどを実装しながら、どんな機能があるのかをレビューしていきます。

準備

前編からの続きなので、ここでは省略します。
必要であれば、前編の内容をご参照ください。

おさらい

前回のコードから、認証処理のみを抜粋したコードが以下になります。

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  Future<bool> normalAuthenticate() async {
    final LocalAuthentication auth = LocalAuthentication();
    try {
      final bool didAuthenticate = await auth.authenticate(localizedReason: 'Please authenticate to show account balance');
      if (didAuthenticate) {
        Fluttertoast.showToast(msg: '認証成功!!!');
      } else {
        Fluttertoast.showToast(msg: '認証失敗・・・');
      }
      return didAuthenticate;
    } on PlatformException catch (e) {
      Fluttertoast.showToast(msg: 'code: ${e.code} message: ${(e.message ?? e.details)}');
    }
    return false;
  }

local_authプラグインを利用することで、これだけのコードで最低限の認証処理が実装出来てしまうのは、ちょっと驚きでしたが、上記のみだとOSの認証設定が未設定の場合を考慮していませんでした。

たとえば、Androidで画面ロック解除の認証設定が未設定(つまり誰でも画面が見れてしまう状態)の場合、上記のコードだとdidAuthenticateにはfalseが返却されてしまいます。これは仕様ですが、プログラマーが意図的に判定させたい場合もあると思いますので、認証処理より前に確認できないのか?を見ていきたいと思います。

認証設定有無の確認方法

認証設定の設定有無について確認する場合、以下のメソッドを実行することで確認可能です。

final LocalAuthentication auth = LocalAuthentication();
final bool isDeviceSupported = await auth.isDeviceSupported();
if (isDeviceSupported) {
  Fluttertoast.showToast(msg: '認証設定済');
} else {
  Fluttertoast.showToast(msg: '認証未設定');
}

例えば以下の設定の場合、

画面ロックの選択が「なし」または「スワイプ」が選択されていた場合、isDeviceSupported()から返却される値は「false」になります。逆にパターン、PIN、パスワードが設定されていた場合は「true」が返却されます。

isDeviceSupported()を利用することで、認証処理実行前に事前に確認することが可能であり、画面ロック設定無しだった場合の処理分けが容易に実装できます。

今回、認証設定が未設定の場合は、「認証設定をお願いします。」のメッセージを表示するように実装してみます。認証処理の前に以下のコードを追加します。

  Future<bool> normalAuthenticate() async {
    final LocalAuthentication auth = LocalAuthentication();
    try {
      // ↓↓↓ 以下を追加 ↓↓↓
      final bool isDeviceSupported = await auth.isDeviceSupported();
      if (!isDeviceSupported) {
        Fluttertoast.showToast(msg: '認証設定をお願いします。');
        return false;
      }
      // ↑↑↑ ここまで ↑↑↑

      final bool didAuthenticate = await auth.authenticate(localizedReason: 'Please authenticate to show account balance');
      if (didAuthenticate) {
        Fluttertoast.showToast(msg: '認証成功!!!');
      } else {
        Fluttertoast.showToast(msg: '認証失敗・・・');
      }
      return didAuthenticate;
    } on PlatformException catch (e) {
      Fluttertoast.showToast(msg: 'code: ${e.code} message: ${(e.message ?? e.details)}');
    }
    return false;
  }

ここまで実装したら、画面ロック設定を「なし」にして、再実行してみます。

「+」ボタンを押下

エラーメッセージが表示される

以上でisDeviceSupported()の利用方法ご理解いただけたかと思います。

生体認証有無の確認方法

local_authプラグインには、デバイスに生体認証機能が備わっているかの有無を確認するプロパティが存在します。以下のコードを実行することで、生体認証の有無の判別が可能です。

final LocalAuthentication auth = LocalAuthentication();
final bool canAuthenticateWithBiometrics = await auth.canCheckBiometrics;
if (canAuthenticateWithBiometrics) {
  Fluttertoast.showToast(msg: '生体認証あり');
} else {
  Fluttertoast.showToast(msg: '生体認証なし');
}

上記にて判定を行い、生体認証機能のあるデバイスだった場合とない場合のそれぞれで処理分けすることも可能になります。

また、生体認証がありだった場合、さらに以下のコードでどの生体認証が利用可能かを取得することができます。

final LocalAuthentication auth = LocalAuthentication();
final List<BiometricType> availableBiometrics = 
                            await auth.getAvailableBiometrics();
availableBiometrics.forEach((type) {
    // 利用可能な生体認証の種類
});

上記を実行することで、利用可能な生体認証タイプ(BiometricType)が配列で返却されます。
BiometricTypeの中身を確認すると、5種類の定義があります。

// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// Various types of biometric authentication.
/// Some platforms report specific biometric types, while others report only
/// classifications like strong and weak.
enum BiometricType {
  /// Face authentication. 
  face,  // 顔認証

  /// Fingerprint authentication. 
  fingerprint,  // 指紋認証

  /// Iris authentication. 
  iris,  // 虹彩認証(公式に説明無し)

  /// Any biometric (e.g. fingerprint, iris, or face) on the device that the
  /// platform API considers to be strong. For example, on Android this
  /// corresponds to Class 3.
  strong,  // 認証強度クラス3

  /// Any biometric (e.g. fingerprint, iris, or face) on the device that the
  /// platform API considers to be weak. For example, on Android this
  /// corresponds to Class 2.
  weak,  // 認証強度クラス2
}

Android、iOS側にて、それぞれどういう生体認証タイプが返却されるのか確認してみました。
結果として、以下の内容として返却されました。

デバイス機種名返却された生体認証タイプ(BiometricType)
iPhoneSE(第2世代)BiometricType.fingerprint
iPhone13BiometricType.face
Pixel4BiometricType.weak
BiometricType.strong
Pixel5aBiometricType.weak
BiometricType.strong
HUAWEI P20LiteBiometricType.weak(指紋認証設定時のみ)
BiometricType.strong(指紋認証設定時のみ)

手元にある端末で確認した結果になります。
iPhone系では、指紋認証、顔認証とわかりやすいタイプが返却されました。
Android系だと、認証強度クラス2およびクラス3がタイプとして返却されました。Android系だと、顔認証、指紋認証って区切りでは無いようなので、ちょっと分かりづらいかもしれません。
いずれも虹彩認証は返却されませんでした。しかし、このプラグイン自体はWindowsもサポートしているため、他OSでは返却されるのかもしれませんが、本件では割愛させてください。

HUAWEIのP20Liteだけは特殊で、顔認証、および指紋認証それぞれサポートしているのですが、顔認証のみだと生体認証タイプは返却されず指紋認証を設定したところ、認証タイプが返却されました。想定になりますが、P20Liteの顔認証はOSとは別のデバイス独自の認証と思われるため、OSの生体認証として扱われていないのではないかと思っております。

Android、iOS両方をサポートする場合、以下のように処理分けが必要になります。

if (availableBiometrics.isNotEmpty) {
  // 生体認証未設定だった場合
}

if (availableBiometrics.contains(BiometricType.strong) ||
    availableBiometrics.contains(BiometricType.face) ||
    availableBiometrics.contains(BiometricType.fingerprint)) {
  // iOSで顔認証、指紋認証、Androidで生体認証設定済みの場合.
}

両OSで顔認証、または指紋認証設定済みの場合の処理分け例です。
Androidの場合、face、fingerprintだと条件にマッチしないため、strongも条件に入れております。
今回の検証時、Android側でweakのみが返却されるということが無かったので、どういった状態でstrongが返らない状況になるのかは書くことはできないのですが、上記ではstrong必須条件として処理を書いております。

以上、生体認証有無の確認方法でした。
全端末で確認したわけでもないので、気になる方はご自身の端末でチェックすることをおすすめします。特にAndroid系は十人十色なので面白いです。

その他認証処理のオプション設定について

前編でも処理を実装しましたが、それが以下になります。

try {
  final LocalAuthentication auth = LocalAuthentication();
  final bool didAuthenticate = await auth.authenticate(
      localizedReason: 'Please authenticate to show account balance');
  // ···
} on PlatformException {
  // ...
}

上記では、画面ロック時の認証設定をそのまま呼び出す処理になりますが、オプションを設定することで生体認証のみにすることも可能になります。その他、オプション設定があるので、ここで紹介します。

認証処理を生体認証のみにする

authenticateメソッド実行時、optionsに対して以下のように設定することで認証処理を生体認証のみにすることが可能になります。

final LocalAuthentication auth = LocalAuthentication();
final bool didAuthenticate = await auth.authenticate(
        localizedReason: 'Please authenticate to show account balance',
        options: const AuthenticationOptions(biometricOnly: true)
      );

上記のoptionsに「AuthenticationOptions(biometricOnly: true)」を設定するだけです。
デフォルトはfalseですが、trueにすることで生体認証中にPIN認証などの切り替えが不可能となります。
逆に生体認証機能の無いデバイスや、生体認証未設定時はエラーが返却(※)されるようになります。

※AndroidだとPlatformExceptionが返却され、iOSの場合は設定を促す以下のダイアログが表示されます。

エラーダイアログを非表示にする

authenticateメソッド実行時、optionsに対して以下のように設定することで認証処理のエラーをダイアログ表示するのではなく、PratformException例外として扱うことが可能になります。

    try {
      final bool didAuthenticate = await auth.authenticate(
          localizedReason: 'Please authenticate to show account balance',
          options: const AuthenticationOptions(biometricOnly: true, useErrorDialogs: false));
      if (didAuthenticate) {
        Fluttertoast.showToast(msg: '認証成功!!!');
      } else {
        Fluttertoast.showToast(msg: '認証失敗・・・');
      }
      return didAuthenticate;
    } on PlatformException catch (e) {
      Fluttertoast.showToast(msg: 'code: ${e.code} message: ${(e.message ?? e.details)}');
    }

例として、認証未設定時に表示された上記でのエラーダイアログも、「useErrorDialogs:false」にすることで、以下のように例外扱いにすることができます。プラグイン側で表示するエラーダイアログではなく、自前でエラー処理させたい場合におすすめです。

個人的には、デフォルト値「useErrorDialogs:true」だとAndroidとiOSで挙動が異なることもあるため、マルチプラットフォームアプリを検討するなら、「useErrorDialogs:false」として、エラー時は例外に処理を流して自前でエラー処理を実装することをおすすめします。

stickyAuthについて

認証中にアプリをバックグラウンドに切り替えた際、デフォルトでは無条件に認証が終了し、結果として「認証失敗」扱いにされてしまいます。それを防ぐのが以下のオプションパラメータ「stickyAuth」になります。
final bool didAuthenticate = await auth.authenticate(
    localizedReason: 'Please authenticate to show account balance',
    options: const AuthenticationOptions(stickyAuth: false));
if (didAuthenticate) {
  Fluttertoast.showToast(msg: '認証成功!!!');
} else {
  Fluttertoast.showToast(msg: '認証失敗・・・');
}

stickyAuthに「false」を設定することで、アプリがバックグラウンドに切り替わった場合でも、認証失敗になることなく、アプリが再びフォアグラウンドに戻ったときに再度認証を続けることが可能になります。

ただし、動作確認する限り、stickyAuth:falseの恩恵を受けているのはiOSのみであり、現状Androidではfalseであったとしても無条件に認証失敗になります。

sensitiveTransactionについて

AuthenticationOptionsのパラメータですが、公式の説明に無いパラメータです。
デフォルトは「true」ですが、これを「false」に変えて実行した場合も特に処理に変化が見られなかったため、不明なパラメータになります。
final bool didAuthenticate = await auth.authenticate(
    localizedReason: 'Please authenticate to show account balance',
    options: const AuthenticationOptions(sensitiveTransaction: false));
コードのコメントでは、以下の内容が記載されていました。
Whether platform specific precautions are enabled. For instance, on face unlock, Android opens a confirmation dialog after the face is recognized to make sure the user meant to unlock their device.

プラットフォーム固有の予防措置が有効かどうか。例えば、顔ロック解除の場合、Androidは顔認識後に確認ダイアログを開き、ユーザーがロック解除を意図していることを確認する。(Deep翻訳より)

以上、AuthenticationOptionsで設定できるパラメータを紹介しました。

最後に

前編では最速の実装を目指した形であったため、なるべく簡単な実装で済ませておりましたが、
後編となる本章では、各機能を深堀してみました。

可能な限りの深堀をしましたが、いかがだったでしょうか。
今後も使っていくうちに新たな発見もあるかもしれないので、また別の記事で紹介させていただきます。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる