This repository contains the example app demonstrating how to use the Nevis Mobile Authentication SDK in an Android mobile application. The Nevis Mobile Authentication SDK allows you to integrate passwordless authentication to your existing mobile app, backed by the FIDO UAF 1.1 Standard.
Some SDK features demonstrated in this example app are:
- Using the SDK with the Nevis Authentication Cloud
- Registering with QR code & app link URIs
- Simulating in-band authentication after registration
- Deregistering a registered account
- Changing the PIN of the PIN authenticator
- Changing the device information
Please note that the example app only demonstrates a subset of the available SDK features. The main purpose is to demonstrate how the SDK can be used, not to cover all supported scenarios.
Before you start compiling and using the example applications please ensure you have the following ready:
- In case you are planning to use Authentication Cloud you need an Authentication Cloud instance provided by Nevis and an access key to use it.
- In case you are planning to use an Identity Suite environment ensure that the environment is up and running and you have all necessary URLs and permissions to access it.
- Android 6 or later, with API level 23
- Android 10 or later, with API level 29, for the biometric authenticator to work
- Android 11 or later, with API level 30, for the device passcode authenticator to work
- JDK 17
- Gradle 8.7 or later
SDK dependency used by this project are provided via GitHub Packages that is used as a Maven repository. To access GitHub Packages a valid GitHub account and a Personal Access Token is needed. If you have not done it yet, please create a Personal Access Token with Packages Read permission. Once the Personal Access Token is created add the following properties to your global gradle.properties
file (e.g.: /Users/<YOUR USERNAME>/.gradle/gradle.properties
).
GH_USERNAME=<YOUR USERNAME>
GH_PERSONAL_ACCESS_TOKEN=<YOUR PERSONAL ACCESS TOKEN>
Clone the example application GitHub repository and open it with Android Studio.
The example applications support two kinds of configuration: authenticationCloud
and identitySuite
. The following chapters describes how to change the base configuration to match your environment.
The configuration could be changed by modifying the ApplicationModule file which describes the dependency injection related configuration using the Dagger Hilt library and the AndroidManifest.xml.
Note
Only build-time configuration change is supported.
Before being able to use the example application with your Authentication Cloud instance, you will need to update the configuration with the right host information.
Edit the ApplicationModule file and replace the host name information with your Authentication Cloud instance in method provideAuthenticationCloudConfiguration
.
If you want to use identitySuite
environment modify the configuration in method provideIdentitySuiteConfiguration
and modify this part:
@Provides
@Singleton
fun provideConfigurationProvider(application: Application): ConfigurationProvider =
ConfigurationProviderImpl(
Environment.AUTHENTICATION_CLOUD,
provideAuthenticationCloudConfiguration(application)
)
to
@Provides
@Singleton
fun provideConfigurationProvider(application: Application): ConfigurationProvider =
ConfigurationProviderImpl(
Environment.IDENTITY_SUITE,
provideIdentitySuiteConfiguration(application)
)
The example applications handle deep links those contain a valid dispatchTokenResponse
query parameter of an out-of-band operation. The related configuration located in the AndroidManifest.xml for MainActivity with action android.intent.action.VIEW
.
Change the myaccessapp
scheme value in the following intent-filter
with the right scheme information of your environment.
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myaccessapp" />
</intent-filter>
Note
For more information about deep links, visit the official Android guide.
The FIDO server (i.e. nevisFIDO) must be configured with the facet ID(s) of your application(s). If the facet ID of your application is not referenced by the nevisFIDO configuration, the operations will fail with an UNTRUSTED_FACET_ID error.
By default the SDK assumes that the facet ID to be used is the one that follows the FIDO UAF 1.1 Specifications that is the facet ID on Android should follow the android:apk-key-hash:HASH_VALUE
format, where the HASH_VALUE
is Base64 encoded SHA-256 hash of the APK signing certificate.
The facet ID can be calculated using the following code snippet:
import android.content.pm.PackageInfo
import android.util.Base64
import java.io.ByteArrayInputStream
import java.security.MessageDigest
import java.security.cert.CertificateFactory
object FacetIdCalculator {
fun calculateFacetId(packageInfo: PackageInfo): String {
val byteArrayInputStream = ByteArrayInputStream(packageInfo.signingInfo.signingCertificateHistory[0].toByteArray())
val certificate = CertificateFactory.getInstance("X509").generateCertificate(byteArrayInputStream)
val digest = MessageDigest.getInstance("SHA-256")
return "android:apk-key-hash" + Base64.encodeToString(digest.digest(certificate.encoded), Base64.NO_PADDING or Base64.NO_WRAP)
}
}
Note
An alternative way to set a constant Facet ID is to call facetId(String facetId)
method of ch.nevis.mobile.sdk.api.Configuration.Builder
in methods provideAuthenticationCloudConfiguration
and provideIdentitySuiteConfiguration
in ApplicationModule file.
The value of the facet ID depends on the certificate used to build the application, which can change during the development, that is why this method has been introduced: by providing a constant facet ID and having it referenced in the server server configuration, temporary changes in the APK signing certificate do not require changes in the backend.
This method must be used for development scenarios only. For production code do not invoke this method and configure the backend with the facet ID that can be calculated with the code snippet above. See the chapter Application Facet ID and the nevisFIDO Backend Configuration of the SDK reference guide for more details.
Now you are ready to build and run the example app by choosing Run > Run 'app' from Android Studio's menu or by clicking the Run button in your project’s toolbar.
If you want to build and install the application from the command-line from *nix systems, you can execute the following:
$ cd <example repository root>
$ ./gradlew installDebug
It will build the application and install it if there is an emulator running in your machine, or if a physical device with debug enabled is connected to your machine.
Now that the Android example app is up and running, it's time to try it out!
Check the Using the Example Application of our quickstart guide for usage instructions.
The navigation between the views/fragments are handled and described using the Android Jetpack Navigation component. The Gradle plugin Safe Args is also used to pass data to navigation destination fragments with type safety.
Dependency Injection framework Dagger Hilt is used to inject an SDK instance and logger, view model, operation and delegate instances where necessary.
As this is the suggested and supported application architecture, MVVM is used. Each view is implemented as a androidx.fragment.app.Fragment
and each view has a androidx.lifecycle.ViewModel
implementation if necessary.
The application is written in Kotlin.
SDK operations are executed in async way using Kotlin coroutines, they are wrapped by and executed in suspend
functions.
Nevis Mobile Authentication SDK Android library is a Java library. After a successful MobileAuthenticationClient
initialization (for more details see next chapter) the initialized client object can be obtained from ClientProvider
and an operation can be executed by calling the proper method of the MobileAuthenticationClient
and passing them the necessary input parameters. In the example application the default implementation of ClientProvider
interface is injected by Dagger into the use-case implementations.
E.g.: the next code snippet shows how to start an in-band authentication operation where
username
identifies the account we want to use for in-band authentication,authenticatorSelector
is an implementation ofch.nevis.mobile.sdk.api.operation.selection.AuthenticatorSelector
interaction callback interface that will be called by the SDK when authenticator selection is required to continue the operation,pinUserVerifier
is an implementation ofch.nevis.mobile.sdk.api.operation.userverification.PinUserVerifier
interaction callback interface that will be called by the SDK when PIN verification is required to continue the operation,fingerprintUserVerifier
is an implementation ofch.nevis.mobile.sdk.api.operation.userverification.FingerprintUserVerifier
interaction callback interface that will be called by the SDK when fingerprint verification is required to continue the operation,biometricUserVerifier
is an implementation ofch.nevis.mobile.sdk.api.operation.userverification.BiometricUserVerifier
interaction callback interface that will be called by the SDK when biometric verification is required to continue the operation,onSuccess
is an implementation ofch.nevis.mobile.sdk.api.util.Consumer
interaction callback interface that will be called by the SDK when the operation successfully completed,onError
is an implementation ofch.nevis.mobile.sdk.api.util.Consumer
interaction callback interface that will be called by the SDK when the operation stopped because an error occurred.
val client = clientProvider.get()
client?.operations().authentication()
.username(username)
.authenticatorSelector(authenticatorSelector)
.pinUserVerifier(pinUserVerifier)
.fingerprintUserVerifier(fingerprintUserVerifier)
.biometricUserVerifier(biometricUserVerifier)
.onSuccess(onSuccess)
.onError(onError)
.execute()
Because the SDK uses callback concept if we ran the code above in a coroutine scope then the process cannot be suspended, the call will return immediately and later a callback instance will be called by the SDK outside the coroutine scope and suspend function. For Kotlin coroutines, suspend functions, SDK calls have to be wrapped into a suspend cancellable coroutine.
The example application wraps the SDK calls in the use-case implementations in their suspend fun execute(...)
functions. It runs the SDK call inside a suspendCancellableCoroutine
block and caches the received cancellableContinuation
object. When a interaction callback is called by the SDK the cancellableContinuation
will be get from the cache and it will be resumed to resume the suspended block/function.
As an example check the InBandAuthenticationUseCaseImpl and the AuthenticatorSelectorImpl classes.
The source code is documented using KDoc syntax. Dokka is an API documentation engine for Kotlin. Documentation can be generated by running:
./gradlew dokkaHtml dokkaHtmlMultiModule
The output can be found in the build/dokka/htmlMultiModule
folder.
In this section you can find hints about how the Nevis Mobile Authentication SDK is integrated into the example app.
- All SDK invocation is implemented as use-cases in the usecase package.
- All SDK specific user interaction related protocol implementation can be found in the handler package.
The InitializeClientUseCaseImpl class is responsible for creating and initializing a MobileAuthenticationClient
instance which is the entry point to the SDK. Later this instance can be used to start the different operations. The initialized MobileAuthenticationClient
instance can be accessed via ClientProviderImpl.
Before being able to authenticate using the Nevis Mobile Authentication SDK, go through the registration process. Depending on the use-case, there are two types of registration: in-app registration and out-of-band registration.
If the application is using a backend using the Nevis Authentication Cloud, the AuthCloudApiRegistrationUseCaseImpl class will be used by passing the enrollment
response or an appLinkUri
.
When the backend used by the application does not use the Nevis Authentication Cloud the name of the user to be registered is passed to the InBandRegistrationUseCaseImpl class.
If authorization is required by the backend to register, provide an AuthorizationProvider
. In the example app a CookieAuthorizationProvider
is created from the cookies (see LegacyLoginViewModel obtained by the LoginUseCaseImpl class.
When the registration is initiated in another device or application, the information required to process the operation is transmitted through a QR code or a link. After the payload obtained from the QR code or the link is decoded by DecodePayloadUseCaseImpl the ProcessOutOfBandPayloadUseCaseImpl class processes the OutOfBandPayload
starts the out-of-band operation.
Using the authentication operation, you can verify the identity of the user using an already registered authenticator. Depending on the use-case, there are two types of authentication: in-app authentication and out-of-band authentication.
For the application to trigger the authentication, the name of the user is provided to the InBandAuthenticationUseCaseImpl class.
When the authentication is initiated in another device or application, the information required to process the operation is transmitted through a QR code or a link. After the payload obtained from the QR code or the link is decoded by DecodePayloadUseCaseImpl the ProcessOutOfBandPayloadUseCaseImpl class starts the out-of-band operation.
There are cases when specific information is to be presented to the user during the user verification process, known as transaction confirmation. The AuthenticatorSelectionContext
and the AccountSelectionContext
contain a byte array with the information. In the example app it is handled in the AccountSelectorImpl class.
The DeregisterUseCaseImpl class is responsible for deregistering either a user or all of the registered users from the device.
The change PIN operation is implemented in the StartChangePinUseCaseImpl and ChangePinUseCaseImpl classes with which you can modify the PIN of a registered PIN authenticator for a given user.
The change Password operation is implemented in the StartChangePasswordUseCaseImpl and ChangePasswordUseCaseImpl classes with which you can modify the password of a registered Password authenticator for a given user.
During registration, the device information can be provided that contains the name identifying your device, and also the Firebase Cloud Messaging registration token. Updating both the name and the token is implemented in the ChangeDeviceInformationUseCaseImpl class.
The following use cases are responsible for getting information with the help of LocalData
:
- The GetAccountsUseCaseImpl class obtains the registered accounts.
- The GetAuthenticatorsUseCaseImpl class obtains the authenticator information.
- The GetDeviceInformationUseCaseImpl class obtains the device information.
© 2024 made with ❤ by Nevis