diff options
Diffstat (limited to 'mobile/android/android-components/components/feature/webauthn')
8 files changed, 281 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/feature/webauthn/README.md b/mobile/android/android-components/components/feature/webauthn/README.md new file mode 100644 index 0000000000..ba8f1c8c66 --- /dev/null +++ b/mobile/android/android-components/components/feature/webauthn/README.md @@ -0,0 +1,59 @@ +# [Android Components](../../../README.md) > Feature > WebAuthn + +A feature that provides WebAuthn functionality for supported engines. + +## Usage + +Add the feature to the Activity/Fragment: + +```kotlin +val webAuthnFeature = WebAuthnFeature( + engine = GeckoEngine, + activity = requireActivity() +) +``` + +**Note:** If the feature is on the fragment, ensure that `onActivityResult` calls from the activity are forwarded to the fragment. + +Allow the feature to consume the `onActivityResult` data: + +```kotlin +override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + webAuthFeature.onActiviyResult(requestCode, data, resultCode) +} +``` + +As with other features in Android Components, `WebAuthnFeature` implements `LifecycleAwareFeature`, so it's recommended to use `ViewBoundFeatureWrapper` to handle the lifecycle events of the feature: + +```kotlin +private val webAuthnFeature = ViewBoundFeatureWrapper<WebAuthnFeature>() + +override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + webAuthnFeature.set( + feature = WebAuthnFeature( + engine = GeckoEngine, + activity = requireActivity() + ), + owner = this, + view = view + ) +} + +override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + webAuthnFeature.onActivityResult(requestCode, data, resultCode) } +} +``` + +### Setting up the dependency + +Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)): + +```Groovy +implementation "org.mozilla.components:feature-webauthn:{latest-version}" +``` + +## License + + 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/ diff --git a/mobile/android/android-components/components/feature/webauthn/build.gradle b/mobile/android/android-components/components/feature/webauthn/build.gradle new file mode 100644 index 0000000000..519b717dcf --- /dev/null +++ b/mobile/android/android-components/components/feature/webauthn/build.gradle @@ -0,0 +1,36 @@ +/* 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 { + defaultConfig { + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + namespace 'mozilla.components.feature.webauthn' +} + +dependencies { + implementation project(':concept-engine') + + testImplementation project(':support-test') + + testImplementation ComponentsDependencies.androidx_test_core + testImplementation ComponentsDependencies.androidx_test_junit +} + +apply from: '../../../android-lint.gradle' +apply from: '../../../publish.gradle' +ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/mobile/android/android-components/components/feature/webauthn/proguard-rules.pro b/mobile/android/android-components/components/feature/webauthn/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/components/feature/webauthn/proguard-rules.pro @@ -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 diff --git a/mobile/android/android-components/components/feature/webauthn/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/webauthn/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e16cda1d34 --- /dev/null +++ b/mobile/android/android-components/components/feature/webauthn/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ +<!-- 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/. --> +<manifest /> diff --git a/mobile/android/android-components/components/feature/webauthn/src/main/java/mozilla/components/feature/webauthn/WebAuthnFeature.kt b/mobile/android/android-components/components/feature/webauthn/src/main/java/mozilla/components/feature/webauthn/WebAuthnFeature.kt new file mode 100644 index 0000000000..c896421af5 --- /dev/null +++ b/mobile/android/android-components/components/feature/webauthn/src/main/java/mozilla/components/feature/webauthn/WebAuthnFeature.kt @@ -0,0 +1,66 @@ +/* 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.feature.webauthn + +import android.app.Activity +import android.content.Intent +import android.content.IntentSender +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.activity.ActivityDelegate +import mozilla.components.support.base.feature.ActivityResultHandler +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.base.log.logger.Logger + +/** + * A feature that implementing the [ActivityDelegate] to adds support + * for [WebAuthn](https://tools.ietf.org/html/rfc8809). + */ +class WebAuthnFeature( + private val engine: Engine, + private val activity: Activity, + private val exitFullScreen: (String?) -> Unit, + private val currentTab: () -> String?, +) : LifecycleAwareFeature, ActivityResultHandler, ActivityDelegate { + private val logger = Logger("WebAuthnFeature") + private var requestCodeCounter = ACTIVITY_REQUEST_CODE + private var callbackRef: ((Intent?) -> Unit)? = null + + override fun start() { + engine.registerActivityDelegate(this) + } + + override fun stop() { + engine.unregisterActivityDelegate() + } + + override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean { + logger.info( + "Received activity result with " + + "code: $requestCode " + + "and original request code: $requestCodeCounter", + ) + + if (requestCode != requestCodeCounter) { + return false + } + + requestCodeCounter++ + + callbackRef?.invoke(data) + + return true + } + + override fun startIntentSenderForResult(intent: IntentSender, onResult: (Intent?) -> Unit) { + logger.info("Received activity delegate request with code: $requestCodeCounter") + exitFullScreen(currentTab()) + activity.startIntentSenderForResult(intent, requestCodeCounter, null, 0, 0, 0) + callbackRef = onResult + } + + companion object { + const val ACTIVITY_REQUEST_CODE = 10 + } +} diff --git a/mobile/android/android-components/components/feature/webauthn/src/test/java/mozilla/components/feature/webauthn/WebAuthnFeatureTest.kt b/mobile/android/android-components/components/feature/webauthn/src/test/java/mozilla/components/feature/webauthn/WebAuthnFeatureTest.kt new file mode 100644 index 0000000000..1714d44c17 --- /dev/null +++ b/mobile/android/android-components/components/feature/webauthn/src/test/java/mozilla/components/feature/webauthn/WebAuthnFeatureTest.kt @@ -0,0 +1,92 @@ +/* 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.feature.webauthn + +import android.app.Activity +import android.content.Intent +import android.content.IntentSender +import mozilla.components.concept.engine.Engine +import mozilla.components.feature.webauthn.WebAuthnFeature.Companion.ACTIVITY_REQUEST_CODE +import mozilla.components.support.test.eq +import mozilla.components.support.test.mock +import mozilla.components.support.test.nullable +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.verify + +class WebAuthnFeatureTest { + private lateinit var engine: Engine + private lateinit var activity: Activity + private val exitFullScreen: (String?) -> Unit = { _ -> exitFullScreenUseCaseCalled = true } + private var exitFullScreenUseCaseCalled = false + + @Before + fun setup() { + engine = mock() + activity = mock() + } + + @Test + fun `feature registers itself on start`() { + val feature = webAuthnFeature() + + feature.start() + + verify(engine).registerActivityDelegate(feature) + } + + @Test + fun `feature unregisters itself on stop`() { + val feature = webAuthnFeature() + + feature.stop() + + verify(engine).unregisterActivityDelegate() + } + + @Test + fun `activity delegate starts intent sender`() { + val feature = webAuthnFeature() + val callback: ((Intent?) -> Unit) = { } + val intentSender: IntentSender = mock() + + feature.startIntentSenderForResult(intentSender, callback) + + verify(activity).startIntentSenderForResult(eq(intentSender), anyInt(), nullable(), eq(0), eq(0), eq(0)) + } + + @Test + fun `callback is invoked`() { + val feature = webAuthnFeature() + var callbackInvoked = false + val callback: ((Intent?) -> Unit) = { callbackInvoked = true } + val intentSender: IntentSender = mock() + + feature.onActivityResult(ACTIVITY_REQUEST_CODE, Intent(), 0) + + assertFalse(callbackInvoked) + + feature.startIntentSenderForResult(intentSender, callback) + feature.onActivityResult(ACTIVITY_REQUEST_CODE + 1, Intent(), 0) + + assertTrue(callbackInvoked) + } + + @Test + fun `feature won't process results with the wrong request code`() { + val feature = webAuthnFeature() + + val result = feature.onActivityResult(ACTIVITY_REQUEST_CODE - 5, Intent(), 0) + + assertFalse(result) + } + + private fun webAuthnFeature(): WebAuthnFeature { + return WebAuthnFeature(engine, activity, { exitFullScreen("") }) { "" } + } +} diff --git a/mobile/android/android-components/components/feature/webauthn/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/webauthn/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..cf1c399ea8 --- /dev/null +++ b/mobile/android/android-components/components/feature/webauthn/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1,2 @@ +mock-maker-inline +// This allows mocking final classes (classes are final by default in Kotlin) diff --git a/mobile/android/android-components/components/feature/webauthn/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/webauthn/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..932b01b9eb --- /dev/null +++ b/mobile/android/android-components/components/feature/webauthn/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=28 |