Obtaining a credential
Provisioning
This codelab teaches you how to implement OpenID4VCI (OpenID Connect for Verifiable Credential Issuance) in a Kotlin Multiplatform mobile wallet application. You'll build a working wallet that can receive and store digital credentials like Utopia membership.
The issuer.multipaz.org server is just for testing, you can create your own server for production use. You can refer to the source code for more info Architecture Overview
The screenshots below illustrate the provisioning process:
-
In the app, click “Get Credentials from Issuer”
-
The browser opens at issuer.multipaz.org → click “OpenID4VCI server”
-
Select the “Utopia Wholesale” credential
-
Click “Credential Offer using custom URL scheme”
-
Complete the Provisioning step
-
On the Verification Page, select the person(In our app, please select "Lee Tom")
-
The Credential is issued
-
Back in the app, click “Present mDL via QR”
-
The QR code is displayed









What is Identity Credential Provisioning?
Identity credential provisioning is the process of securely issuing digital credentials (like driver's licenses, passports, or other identity documents) to a user's digital wallet. This process involves:
- Authentication: Verifying the user's identity
- Authorization: Determining what credentials the user is eligible to receive
- Issuance: Securely transferring the credentials to the user's wallet
- Storage: Safely storing the credentials in the wallet's secure storage
Step-by-Step Implementation
Step 1: Project Setup and Exploration
1.1 Explore the Project Structure
First, set your project’s android:launchMode="singleInstance in AndroidManifest.xml to prevent unnecessary recompositions, which may otherwise break the issuance process.
Look for these key files:
- ProvisioningSupport.kt - Core backend implementation
- App.kt - Main application class
- ProvisioningTestScreen.kt - UI for provisioning
1.2 Understand the ProvisioningSupport Class
//TODO: implement OpenID4VCI_CLIENT_PREFERENCES
val OPENID4VCI_CLIENT_PREFERENCES = OpenID4VCIClientPreferences(
clientId = CLIENT_ID,
redirectUrl = APP_LINK_BASE_URL,
locales = listOf("en-US"),
signingAlgorithms = listOf(Algorithm.ESP256, Algorithm.ESP384, Algorithm.ESP512)
)
ProvisioningSupport is a subclass of OpenID4VCIBackend, which is defined in the Multipaz library. ProvisioningSupport class is the bridge between your wallet and credential issuers. It handles authentication, authorization, and secure communication.
Here we creates an OPENID4VCI_CLIENT_PREFERENCES object, which defines configuration parameters such as clientId, redirectUrl, locals, and signingAlgorithms. The OPENID4VCI_CLIENT_PREFERENCES is then used when calling launchOpenID4VCIProvisioning.
1.3 Examine Key Methods
In ProvisioningSupport.kt
createJwtClientAssertion:
//TODO: implement head
val head = buildJsonObject {
put("typ", "JWT")
put("alg", alg)
put("kid", localClientAssertionKeyId)
}.toString().encodeToByteArray().toBase64Url()
This method creates a JWT header with the signing algorithm and key ID.
Step 2: Understanding URL Processing
Examine the URL Handler
In App.kt file
//TODO: call processAppLinkInvocation(url)
provisioningSupport.processAppLinkInvocation(url)
Credential Offer URLs: Start with openid-credential-offer:// or haip://
During provisioning, the app receives a URL from the server, and the client must perform specific processing based on that URL.
Step 3: Understanding the User Interface
ProvisioningTestScreen.kt
//TODO: update text depends on provisioningState
val text = when (provisioningState) {
ProvisioningModel.Idle -> "Initializing..."
ProvisioningModel.Idle -> "Starting provisioning..."
ProvisioningModel.Connected -> "Connected to the back-end"
ProvisioningModel.ProcessingAuthorization -> "Processing authorization..."
ProvisioningModel.ProcessingAuthorization -> "Authorized"
ProvisioningModel.RequestingCredentials -> "Requesting credentials..."
ProvisioningModel.CredentialsIssued -> "Credentials issued"
is ProvisioningModel.Error -> throw IllegalStateException()
is ProvisioningModel.Authorizing -> throw IllegalStateException()
}
Text(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(8.dp),
style = MaterialTheme.typography.titleLarge,
text = text
)
The provisioning flow progresses through the following states: Idle, Connected, ProcessingAuthorization, RequestingCredentials,CredentialsIssued, etc. Your application should monitor the current provisioning state and display a notification that corresponds to it.
Step 4: Understanding Authorization
4.1 Authorization Handler
In ProvisioningTestScreen.kt
//TODO: init EvidenceRequestWebView
EvidenceRequestWebView(
evidenceRequest = challenge,
provisioningModel = provisioningModel,
provisioningSupport = provisioningSupport
)
EvidenceRequestWebView is called inside Authorize function. The Authorize function receives a list of authorization challenges ,handles OAuth challenges and Calls EvidenceRequestWebView for OAuth challenges
4.2 OAuth Flow Handler
//TODO: add provideAuthorizationResponse
provisioningModel.provideAuthorizationResponse(
AuthorizationResponse.OAuth(stableEvidenceRequest.id, invokedUrl)
)
What it do:
- OAuth Challenge Handling: Receives an OAuth authorization challenge from the issuer
- External Browser Launch: Opens the user's default browser with the OAuth URL
- Callback Management: Waits for the user to complete authentication and return via app links
- Response Processing: Handles the OAuth callback and provides the response to the provisioning model
It launches the external browser instead and manages the OAuth flow through app links.
Step 5 (Optional): APP_LINK_SERVER Configuration and OAuth Callback Handling
| Info: This section explains an optional configuration. The Wholesale Codelab uses custom schemes by default, so the app should work without applying these steps, since custom intents do not require verification. |
|---|
The APP_LINK_SERVER is a critical component that enables OAuth callback handling through Android App Links. This section explains how it works and how to configure it properly.
5.1 What is APP_LINK_SERVER?
Defautly we are using Custom URL scheme rather than HTTP App Links.
The APP_LINK_SERVER serves as the OAuth callback endpoint for your credential provisioning flow. It's the URL where the external browser redirects after the user completes OAuth authentication.
companion object Companion {
// Default custom scheme (enabled in AndroidManifest.xml)
const val APP_LINK_SERVER = "wholesale-test-app"
const val APP_LINK_BASE_URL = "${APP_LINK_SERVER}://landing/"
// Alternative HTTP App Links (more secure). See AndroidManifest.xml Option #2
/*const val APP_LINK_SERVER = "https://apps.multipaz.org"
const val APP_LINK_BASE_URL = "$APP_LINK_SERVER/landing/"*/
}
If you use HTTP App Links in your app, since your app's fingerprint has not been uploaded to "apps.multipaz.org" website), app links from the website cannot be handled by the app and will instead open in the browser. You have to register your app’s fingerprint on the Multipaz server(or your own website). If you app's fingerprint is registered successfully. Long click you app and click App Info → Open by default, you will see "1 verified Link" just like below
If you click "1 verified link", you will see apps.multipaz.org(or your website link) is verified just like below:
For more Verify App Links knowledge you check official documentation.
5.2 Android Manifest Configuration
The codelab enables custom URI schemes out of the box. This intent filter matches the default configuration (wholesale-test-app://landing):
<!-- Option #1 - Custom URI Scheme (default) -->
<!-- Must match ApplicationSupportLocal.APP_LINK_SERVER -->
<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="wholesale-test-app"/>
<data android:host="landing"/>
</intent-filter>
<!-- Option #2 - HTTPS App Links - Requires .well-known/assetlinks.json -->
<!-- Examples: https://apps.multipaz.org/landing/ -->
<!-- Must match ApplicationSupportLocal.APP_LINK_SERVER -->
<!--<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!--
Do not include other schemes, only https. If domain is changed here, it
also MUST be changed in ApplicationSupportLocal class.
-->
<data
android:scheme="https"
android:host="apps.multipaz.org"
android:pathPattern="/landing/.*"/>
</intent-filter>-->
Above code in AndroidManifest.xml explains the Custom URI(option 1) and HTTPS App Links (Option 2)
Step 6 (Optional)Set up your Own Credential Server
If you are setting up your own Credential server, the steps below will guide you through adding your app’s fingerprint.
6.1 App Link Verification and Trust
Android verifies that your app is trusted to handle URLs from the specified domain. This prevents malicious apps from intercepting OAuth callbacks.
App link (High Security):
- Requires .well-known/assetlinks.json on the server
- Must include your app's signing certificate fingerprint
- Android automatically verifies the trust relationship
Customize URI (Low Security):
- No verification required
- Works immediately for testing
- Less secure but easier to set up
6.2 app fingerprint add in server
If you want to use your own server instead of apps.multipaz.org:
In client you should change below constants
const val APP_LINK_SERVER = "https://your-server.com"
const val APP_LINK_BASE_URL = "$APP_LINK_SERVER/landing/"
In client side you should update AndroidManifest.xml
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="your-server.com"
android:pathPattern="/landing/.*"/>
</intent-filter>
Create assetlinks.json which contains app’s fingerprint and update it in server side
Upload this file to https://your-server.com/.well-known/assetlinks.json:
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "org.multipaz.samples.wallet.cmp",
"sha256_cert_fingerprints": [
"YOUR_APP_SIGNING_CERTIFICATE_FINGERPRINT"
]
}
}
]
Security Features
private val attestationCertificate by lazy {
runBlocking {
X509Cert.fromPem(
Res.readBytes("files/attestationCertificate.pem").decodeToString().trimIndent()
)
}
}
private val attestationPrivateKey =
runBlocking {
EcPrivateKey.fromPem(Res.readBytes("files/attestationPrivateKey.pem").decodeToString().trimIndent().trimIndent(),
attestationCertificate.ecPublicKey
)
}
What are Attestation Certificate and Private Key?
Attestation Certificate (attestationCertificate):
- An X.509 digital certificate that proves the wallet's identity and security properties
- Contains the wallet's public key and metadata (issuer, validity period, etc.)
- Acts as a "digital passport" that issuers can trust
- In this implementation, it's embedded in the app for testing purposes
Attestation Private Key (attestationPrivateKey):
- The corresponding private key used to sign attestation tokens
- Must be kept secret and secure
- Used to prove that the wallet actually controls the certificate