Learn how to build an Android application that can claim an mDoc via OID4VCI
Introduction
In this tutorial we will use the Android mDoc holder SDK to build an Android application that can claim an mDoc issued via an OID4VCI workflow:
- The user launches the application and scans a QR code received from an issuer.
- The application displays what credential is being offered to the user and by what issuer.
- The user agrees to claiming the offered credential.
- The user is redirected to complete authentication.
- Upon successful authentication, the credential is issued to the userโs application, where they can now store, view and present it.
The result will look something like this:
Prerequisites
Before we get started, letโs make sure you have everything you need.
Prior knowledge
-
The issuance workflow described in this tutorial is based on the OID4VCI specification. If you are unfamiliar with this specification, refer to our Docs section for more information:
- What is credential issuance?
- Breakdown of the OID4VCI workflow.
- What are mDocs?
-
We assume you have experience developing Android apps in Kotlin.
If you need to get a holding solution up and running quickly with minimal development resources and in-house domain expertise, talk to us about our white-label MATTR GO Hold app which might be a good fit for you.
Assets
- As part of your onboarding process you should have been provided with access to the following
assets:
- ZIP file which includes the required library (
holder-*version*.zip
). - Sample Holder app: You can use this app for reference as we work through this tutorial.
- ZIP file which includes the required library (
This tutorial is only meant to be used with the most recent version of the Android mDocs Holder SDK.
Development environment
Testing device
- Supported Android device to run the built application on, setup with:
- Biometric authentication (Face recognition, fingerprint recognition).
- Available internet connection.
- Debugging enabled.
Got everything? Letโs get going!
Environment setup
Perform the following steps to setup and configure your development environment:
Create a new project
- Create a new Android Studio project, using the Empty Activity template.
- Name the project
Holder Tutorial
. - Select API 24 as the
Minimum SDK
version. - Select Kotlin DSL as the
Build configuration language
.
- Sync the project with Gradle files.
Add required dependencies
-
Select the Project view.
-
Create a new directory named
repo
in your projectโs folder. -
Unzip the
holder-*version*.zip
file and copy the unzippedglobal
folder into the newrepo
folder. -
Open the
settings.gradle.kts
file in theHolderTutorial
folder and add the following Maven repository to thedependencyResolutionManagement.repositories
block:settings.gradle.ktsmaven { url = uri("repo") }
-
Open the
app/build.gradle.kts
file in yourapp
folder and add the following dependencies to thedependencies
block:app/build.gradle.ktsimplementation("global.mattr.mobilecredential:holder:1.0.0") implementation("androidx.navigation:navigation-compose:2.8.4")
The required navigation-compose
version may differ based on your version of the IDE, Gradle,
and other project dependencies.
-
In the same
app/build.gradle.kts
file, add the following values to thedefaultConfig
block. These represent the Authentication provider we will use for this tutorial:app/build.gradle.ktsmanifestPlaceholders["mattrScheme"] = "io.mattrlabs.sample.mobilecredentialtutorialholderapp" manifestPlaceholders["mattrDomain"] = "credentials"
These values are part of the
redirect URI
the SDK will redirect the user to once they complete Authentication with the issuer:mattrScheme
: Thescheme
can be any path that is handled by your application and registered with the issuer.mattrDomain
: Thedomain
can be any path, but our best practice recommendation is to configure this to becredentials
, as the standard format for theredirect URI
is{redirect.scheme}://credentials/callback
.
These values (alongside the client ID, which will be discussed later) must be registered as part
of the issuerโs OID4VCI workflow redirect URI
. For this tutorial you will be claiming a
credential from a MATTR Labs issuer which is already configured with the parameters detailed
above. We will help you configure your unique values as you move your implementation into
production.
-
Sync the project with Gradle files.
-
Open the Build tab and select
Sync
to make sure that the project has synced successfully.
Run the application
-
Connect a debuggable Android mobile device to your machine.
-
Build and run the app on the connected mobile device.
The app should launch with a โHello, Android!โ text displayed:
Nice work, your application is now all set to begin using the SDK!
Tutorial steps
To enable a user to interact with an OID4VCI Credential offer and claim an mDoc, you will build the following capabilities into your application:
- Initialize the SDK.
- Interact with a Credential offer.
- Retrieve offer details and present them to the Holder.
- Obtain user consent and initiate Credential issuance.
Initialize the SDK
The first capability you will build into your app is to initialize the SDK so that your app can use
SDK functions and classes. To achieve this, we need to initialize the MobileCredentialHolder
class:
-
Open the
MainActivity
file in your project and replace any existing code with the following:MainActivity.ktpackage com.example.holdertutorial import android.app.Activity import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.example.holdertutorial.ui.theme.HolderTutorialTheme import global.mattr.mobilecredential.common.dto.MobileCredential import global.mattr.mobilecredential.holder.MobileCredentialHolder import global.mattr.mobilecredential.holder.ProximityPresentationSession import global.mattr.mobilecredential.holder.issuance.dto.DiscoveredCredentialOffer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Claim Credential - Step 1.2: Initialize the SDK setContent { HolderTutorialTheme { val navController = rememberNavController() NavHost( modifier = Modifier .fillMaxSize() .padding(8.dp), startDestination = "home", navController = navController, ) { composable("home") { HomeScreen(this@MainActivity, navController) } composable("scanOffer") { // Claim Credential - Step 2.5: Add "Scan Offer" screen call } composable("retrievedCredential") { // Claim Credential - Step 4.8: Add "Retrieved Credential" screen call } composable("presentationQr") { // Proximity Presentation - Step 1.2: Add "QR Presentation" screen call } composable("presentationSelectCredentials") { // Proximity Presentation - Step 2.6: Add "Select Credential" screen call } } } } } } @Composable fun HomeScreen(activity: Activity, navController: NavController) { val coroutineScope = rememberCoroutineScope() Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Button(onClick = { navController.navigate("scanOffer") }, Modifier.fillMaxWidth()) { Text("Scan Credential Offer") } // Proximity Presentation - Step 1.3: Add button for starting the credentials presentation workflow // Claim Credential - Step 3.3: Display discovered credential offer } } // Claim Credential - Step 4.3: Create function to retrieve credentials // Claim Credential - Step 4.2: Add OpenID4VCI constants object object SharedData { // Claim Credential - Step 3.1: Add discovered credential offer variable // Claim Credential - Step 4.1: Add retrieved credentials variable // Proximity Presentation - Step 2.1: Add proximity presentation request variable }
This will serve as the basic structure for your application. We will copy and paste different code snippets into specific locations in this codebase to achieve the different functionalities. These locations are indicated by comments that reference both the section and the step.
We recommend copying and pasting the comment text (e.g. // Claim Credential - Step 1.2: Initialize the SDK
) to
easily locate it in the code.
-
Add the following code after the
// Claim Credential - Step 1.2: Initialize the SDK
comment to initialize the SDK by creating a new instance of theMobileCredentialHolder
class:MainActivity.ktlifecycleScope.launch { MobileCredentialHolder.getInstance().initialise(this@MainActivity) }
This will initialize the SDK, making it available for your application.
- Run the app to make sure it compiles properly.
Interact with a Credential offer
Once the SDK is initialized, the next step is to build the capability to handle a Credential offer.
Users can receive OID4VCI Credential offers as deep-links or QR codes. In this tutorial we will use the following QR code, which is a MATTR Labs example of an OID4VCI Credential offer:
Creating your own Credential offer is not within the scope of the current tutorial. You can follow the OID4VCI guide that will walk you through creating one.
Your application must provide a way for the user to interact with Credential offers. As this tutorial uses an offer formatted as a QR code, your application needs to be able to scan and process it. For ease of implementation, we will use a third party library to achieve this:
-
Add the following dependencies to your application
app/build.gradle.kts
file:app/build.gradle.ktsimplementation("com.google.accompanist:accompanist-permissions:0.36.0") implementation("com.journeyapps:zxing-android-embedded:4.3.0")
-
Sync your project with Gradle files.
-
In your package, create a new file named
ScanOfferScreen.kt
. -
Add the following code to the new file:
ScanOfferScreen.ktimport android.Manifest import android.app.Activity import android.content.Context import android.util.Log import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.journeyapps.barcodescanner.BarcodeCallback import com.journeyapps.barcodescanner.DecoratedBarcodeView import global.mattr.mobilecredential.holder.MobileCredentialHolder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch // Gets the permissions and shows the screen content, when the permissions are obtained @OptIn(ExperimentalPermissionsApi::class) @Composable fun ScanOfferScreen(navController: NavController) { val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA) val requestPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {} LaunchedEffect(cameraPermissionState) { if (!cameraPermissionState.status.isGranted) { requestPermissionLauncher.launch(Manifest.permission.CAMERA) } } if (cameraPermissionState.status.isGranted) Content(navController) } // Screen content @Composable private fun Content(navController: NavController) { val context = LocalContext.current val barcodeView = remember { DecoratedBarcodeView(context) } val coroutineScope = rememberCoroutineScope() var isQrScanned by remember { mutableStateOf(false) } val barcodeCallback = remember { BarcodeCallback { result -> // Executed when the QR code was scanned coroutineScope.launch { onQrScanned(context, result.text, navController) } barcodeView.pause() isQrScanned = true } } // Setting up the QR scanner DisposableEffect(Unit) { barcodeView.decodeContinuous(barcodeCallback) barcodeView.resume() onDispose { barcodeView.pause() } } // Showing the scanner until the QR is scanned. Showing a progress bar after that if (!isQrScanned) { AndroidView(factory = { barcodeView }, modifier = Modifier.fillMaxSize()) } else { Box(Modifier.fillMaxSize()) { CircularProgressIndicator(Modifier.align(Alignment.Center)) } } } private suspend fun onQrScanned(context: Context, offer: String, navController: NavController) { // Step 3.2: Discover credential offer }
-
Back in the
MainActivity
file, add the following code under the// Claim Credential - Step 2.5: Add Scan Offer screen call
comment to connect the created composable to the navigation graph:MainActivity.ktScanOfferScreen(navController)
-
Run the app and select the Scan Credential Offer button.
As the user selects the Scan Credential Offer button, the app asks for camera permission, and launches the device camera to enable the user to scan a QR code.
Retrieve offer details and present them to the user
The next capability to build is for the user to be able to review the Credential offer details before agreeing to claim any credentials. Credential discovery is the process in which an application retrieves the offer details, including:
- What Issuer is offering the credentials?
- What credentials are being offered, in what format and what claims do they include?
To display this information to the user, your application should call the
discoverCredentialOffer
function.
-
In your
MainActivity
file, add the following code under the// Claim Credential - Step 3.1: Add discovered credential offer variable
comment to add a new variable that will hold the credential offer:MainActivity.ktvar discoveredCredentialOffer: DiscoveredCredentialOffer? = null
-
In your
ScanOfferScreen
file, add the following code under the// Step 3.2: Discover credential offer
comment to handle the credential offer upon scanning a QR code:ScanOfferScreen.kttry { SharedData.discoveredCredentialOffer = MobileCredentialHolder.getInstance().discoverCredentialOffer(offer) } catch (e: Exception) { Toast.makeText(context, "Failed to discover offer", Toast.LENGTH_SHORT).show() } navController.navigateUp()
Now, once the user scans a QR Code, the
discoverCredentialOffer
function is called and accepts the returnedoffer
string as its parameter.This is a URL-encoded Credential offer which in our example was embedded in a QR code. In other implementations you might have to retrieve this parameter from a deep-link.
The
discoverCredentialOffer
function makes a request to theoffer
URL to retrieve the offer information and returns it as aDiscoveredCredentialOffer
object:@Serializable data class DiscoveredCredentialOffer( val issuer: String, val authorizeEndpoint: String, val tokenEndpoint: String, val credentialEndpoint: String, val credentials: List<OfferedCredential>, val mdocIacasUri: String, requestParameters: RequestParameters? )
The application can now use the
issuer
andcredentials
properties and present this information for the user to review. -
In your
MainActivity
file, add the following code under the// Claim Credential - Step 3.3: Display discovered credential offer
to display the offer details to the user:MainActivity.ktSharedData.discoveredCredentialOffer?.let { discoveredOffer -> Text("Received Credential Offer from ${discoveredOffer.issuer}") LazyColumn( Modifier .fillMaxWidth() .weight(1f) ) { items(discoveredOffer.credentials, key = { it.doctype }) { credential -> Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(4.dp)) { Text("Name: ${credential.name ?: ""}") Text("DocType: ${credential.doctype}") } } } } // Claim Credential - Step 4.4: Add consent button }
-
Run the app, select the Scan Credential Offer button and scan the following QR code:
You should see a result similar to the following:
As the user scans the QR code, the application displays the Credential offer details.
Obtain user consent and initiate credential issuance
The next (and final!) step is to build the capability for the user to accept the credential offer based on the displayed information. This should then trigger issuing the credential and storing it in the application.
In our example this is achieved by selecting a Consent and retrieve Credential(s) button. Once
the user provides their consent by selecting this button, your application must call the
retrieveCredentials
function to trigger the credential issuance.
-
In your
MainActivity
file, add the following code under the// Claim Credential - Step 4.1: Add retrieved credentials variable
comment to add a new variable that will hold the result returned by theretrieveCredentials
function:MainActivity.ktvar retrievedCredentials: List<MobileCredential> = emptyList()
-
Add a new
Constants
object under the// Claim Credential - Step 4.2: Add OpenID4VCI constants object
comment to add the values that will be used for theretrieveCredentials
call:MainActivity.ktobject Constants { const val CLIENT_ID = "android-mobile-credential-tutorial-holder-app" const val REDIRECT_URI = "io.mattrlabs.sample.mobilecredentialtutorialholderapp://credentials/callback" }
CLIENT_ID
: This identifier is used by the issuer to recognize the application. This is only used internally in the interaction between the application and the issuer and can be any string as long as it is registered with the issuer as a trusted application.REDIRECT_URI
: Constructed of the samescheme
anddomain
values we have set as the manifest placeholders in theapp/build.gradle.kts
file.
-
Add the following code under the
// Claim Credential - Step 4.3: Create function to retrieve credentials
comment to create a new function that will call theretrieveCredentials
method when the user gives the consent for the credentials retrieval:MainActivity.ktprivate fun onRetrieveCredentials( coroutineScope: CoroutineScope, activity: Activity, discoveredOffer: DiscoveredCredentialOffer, navController: NavController ) { coroutineScope.launch { try { val mdocHolder = MobileCredentialHolder.getInstance() val retrieveCredentialResults = mdocHolder.retrieveCredentials( activity, discoveredOffer, Constants.CLIENT_ID, Constants.REDIRECT_URI, autoTrustMobileCredentialIaca = true ) // Claim Credential - Step 4.5: Display retrieved credentials } catch (e: Exception) { Toast.makeText(activity, "Failed to retrieve credentials", Toast.LENGTH_SHORT).show() } } }
Letโs review where we get all the parametersโ values from:
discoveredOffer
: This is theDiscoveredCredentialOffer
object returned by thediscoverCredentialOffer
function in step 3 above.Constants.Client_ID
: This was configured as a constant. It is used by the issuer to identify the application that is making a request to claim credentials.Constants.REDIRECT_URI
: This was configured as a constant, and also added to the build script when setting up your development environment. It is used by the SDK to redirect the user back to your application after completing authentication.
-
Add the following code under the
// Claim Credential - Step 4.4: Add consent button
comment to add a button for calling the new function:MainActivity.ktButton( onClick = { onRetrieveCredentials(coroutineScope, activity, discoveredOffer, navController) }, Modifier.fillMaxWidth() ) { Text("Consent and retrieve Credential(s)") }
Once the new button is selected the function is called, redirecting the user to
authenticate
with the configured Authentication provider defined in the authorizeEndpoint
element of the
DiscoveredCredentialOffer
object.
Upon successful authentication, the user can proceed to complete the
OID4VCI workflow configured by the issuer. This workflow can include
different steps based on the issuerโs configuration, but eventually the user is redirected to the
configured redirectUri
which should be handled by your application.
In the example Credential offer used in this tutorial, the issuance workflow only includes authenticating with a mock authentication provider and claiming the credential. But check out our other guides for creating rich and flexible user experiences.
As the user is redirected to redirectUri
, the issuer sends the issued mDocs to your application.
The SDK then processes the received credentials and validates them against the
ISO/IEC 18013-5:2021 standard. Credentials who meet
validation rules are stored in the application internal data storage.
The
retrieveCredentials
function then returns a
RetrieveCredentialResult
list, which references all retrieved credentials:
[RetrieveCredentialResult]
@Serializable
data class RetrieveCredentialResult(
val doctype: String,
val credentialId: String?,
val error: RetrieveCredentialError?
)
[
{
"doctype":"org.iso.18013.5.1.mDL",
"credentialId":"F52084CF-8270-4577-8EDD-23149639D985"
}
]
doctype
: Identifies the credential type.credentialId
: Unique identifier (UUID) of this credential.
Your application can now retrieve specific credentials by calling the
getCredential
function with the credentialId
of any of the retrieved credentials.
The
getCredential
function returns a
MobileCredential
object which represents the issued mDoc, and your application can now introduce UI elements to
enable the user to view the credential.
-
Add the following code under the
// Claim Credential - Step 4.5: Display retrieved credentials
comment to retrieve the credentials by their IDs from the local storage, save them to theSharedData.retrievedCredentials
variable and navigate to theretrievedCredential
screen to display the retrieved credential:MainActivity.ktSharedData.retrievedCredentials = retrieveCredentialResults.mapNotNull { try { mdocHolder.getCredential(it.credentialId!!, skipStatusCheck = true) } catch (e: Exception) { val msg = "Failed to get credential from storage" Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show() null } } navController.navigate("retrievedCredential") SharedData.discoveredCredentialOffer = null
-
In your package, create a new file named
RetrievedCredentialsScreen.kt
. -
Add the following code to the new file to display the
docType
andclaims
of retrieved credentials to the user:RetrievedCredentialsScreen.ktimport androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import global.mattr.mobilecredential.common.deviceretrieval.deviceresponse.NameSpace import global.mattr.mobilecredential.common.dto.MobileCredentialElement @Composable fun RetrievedCredentialsScreen() { if (SharedData.retrievedCredentials.isEmpty()) { Text("No credentials received") } else { Column { Text( "Retrieved Credentials", modifier = Modifier.fillMaxWidth(), style = MaterialTheme.typography.titleLarge ) LazyColumn(Modifier.fillMaxWidth()) { items(SharedData.retrievedCredentials, key = { it.id }) { credential -> Document( credential.docType, credential.claims.mapValues { (_, claims) -> claims.map { (name, value) -> "$name: ${value.toUiString()}" }.toSet() } ) } } } } } @Composable fun Document( docType: String, namespacesAndClaims: Map<NameSpace, Set<String>>, modifier: Modifier = Modifier ) { Card( modifier .fillMaxWidth() .padding(6.dp)) { Column { Text(docType, Modifier.padding(6.dp), style = MaterialTheme.typography.titleMedium) namespacesAndClaims.forEach { (namespace, claims) -> Text(namespace, Modifier.padding(4.dp), style = MaterialTheme.typography.titleSmall) Column( Modifier .padding(6.dp) .fillMaxWidth() .background(MaterialTheme.colorScheme.background, RoundedCornerShape(6.dp)) .padding(6.dp) ) { claims.forEach { claim -> Text(claim) } } } } } } fun MobileCredentialElement.toUiString() = when (this) { is MobileCredentialElement.ArrayElement, is MobileCredentialElement.DataElement, is MobileCredentialElement.MapElement -> this::class.simpleName ?: "Unknown element" else -> value.toString() }
-
Back in your
MainActivity
file, add the following code under the// Claim Credential - Step 4.8: Add "Retrieved Credential" screen call
comment to connect the created composable to the navigation graph:MainActivity.ktRetrievedCredentialsScreen()
-
Run the app, select the Scan Credential Offer button, scan the QR code and then select Consent and retrieve Credential(s).
You should see a result similar to the following:
As the user scans the QR code, they are then provided with the offer details by the wallet. The user then provides consent to retrieving the credentials to which the wallet responds by initiating the issuance workflow and displaying the retrieved credentials to the user.
This tutorial uses a demo MATTR Labs Credential offer to issue the credential. This offer uses a workflow that doesnโt actually authenticate the user before issuing a credential, but redirects them to select the credential they wish to issue. In production implementations this must be replaced by a proper authentication mechanism to comply with the ISO/IEC 18013-5:2021 standard and the OID4VCI specification.
Congratulations! Your application can now interact with an OID4VCI Credential offer to claim mDocs!
Summary
You have just used the Android mDoc holder SDK to build an application that can claim an mDoc issued via an OID4VCI workflow:
This was achieved by building the following capabilities into the application:
- Initialize the SDK so the application can use its functions and classes.
- Interact with a Credential offer formatted as a QR code.
- Retrieve the offer details and present them to the user.
- Obtain user consent and initiate the credential issuance workflow.
Whatโs next?
- You can build the capability to present the claimed credential for proximity verification.
- You can check out the Android mDoc Holder SDK Docs to learn more about available functions and classes.