技術は私たちの力。技術は私たちの楽しみ。 Creative Developer BLOG 技術部ブログ
Technology is our strength. Technology is what we enjoy.

生体認証ログイン機能を実装してみた話

2024-08-05 勉強会
こんにちは、システム部開発ユニットの大原です!

今日はモバイル端末での生体認証機能を使った
ログイン機能の実装についてお話しします。
「モバイルアプリの開発で"めんどくさい"をなくすためにできることはなんだろう」
というテーマで、勉強会に取り組んでみました!

この記事では、私がなぜこの機能を実装しようと思ったのか、
そのプロセス、そして実際にどう実装したのかを詳しく紹介します。

きっかけ

まず、私が生体認証ログイン機能をどんなプロセスで思いつき、その実装に至ったか。
という背景をお話しします。

きっかけ①モバイルエンジニアなのにスマホでの英語入力が苦手なこと

自分はモバイルアプリエンジニアを2年以上やっていますが、
そこまでスマホの操作に長けているわけではなく…
ログイン情報を入力する際に、キーボード入力だと何度も打ち間違えたり、
フリック入力にしても英語のフリック入力の場合は慣れていないこともあり
時間がかかってしまったりで、ログイン情報を間違えてやり直したりすることがありました。

…ログインするのがめんどくさすぎる!
という大原の超絶めんどくさがりが発動したため、
私利私欲のために簡単にログインできる方法はないかと考えていました。

↑スマホのキーボード入力はこんなことに…

この例はあまりにも打ち間違えてますが、意外とつまづくことが日常的にも多いです…

きっかけ②SUBLINEの本人確認実装のプロジェクトに携わった経験

以前、弊社のサービスSUBLINEで、本人確認機能を実装するプロジェクトがありまして、
検証者としてプロジェクトに参加した際に、免許証の撮影や自撮りでの本人確認を行なってみて、
生体情報ってセキュリティ的に非常に信頼できるものなんだなぁと感じました。

それから日常的にスマホを使用しながら生活する中で、スマホ本体のロック解除や
インストールしているスマホアプリ内で生体認証を求められることが多々あり、
この機能は何かに使えるんじゃないか、自分にも実装できないか、
と思いながら生活するようになっていました。

これが生体認証ログイン機能を実装するもう一つのきっかけとなりました。

実際に実装してみよう

…とまあ、今回のテーマを思いついたきっかけをつらつらと書いてきましたが、
さっそく、実装の方に移って聞きたいと思います。

ステップ1:自作アプリでの試行

まず、簡単なログインだけのアプリをAndroid/iOSともに作成して生体認証の成功 or 失敗 の判定を行うような簡易アプリ用意しました。

※ここの自作アプリの時点で技術的な話はほとんどしてしまいます

Android自作アプリでの生体認証

試しにAndroidでログインするだけのアプリを作って改造してみました。
KotlinでのAndroidアプリ開発において、生体認証でのログイン機能を実装するための詳細な手順を以下に示します。

↑自作Androidアプリの実際の動作

手が写っていてめちゃくちゃ恥ずかしいのですが、完成アプリの生体認証ログイン動画です。
Androidの生体認証(BiometricPrompt)の仕様で、スクリーンキャプチャをしても
セキュリティ対策による画面スクショのブロックで真っ黒な画面しか表示されなかったため、外部から他のデバイスで撮影した映像となります(古典的な方法で対応しました笑)

必要なライブラリの追加

まず、Androidアプリのプロジェクトを作成したら標準で存在するbuild.gradleに以下の依存関係を追加します。

dependencies {
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
}
※Androidの自作アプリをいじったのが、GWのタイミングだったのでその時点でのandroidx.biometricの最新バージョンが1.2.0-alpha05でした。

【参考】androidx.biometric公式ドキュメント

https://developer.android.com/jetpack/androidx/releases/biometric?hl=ja
ちなみに、8/5の勉強会時点では1.4.0-alpha01まで更新されていました
(意外と最近でも頻繁に更新されてる仕様なんですね)

パーミッション(権限の許可)の追加

次に、これまたAndroidアプリのプロジェクトを作成したら標準で存在するAndroidManifest.xmlに
パーミッション(権限の許可)の追加を行います。
manifestタグ直下に以下を追加します。

<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
なんと、、下準備はこれだけです。
以降はkotlinで適した処理を書いていきます。

端末が生体認証に対応しているかの確認処理

checkBiometric()というメソッドを作ります。
この関数で端末が生体認証できるかどうかを判定して
Boolean型で返します。
    // 端末が生体認証に対応しているか確認
private fun checkBiometric(): Boolean {
val biometricManager = BiometricManager.from(this)
when (biometricManager.canAuthenticate()) {
BiometricManager.BIOMETRIC_SUCCESS -> {
// 生体認証に対応している場合はここ
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
// 生体認証ハードウェアなし
}
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
// 生体認証ハードウェアは利用できないためもう一度お試しください的なところ
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
// 端末自体に生体認証の登録を1つもしていない場合はここ
}
else -> {
// その他のエラー
}
}
return biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
}
エラー内容をそれぞれのケースに合わせてダイアログで表示したりできます
例)
BIOMETRIC_ERROR_NONE_ENROLLED の場合に
「端末に生体認証情報が登録されていません。」などのダイアログ

認証後の処理の設定

生体認証が行われた後に行われる処理をこの中で書きます。

Succeeded、Failed、Errorでそれぞれ処理を書くことができます。
        val biometricPrompt = BiometricPrompt(this, executor,
object : BiometricPrompt.AuthenticationCallback() {

// 認証後の処理の設定

// 成功したらonAuthenticationSucceededに入ります
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.d("Biometric", "Succeeded")
runOnUiThread {
// 生体認証が成功した後の処理
// 例としてloginメソッドがある想定
login()
}
}
// 失敗したらonAuthenticationFailedに入ります
// 指紋は読み取れたけど未登録と判断したとき
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.d("Biometric", "Failed")
}
// 失敗したらonAuthenticationErrorに入ります
// どれにも当てはまらない復帰不可能なエラーを検出したとき
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.d("Biometric", "Error")
}
})
生体認証がうまく読み取れなかった際はBiometricPromptの標準の動きで
失敗の表示があるため基本的には基本的には失敗時はいじらなくて良さそうでした。

表示ダイアログの設定

最後に、指紋認証時に表示されるダイアログの文言などの設定をします。

ボタンクリック時に
biometricPrompt.authenticate(promptInfo)
の処理を実行することで呼び出せるようになっています
        // 表示ダイアログの設定
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("生体認証ログイン")
.setSubtitle("指紋を読み取ります")
.setDescription("端末に保存された指紋を認証して\nログイン情報を入力します")
.setNegativeButtonText("キャンセル")
.build()

val button =
findViewById<Button>(R.id.button)
button.setOnClickListener {
biometricPrompt.authenticate(promptInfo)
}
setTitle:ダイアログのタイトルの設定 (必須)
setSubtitle:詳しい説明とかに使えるテキスト
setNegativeButtonText:左下に表示するtext (必須)
setDescription:ダイアログの説明

他にも
setDeviceCredentialAllowed:別の認証方法を提案するかどうか
setConfirmationRequired:認証後明示的なユーザー確認をするかどうか
などの設定もできるそうでした

準備が整ったらあとは決まった処理を書くだけ

以下が、Android自作アプリの全体像です

package com.example.oharaauthapp

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.biometric.BiometricPrompt
import java.util.concurrent.Executors

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val mainTv = findViewById(R.id.main_tv)
val mainUser = findViewById(R.id.main_user)
val mainPass = findViewById(R.id.main_pass)

val loginBtn = findViewById<Button>(R.id.login_btn)
val authBtn = findViewById<Button>(R.id.auth_btn)
val executor = Executors.newSingleThreadExecutor()

val biometricPrompt = BiometricPrompt(this, executor,
object : BiometricPrompt.AuthenticationCallback() {

// 認証後の処理の設定

// 成功したらonAuthenticationSucceededに入ります
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.d("Biometric", "Succeeded")
runOnUiThread {
// 生体認証が成功した後の処理
mainUser.setText("ohara")
mainPass.setText("test123")

loginBtn.performClick()
}
}
// 失敗したらonAuthenticationFailedに入ります
// 指紋は読み取れたけど未登録と判断したとき
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.d("Biometric", "Failed")
}
// 失敗したらonAuthenticationErrorに入ります
// どれにも当てはまらない復帰不可能なエラーを検出したとき
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.d("Biometric", "Error")
}
})

// 表示ダイアログの設定

val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("生体認証ログイン")
.setSubtitle("指紋を読み取ります")
.setDescription("端末に保存された指紋を認証して\nログイン情報を入力します")
.setNegativeButtonText("キャンセル")
.build()

loginBtn.setOnClickListener {
if(TextUtils.isEmpty(mainUser.text) || TextUtils.isEmpty(mainPass.text)) {
mainTv.text = "ユーザー名とパスワードを入力してください。"
mainTv.visibility = View.VISIBLE
} else if(mainUser.text.toString() == "ohara" && mainPass.text.toString() == "test123") {
// SubActivityへの画面遷移
val intent = Intent(this, SubActivity::class.java)
intent.putExtra("UserName",mainUser.text.toString())
intent.putExtra("Pass",mainPass.text.toString())
startActivity(intent)
}else{
mainTv.text = "ユーザー名もしくはパスワードが間違っています"
mainTv.visibility = View.VISIBLE
}
}

authBtn.setOnClickListener {
checkBiometric()
// ここで生体認証ダイアログpromptInfoとその判定を呼び出しています
biometricPrompt.authenticate(promptInfo)
}

onBackPressedDispatcher.addCallback(callback)
}

// 端末が生体認証に対応しているか確認
private fun checkBiometric(): Boolean {
val biometricManager = BiometricManager.from(this)
when (biometricManager.canAuthenticate()) {
BiometricManager.BIOMETRIC_SUCCESS -> {
// 生体認証に対応している場合はここ
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
// 生体認証ハードウェアなし
// エラーダイアログなどのエラー処理
}
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
// 生体認証ハードウェアは利用できないためもう一度お試しください的なところ
// エラーダイアログなどのエラー処理
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
// 端末自体に生体認証の登録を1つもしていない場合はここ
// エラーダイアログなどのエラー処理
}
else -> {
// その他のエラー処理
}
}
return biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
}

private val callback = object : OnBackPressedCallback(true) {
//コールバックのhandleOnBackPressedを呼び出して、戻るキーを押したときの処理を記述
override fun handleOnBackPressed() {
//端末の戻るボタンでの画面遷移防止のためreturnだけでいい
return
}
}
}
とりあえずログインっぽい動きを実現させたかったので

ユーザ名:ohara
パスワード:test123

でしかログインできないように判定ベタ書きしています


iOS自作アプリでの生体認証

続いて、iOSでも自作のログインアプリを作成しました。
最近の開発で調べた知識の"Keychainを使用して端末にデータを登録する"という機能を利用することで、
ログインするだけのアプリから、ログインしたアカウントを登録するような作りにしてみました。

↑自作iOSアプリでの生体認証ログインはこんな感じに


info.plistに追加

画像の通りプロジェクトファイルのinfoから
"Privacy - Face ID Usage Description"というものを追加してそれっぽい理由を書きます

生体認証に必要な準備はこれだけ

info.plistに設定したらあとは"LocalAuthentication"のインポートと対応する処理を書くだけ。
import UIKit
import LocalAuthentication

class YourController: UIViewController {

@IBOutlet weak var authLoginButton: UIButton!

    ・・・基本処理を省略・・・

@IBAction func tapAuthLogin(_ sender: Any) {
authenticateUser()
}

func authenticateUser() {
let context = LAContext()
var error: NSError?

// Touch ID / Face IDが利用可能かどうかを確認
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
let reason = "Log in with Touch ID / Face ID"

context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
DispatchQueue.main.async {
if success {
// 認証成功
self.loginSuccess()
} else {
// 認証失敗
self.showAuthenticationError(error: authenticationError)
}
}
}
} else {
// 生体認証が利用できない場合の処理
showError(error: error)
}
}

func loginSuccess() {
// ログイン成功時の処理
// 例としてloginメソッドがある想定
login()
}

func showAuthenticationError(error: Error?) {
// 認証失敗時のエラーメッセージ表示
let alert = UIAlertController(title: "Authentication Failed", message: error?.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}

func showError(error: Error?) {
// 生体認証が利用できない場合のエラーメッセージ表示
let alert = UIAlertController(title: "Biometrics Unavailable", message: error?.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
あまりにも簡単すぎました、、

iOSでそのまま自社プロダクトにも取り入れてみます

大原のAndroidアプリ開発の経験が2年半ほど、
iOSアプリ開発の経験が9ヶ月ほどのためこの先は勉強がてら
iOSで自社プロダクトに生体認証ログインを取り入れてみます。

iOSのKeychainを利用すると、安全にログイン情報などのセキュアな値を保持することができて使い勝手が良さそうなので、自作アプリと同様にKeychainを利用して実装してみます。

ステップ2:自社サービス「SUBLINE」での実装

次に、自社サービス「SUBLINE」に生体認証ログイン機能を実装しました。
このサービスでは、ログイン中に今ログインしているアカウントを生体認証ログインアカウントに登録する or 解除するという機能もつけてを実装しました。

↑SUBLINEでの生体認証ログイン(アカウント登録・解除)

ログイン中に生体認証ログインでログインできるアカウントに登録する処理をKeychainを利用することで実現させました。

登録と同様の手順で解除することもでき、別のアカウントでログインして登録する場合は生体認証ログインのアカウントが上書きされます。

ステップ3:自社サービス「サスケWorks」での実装

最後に、自社サービス「サスケWorks」に生体認証ログイン機能を実装しました。
モバイル版のサスケWorksでは、複数アカウントでのログインが可能なため、
配列でアカウントリストを作成し、生体認証ログイン対象のアカウントを複数登録することを実現しました。

↑サスケWorksでの生体認証ログイン(複数アカウント登録)

一度でもログインしたことのあるアカウント情報を端末内のKeychainに保存して、
表示されたログインIDを選択することで好きなアカウントでログインできるようにしました。

まとめ

今回行った生体認証ログイン機能の実装は、意外と調べてみると技術的にはそれほど難しくないものでした。
なので、次回はもっと難易度の高い内容に挑戦してみたいと思います。

現時点では大原の技術的にモバイルアプリ側の開発しかできないので、
次回はバックエンドのAPIの実装と絡めた機能も作ってみたいなぁ。。
と考えたりしました。

今後も、自社のサービスをより便利に多くの人に使ってもらえるよう開発ガシガシ頑張っていきたいと思います〜!

ここまで読んでいただき、ありがとうございました!
記事一覧へ

Member

システム部開発ユニット
システム部SIユニット
クリエイティブ戦略部デザインユニット
クリエイティブ戦略部プランニングユニット
管理部