Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

For #12289 Add lib-auth for authentication using biometrics or PIN. #12291

Merged
merged 13 commits into from
Jun 23, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .buildconfig.yml
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,10 @@ projects:
path: components/support/webextensions
description: 'A component containing building blocks for features implemented as web extensions.'
publish: true
lib-auth:
path: components/lib/auth
description: 'A component for various kinds of authentication mechanisms.'
publish: true
lib-crash:
path: components/lib/crash
description: 'A generic crash reporter library that can report crashes to multiple services.'
Expand Down
35 changes: 35 additions & 0 deletions components/lib/auth/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
compileSdkVersion config.compileSdkVersion

defaultConfig {
minSdkVersion config.minSdkVersion
targetSdkVersion config.targetSdkVersion
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation project(':support-base')
implementation Dependencies.androidx_biometric

testImplementation project(':support-test')
testImplementation Dependencies.androidx_test_core
testImplementation Dependencies.androidx_test_junit
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
}
apply from: '../../../publish.gradle'
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
21 changes: 21 additions & 0 deletions components/lib/auth/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
4 changes: 4 additions & 0 deletions components/lib/auth/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="mozilla.components.lib.auth">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.lib.auth

/**
* Callbacks for BiometricPrompt Authentication
*/
interface AuthenticationDelegate {

/**
* Called when a biometric (e.g. fingerprint, face, etc.)
* is presented but not recognized as belonging to the user.
*/
fun onAuthFailure()

/**
* Called when a biometric (e.g. fingerprint, face, etc.) is recognized,
* indicating that the user has successfully authenticated.
*/
fun onAuthSuccess()

/**
* Called when an unrecoverable error has been encountered and authentication has stopped.
* @param errorText A human-readable error string that can be shown on an UI
*/
fun onAuthError(errorText: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.lib.auth

import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.base.log.logger.Logger

/**
* A [LifecycleAwareFeature] for the Android Biometric API to prompt for user authentication.
* The prompt also requests support for the device PIN as a fallback authentication mechanism.
*
* @param context Android context.
* @param fragment The fragment on which this feature will live.
* @param authenticationDelegate Callbacks for BiometricPrompt.
*/
class BiometricPromptAuth(
private val context: Context,
private val fragment: Fragment,
private val authenticationDelegate: AuthenticationDelegate
) : LifecycleAwareFeature {
private val logger = Logger(javaClass.simpleName)

@VisibleForTesting
internal var biometricPrompt: BiometricPrompt? = null

override fun start() {
val executor = ContextCompat.getMainExecutor(context)
biometricPrompt = BiometricPrompt(fragment, executor, PromptCallback())
}

override fun stop() {
biometricPrompt = null
}

/**
* Requests the user for biometric authentication.
*
* @param title Adds a title for the authentication prompt.
* @param subtitle Adds a subtitle for the authentication prompt.
*/
fun requestAuthentication(
title: String,
subtitle: String = ""
) {
val promptInfo: BiometricPrompt.PromptInfo = BiometricPrompt.PromptInfo.Builder()
.setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
.setTitle(title)
.setSubtitle(subtitle)
.build()
biometricPrompt?.authenticate(promptInfo)
}

internal inner class PromptCallback : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
logger.error("onAuthenticationError: errorMessage $errString errorCode=$errorCode")
authenticationDelegate.onAuthError(errString.toString())
}

override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
logger.debug("onAuthenticationSucceeded")
authenticationDelegate.onAuthSuccess()
}

override fun onAuthenticationFailed() {
logger.error("onAuthenticationFailed")
authenticationDelegate.onAuthFailure()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.lib.auth

import android.content.Context
import android.os.Build
import androidx.biometric.BiometricManager

/**
* Utility class for BiometricPromptAuth
*/

fun Context.canUseBiometricFeature(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val manager = BiometricManager.from(this)
return BiometricUtils.canUseFeature(manager)
} else {
false
}
}

internal object BiometricUtils {

/**
* Checks if the appropriate SDK version and hardware capabilities are met to use the feature.
*/
internal fun canUseFeature(manager: BiometricManager): Boolean {
return isHardwareAvailable(manager) && isEnrolled(manager)
}

/**
* Checks if the hardware requirements are met for using the [BiometricManager].
*/
internal fun isHardwareAvailable(biometricManager: BiometricManager): Boolean {
val status =
biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
return status != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE &&
status != BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
}

/**
* Checks if the user can use the [BiometricManager] and is therefore enrolled.
*/
internal fun isEnrolled(biometricManager: BiometricManager): Boolean {
val status =
biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
return status == BiometricManager.BIOMETRIC_SUCCESS
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.lib.auth

import androidx.biometric.BiometricPrompt
import androidx.fragment.app.Fragment
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.createAddedTestFragment
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class BiometricPromptAuthTest {

private lateinit var biometricPromptAuth: BiometricPromptAuth
private lateinit var fragment: Fragment

@Before
fun setup() {
fragment = createAddedTestFragment { Fragment() }
biometricPromptAuth = BiometricPromptAuth(
testContext,
fragment,
object : AuthenticationDelegate {
override fun onAuthFailure() {
}

override fun onAuthSuccess() {
}

override fun onAuthError(errorText: String) {
}
}
)
}

@Test
fun `prompt is created and destroyed on start and stop`() {
assertNull(biometricPromptAuth.biometricPrompt)

biometricPromptAuth.start()

assertNotNull(biometricPromptAuth.biometricPrompt)

biometricPromptAuth.stop()

assertNull(biometricPromptAuth.biometricPrompt)
}

@Test
fun `requestAuthentication invokes biometric prompt`() {
val prompt: BiometricPrompt = mock()

biometricPromptAuth.biometricPrompt = prompt

biometricPromptAuth.requestAuthentication("title", "subtitle")

verify(prompt).authenticate(any())
}

@Test
fun `promptCallback fires feature callbacks`() {
val authenticationDelegate: AuthenticationDelegate = mock()
val feature = BiometricPromptAuth(testContext, fragment, authenticationDelegate)
val callback = feature.PromptCallback()
val prompt = BiometricPrompt(fragment, callback)

feature.biometricPrompt = prompt

callback.onAuthenticationError(BiometricPrompt.ERROR_CANCELED, "")

verify(authenticationDelegate).onAuthError("")

callback.onAuthenticationFailed()

verify(authenticationDelegate).onAuthFailure()

callback.onAuthenticationSucceeded(mock())

verify(authenticationDelegate).onAuthSuccess()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.lib.auth

import android.os.Build
import androidx.biometric.BiometricManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.whenever
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config

@RunWith(AndroidJUnit4::class)
class BiometricUtilsTest {

@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
@Test
fun `canUseFeature checks for SDK compatible`() {
assertFalse(testContext.canUseBiometricFeature())
}

@Test
fun `isHardwareAvailable is true based on AuthenticationStatus`() {
val manager: BiometricManager = mock {
whenever(canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS)
.thenReturn(BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE)
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE)
}

assertTrue(BiometricUtils.isHardwareAvailable(manager))
assertFalse(BiometricUtils.isHardwareAvailable(manager))
assertFalse(BiometricUtils.isHardwareAvailable(manager))
}

@Test
fun `isEnrolled is true based on AuthenticationStatus`() {
val manager: BiometricManager = mock {
whenever(canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS)
}
assertTrue(BiometricUtils.isEnrolled(manager))
}
}
5 changes: 5 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ permalink: /changelog/
* **support-rusterrors**
* 🆕 New component to report Rust errors

* **lib-auth**
* Added new `lib-auth` component for various forms of authentication.
* Adds a new `BiometricPromptAuth` for authenticating with biometrics or PIN.
[issue # 12289](https://github.com/mozilla-mobile/android-components/issues/12289)

# 102.0.0
* [Commits](https://github.com/mozilla-mobile/android-components/compare/v101.0.0...v102.0.1)
* [Milestone](https://github.com/mozilla-mobile/android-components/milestone/149?closed=1)
Expand Down
1 change: 1 addition & 0 deletions taskcluster/ci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ treeherder:
feature-webcompat: feature-webcompat
feature-webnotifications: feature-webnotifications
lib-crash: lib-crash
lib-auth: lib-auth
lib-crash-sentry: lib-crash-sentry
lib-crash-sentry-legacy: lib-crash-sentry-legacy
lib-dataprotect: lib-dataprotect
Expand Down