AkkeyLab

Biometrics Auth thinking in the stream by ReactiveSwift

LAContext を利用することで touch id もしくは face id を用いることができます。今回は、 ReactiveSwift に対応させながら、テスタブルに実装する事例をご紹介します。 RxSwift でもほぼ同様の実装で実現可能です。

完成形

まずは完成形をみていただきます。呼び出し部分はたったコレだけです。生体認証を行いたいときに evaluatePolicy を呼び出し、成功した場合はアプリケーションのパスワード入力などを省略させる処理を実行します。
この実装方法に興味を持っていただけたのであれば、記事を読み進めることをお勧めします。

import LocalAuthentication
import ReactiveSwift

let context = LAContext()
context.evaluatePolicy(
    .deviceOwnerAuthenticationWithBiometrics,
    localizedReason: "Log in using biometrics."
)
    .take(duringLifetimeOf: self)
    .startWithResult { result in
        switch result {
        case .success:
            // success !!
        case .failure:
            // Check why it failed
        }
}

事前準備:ReactiveSwift 対応

まず、非同期処理を ReactiveSwift に対応させておきます。 RxSwift でもほぼ同様の実装で実現可能です。

import LocalAuthentication
import ReactiveSwift

extension Reactive where Base: LAContext {
    func evaluatePolicy(
        _ policy: LAPolicy, 
        localizedReason: String
    ) -> SignalProducer<Void, Error> {

        SignalProducer<Void, Error> { [weak base] observer, _ in
            base?.evaluatePolicy(
                policy,
                localizedReason: localizedReason,
                reply: { success, error in
                    guard success else {
                        guard let error = error else { return }
                        return observer.send(error: error)
                    }
                    observer.send(value: ())
                    observer.sendCompleted()
                }
            )
        }
    }
}

認証に失敗する場合のパターン

evaluatePolicy を呼び出して failure が返される場合は3パターンです。

  • 生体認証をキャンセルした時( touch id )
  • 生体認証に数回失敗した場合
  • 生体認証が許可されていない場合( face id )

まず、 touch id の場合は表示されたアラートにキャンセルボタンがついています。これをタップした場合でも失敗として結果が返ってきます。従って、「一度認証に失敗したら生体認証を利用できなくする」という仕様の場合、キャンセルを押した場合も利用できなくなることに注意しなければなりません。

次に、生体認証に数回失敗した場合です。これは iOS 側によって制御されるもので、連続で複数回失敗すると一時的に生体認証が無効になります。このモードに突入したときも失敗として結果が返ってきます。従って、失敗したときに「再度認証ボタンを押してください」などのリトライをユーザに促したい場合、 canEvaluatePolicy で生体認証が有効であるかを確認しなければなりません。

最後に、生体認証が許可されていない場合です。 face id の場合、アプリで初めて生体認証を利用しようとしたときに有効化するかを確認するダイアログが表示されます。ここで拒否した場合や、設定画面から手動で無効化した場合には問答無用で失敗が返ってきます。

context.evaluatePolicy(
    ...
)
    .take(duringLifetimeOf: self)
    .startWithResult { [weak self] result in
        switch result {
        case .success:
            // success !!
        case .failure:
            // Check why it failed
            self?.button.isEnabled = self?.context
                .canEvaluatePolicy(
                    .deviceOwnerAuthenticationWithBiometrics, 
                    error: nil
                )
        }
}

先程の3パターンによって挙動を変える場合、 failure が返ってきたときに canEvaluatePolicy を確認します。上記実装例では生体認証を行うボタンの有効・無効を切り替えています。

実行順番に注意

先程の実装例にあるように生体認証を行うボタンを配置したとします。その場合、ボタンの image を face id / touch id にしたり、文言を変更することが考えられます。そこで、 context.biometryType を用いて切り替え処理を行うことになります。

ここで注意したいのが、 context.biometryType を参照するときは canEvaluatePolicy を一度呼び出しておく必要があるということです。事前にそれを呼び出さなかった場合、 LABiometryType.none が返ってきてしまいます。

各種設定

<key>NSFaceIDUsageDescription</key>
<string>Log in using biometrics.</string>
let context = LAContext()
context.evaluatePolicy(
    .deviceOwnerAuthenticationWithBiometrics,
    localizedReason: "Log in using biometrics."
)
    ...

そして、忘れてはいけない設定項目があります。
face id の場合、許可ダイアログの表示が必要なため、その文言を plist ファイルに記述する必要があります。 touch id の場合、許可ダイアログは存在しませんが、認証アラートにその利用目的などを表示できます。 evaluatePolicy の引数 localizedReason にその文言を指定します。

テスタブルな実装

LAContext を protocol 化することで DI が可能になり、テストが書きやすくなります。ReactiveSwift に対応させていることに注意してください。

import LocalAuthentication
import ReactiveSwift

public protocol LAContextProtocol: AnyObject {
    var biometryType: LABiometryType { get }

    @discardableResult
    func canEvaluatePolicy(_ policy: LAPolicy, error: NSErrorPointer) -> Bool
    func evaluatePolicy(_ policy: LAPolicy, localizedReason: String) -> SignalProducer<Void, Error>
}

実装完了

以上の実装で以下のような生体認証フローを実現できるようになります。アイデア、ご指摘等ございましたら、このホームページの連絡フォームより気軽に送っていただけると嬉しいです。
最後までお読みいただき、ありがとうございます。

import LocalAuthentication
import ReactiveSwift

let context = LAContext()
context.evaluatePolicy(
    .deviceOwnerAuthenticationWithBiometrics,
    localizedReason: "Log in using biometrics."
)
    .take(duringLifetimeOf: self)
    .startWithResult { result in
        switch result {
        case .success:
            // success !!
        case .failure:
            // Check why it failed
        }
}

Author

Next post

Build the best remote work environment!

リモートワークを行うエンジニア向けに、シンプルかつ高機能な作業環境を構築する方法を伝授いたします。ここで紹介するものは全て私が使用しているものであり、皆様にお勧めできると判断したものとなっております。

Read More →