Androidアプリ内で端末認証画面を出す方法をまとめてみた


実装しているプロダクトにて、端末認証を利用してユーザ認証を行っておりますが、
API levelによって実装方法が結構変わっているので整理してみました。

## 前提
以下の通りの定義とし、今回は端末認証の話のみ扱います。
ただし、「生体認証情報がある場合は生体認証を行い、それ以外の場合は端末認証情報を利用する」といった利用パターンの場合はこの限りではありません。

|認証|種類|
|--|--|
|端末認証|パターン、PINなど|
|生体認証| 指紋、顔、虹彩など |

以降記載する内容は全て公式が提供している情報に依拠していますので詳しくはAndroidDevelopersを参照ください。

## 結論
OS11以降,OS10,OS9~5でそれぞれ実装方法が変わります。(4以下の話はなしで...)
大きく分類すると以下の通りです。

|OS|実装方法|
|--|--|
|10~|BiometricPrompt (android.hardware.biometrics) を利用する|
|9~5|KeyGuardManagerを利用する |

後述するBiometricライブラリ（androidx.biometric）を利用することで簡略化可能です。
**これから実装するなら当該ライブラリを利用した方が良いと思います。**


### OS9以下
*KeyGuardManager#createConfirmDeviceCredentialIntent()*を利用してIntentを発行します
startActivityForResultを利用して認証結果を待ちます。
RESULT_OKだったら認証成功です。
今だったら[ActivityResultsAPI](https://developer.android.com/training/basics/intents/result?hl=ja#register)が推奨されているので、当該APIを利用した実装例を以下に示します。

```kotlin
    private val checkCredentialLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { rs ->
        if (rs.resultCode == RESULT_OK) {
            //認証成功　
        }
    }

    val keyguardManager =
    context.getSystemService(Context.KEYGUARD_SERVICE) as? KeyguardManager
    val intent = keyguardManager.createConfirmDeviceCredentialIntent(null, null)
    intent?.let { checkCredentialLauncher.launch(it) }
```

### OS10
*KeyGuardManager#createConfirmDeviceCredentialIntent()*がDeprecatedになる代わりに*BiometricPrompt*を利用することが推奨されています。
何もせずに実装すると、生体認証のみとなり、端末認証が利用できませんので*BiometricPrompt*作成時に端末認証を有効にします。
これで生体認証情報がない場合は端末認証情報を利用するようになります

```kotlin
    BiometricPrompt.Builder(context)
        .setTitle("端末認証")
        .setDeviceCredentialAllowed(true) // 端末認証を有効にする
        .build()
```

これだけではオブジェクトを作っただけなのでコールバック等追加して認証を実行するようにします

```kotlin
    val biometricPrompt =
        BiometricPrompt.Builder(context)
            .setTitle("端末認証")
            .setDeviceCredentialAllowed(true)
            .build()

    //処理のキャンセル要求があった
    val cancellationSignal = CancellationSignal()
    cancellationSignal.setOnCancelListener {
        //キャンセルされた
    }

    val authCallBack = object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
            //認証成功
        }

        override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
            //認証失敗 or ダイアログを消す
        }
    }

    val executors = ContextCompat.getMainExecutor(context)
    //端末認証実行
    biometricPrompt.authenticate(cancellationSignal, executors, authCallBack)
```

これを実行すると、以下のようなダイアログが出てきます。左下が「パターンを使用」になっていますが、端末でパターン認証を利用しているからです。この通り、端末に生体認証情報が登録されているなら生体認証を優先し、登録されていないなら端末認証情報を利用することができます。
![device-2021-06-24-193403.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323477/fd3ece11-5706-db3d-789e-b10a2cf4b82c.png)



### OS11以降
*BiometricPrompt#setDeviceCredentialAllowd()*がDeprecatedになる代わりに*setAllowedAuthenticators()*を利用することが推奨されています。
アプリがサポートする認証の種類を詳しく定義できるようになったようです。
今回は端末認証だけを有効にします。他はOS10で実装した物と同じでOKです。
もし生体認証も併せて使いたい場合はsetAllowedAuthenticatorsの引数に追加します。
（下の例は端末認証しか使えない状態です）

```kotlin
    BiometricPrompt.Builder(context)
        .setTitle("端末認証")
        .setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
        .build()
```

### 端末認証が有効かどうかを確認する
念のためですが、**生体認証が有効かどうか**の確認ではありませんのでご注意ください
*KeyGuardManager#isKeyguardSecure*で確認可能です

## ここまでのまとめ
上記までの内容をまとめた実装例を以下に示します

```kotlin
    private fun checkCredential(context: Context) {
        val keyguardManager =
            context.getSystemService(Context.KEYGUARD_SERVICE) as? KeyguardManager
        //端末認証が有効か?
        if (keyguardManager?.isKeyguardSecure == true) {
            //OS10以上か?
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                val biometricPrompt =
                    //OS11以上か?
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                        BiometricPrompt.Builder(context)
                            .setTitle("端末認証")
                            .setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
                            .build()
                    } else {
                        BiometricPrompt.Builder(context)
                            .setTitle("端末認証")
                            .setDeviceCredentialAllowed(true)
                            .build()
                    }

                //認証のキャンセル要求があった
                val cancellationSignal = CancellationSignal()
                cancellationSignal.setOnCancelListener {
                    //キャンセルされた
                }

                val executors = ContextCompat.getMainExecutor(context)
                val authCallBack = object : BiometricPrompt.AuthenticationCallback() {
                    override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
                        //認証成功
                    }

                    override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
                        //認証失敗 or ダイアログを消す
                    }
                }
                biometricPrompt.authenticate(cancellationSignal, executors, authCallBack)
            } else {
                val i = keyguardManager.createConfirmDeviceCredentialIntent(null, null)
                checkCredentialLauncher.launch(i)
            }
        }
    }

```

## biometricライブラリについて
上記の実装ですが、[biometricライブラリ](https://developer.android.com/reference/androidx/biometric/package-summary?hl=ja)を利用することでもうちょっと良い感じにできます。
9系以下でも利用できます。


```build.gradle
implementation "androidx.biometric:biometric:1.1.0"
```


```kotlin
    val executor = ContextCompat.getMainExecutor(this)

    val authCallBack = object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
            // 認証成功
        }

        override fun onAuthenticationFailed() {
            // 認証失敗
        }

        override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
            // 認証エラー
        }
    }

    val biometricPrompt = BiometricPrompt(this, executor, authCallBack)

    val promptInfo =
        //OS11以上か?
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            BiometricPrompt.PromptInfo.Builder()
                .setTitle("端末認証")
                .setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
                .build()
        } else {
            BiometricPrompt.PromptInfo.Builder()
                .setTitle("端末認証")
                .setDeviceCredentialAllowed(true)
                .build()
        }

    biometricPrompt.authenticate(promptInfo)

```