How to configure the application
Table of contents
- General configuration
- DeepLink Schemas configuration
- Scoped Issuance Document Configuration
- How to work with self-signed certificates
- Batch Document Issuance Configuration
- Theme configuration
- Pin Storage configuration
- Analytics configuration
General configuration
All core network and trust settings are centralized in the WalletCoreConfig interface inside the
core-logic module:
interface WalletCoreConfig {
// 1. Issuing API
val vciConfig: List<OpenId4VciManager.Config>
// 2. Wallet Provider Host
val walletProviderHost: String
// 3. Trusted certificates
val config: EudiWalletConfig
}
You configure these properties per flavor by providing a WalletCoreConfigImpl for each build
variant:
core-logic/src/demo/config/WalletCoreConfigImpl.ktcore-logic/src/dev/config/WalletCoreConfigImpl.kt
Each flavor can use different issuer URLs, wallet provider hosts, and trust stores.
- Issuing API
The Issuing API is configured via the vciConfig property:
```kotlin
override val vciConfig: List<OpenId4VciManager.Config>
get() = listOf(
OpenId4VciManager.Config.Builder()
.withIssuerUrl(issuerUrl = "https://issuer.eudiw.dev")
.withClientAuthenticationType(OpenId4VciManager.ClientAuthenticationType.AttestationBased)
.withAuthFlowRedirectionURI(BuildConfig.ISSUE_AUTHORIZATION_DEEPLINK)
.withParUsage(OpenId4VciManager.Config.ParUsage.IF_SUPPORTED)
.withDPoPUsage(OpenId4VciManager.Config.DPoPUsage.IfSupported())
.build()
)
```
Adjust the configuration per flavor in the corresponding WalletCoreConfigImpl.
- Wallet Provider Host
The Wallet Provider Host is configured via the walletProviderHost property:
```kotlin
override val walletProviderHost: String
get() = "https://wallet-provider.eudiw.dev"
```
Again, set a different value per flavor in the corresponding WalletCoreConfigImpl.
- Trusted certificates
Trusted certificates are configured via the config property:
```kotlin
_config = EudiWalletConfig {
configureReaderTrustStore(context, R.raw.eudi_pid_issuer_ut)
}
```
The application's IACA certificates are located here
Configure EudiWalletConfig per flavor inside the appropriate WalletCoreConfigImpl.
- Preregistered Client Scheme
If you plan to use the ClientIdScheme.Preregistered for OpenId4VP configuration, please add the following to the configuration files.
```kotlin
const val OPENID4VP_VERIFIER_API_URI = "your_verifier_url"
const val OPENID4VP_VERIFIER_LEGAL_NAME = "your_verifier_legal_name"
const val OPENID4VP_VERIFIER_CLIENT_ID = "your_verifier_client_id"
configureOpenId4Vp {
withClientIdSchemes(
listOf(
ClientIdScheme.Preregistered(
listOf(
PreregisteredVerifier(
clientId = OPENID4VP_VERIFIER_CLIENT_ID,
verifierApi = OPENID4VP_VERIFIER_API_URI,
legalName = OPENID4VP_VERIFIER_LEGAL_NAME
)
)
)
)
)
}
```
DeepLink Schemas configuration
According to the specifications, issuance, presentation require deep-linking for the same device flows.
If you want to adjust any schema, you can alter the AndroidLibraryConventionPlugin inside the build-logic module.
val eudiOpenId4VpScheme = "eudi-openid4vp"
val eudiOpenid4VpHost = "*"
val mdocOpenId4VpScheme = "mdoc-openid4vp"
val mdocOpenid4VpHost = "*"
val openId4VpScheme = "openid4vp"
val openid4VpHost = "*"
val avspScheme = "avsp"
val avspHost = "*"
val credentialOfferScheme = "openid-credential-offer"
val credentialOfferHost = "*"
Let's assume you want to change the credential offer schema to custom-my-offer:// the AndroidLibraryConventionPlugin should look like this:
val eudiOpenId4VpScheme = "eudi-openid4vp"
val eudiOpenid4VpHost = "*"
val mdocOpenId4VpScheme = "mdoc-openid4vp"
val mdocOpenid4VpHost = "*"
val openId4VpScheme = "openid4vp"
val openid4VpHost = "*"
val avspScheme = "avsp"
val avspHost = "*"
val credentialOfferScheme = "custom-my-offer"
val credentialOfferHost = "*"
val credentialOfferHaipScheme = "haip-vci"
val credentialOfferHaipHost = "*"
In case of an additive change, e.g., adding an extra credential offer schema, you must adjust the following.
AndroidLibraryConventionPlugin:
val credentialOfferScheme = "openid-credential-offer"
val credentialOfferHost = "*"
val credentialOfferHaipScheme = "haip-vci"
val credentialOfferHaipHost = "*"
val myOwnCredentialOfferScheme = "custom-my-offer"
val myOwnCredentialOfferHost = "*"
// Manifest placeholders used for OpenId4VCI
manifestPlaceholders["credentialOfferHost"] = credentialOfferHost
manifestPlaceholders["credentialOfferScheme"] = credentialOfferScheme
manifestPlaceholders["credentialOfferHaipHost"] = credentialOfferHaipHost
manifestPlaceholders["credentialOfferHaipScheme"] = credentialOfferHaipScheme
manifestPlaceholders["myOwnCredentialOfferHost"] = myOwnCredentialOfferHost
manifestPlaceholders["myOwnCredentialOfferScheme"] = myOwnCredentialOfferScheme
addConfigField("CREDENTIAL_OFFER_SCHEME", credentialOfferScheme)
addConfigField("CREDENTIAL_OFFER_HAIP_SCHEME", credentialOfferHaipScheme)
addConfigField("MY_OWN_CREDENTIAL_OFFER_SCHEME", myOwnCredentialOfferScheme)
Android Manifest (inside assembly-logic module):
<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:host="${credentialOfferHost}"
android:scheme="${credentialOfferScheme}" />
</intent-filter>
<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:host="${credentialOfferHaipHost}"
android:scheme="${credentialOfferHaipScheme}" />
</intent-filter>
<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:host="${myOwnCredentialOfferHost}"
android:scheme="${myOwnCredentialOfferScheme}" />
</intent-filter>
DeepLinkType (DeepLinkHelper Object inside ui-logic module):
enum class DeepLinkType(val schemas: List<String>, val host: String? = null) {
OPENID4VP(
schemas = listOf(
BuildConfig.OPENID4VP_SCHEME,
BuildConfig.EUDI_OPENID4VP_SCHEME,
BuildConfig.MDOC_OPENID4VP_SCHEME,
BuildConfig.AVSP_SCHEME,
BuildConfig.AV_SCHEME,
BuildConfig.MY_OWN_CREDENTIAL_OFFER_SCHEME
)
),
CREDENTIAL_OFFER(
schemas = listOf(
BuildConfig.CREDENTIAL_OFFER_SCHEME,
BuildConfig.CREDENTIAL_OFFER_HAIP_SCHEME,
BuildConfig.MY_OWN_CREDENTIAL_OFFER_SCHEME
)
),
ISSUANCE(
schemas = listOf(BuildConfig.ISSUE_AUTHORIZATION_SCHEME),
host = BuildConfig.ISSUE_AUTHORIZATION_HOST
),
DYNAMIC_PRESENTATION(
emptyList()
),
EXTERNAL(emptyList())
}
In the case of an additive change regarding OpenID4VP, you also need to update the EudiWalletConfig for each flavor inside the core-logic module.
configureOpenId4Vp {
withSchemes(
listOf(
BuildConfig.OPENID4VP_SCHEME,
BuildConfig.EUDI_OPENID4VP_SCHEME,
BuildConfig.MDOC_OPENID4VP_SCHEME,
BuildConfig.AVSP_SCHEME,
BuildConfig.YOUR_OWN_OPENID4VP_SCHEME
)
)
}
Scoped Issuance Document Configuration
The credential configuration is derived directly from the issuer's metadata. The issuer URL is configured per flavor via the vciConfig property inside the core-logic module at src/demo/config/WalletCoreConfigImpl and src/dev/config/WalletCoreConfigImpl. If you want to add or adjust the displayed scoped documents, you must modify the issuer's metadata, and the wallet will automatically resolve your changes.
Passport Scanning Issuer Configuration
The application supports multiple issuers for different credential types. By convention, if the
vciConfig list contains multiple issuers, the second issuer (index 1) is used for passport
scanning flows. This allows age verification documents to be issued through a dedicated endpoint
after passport scanning.
The configuration is flavor-specific and defined in src/demo/config/WalletCoreConfigImpl and src/dev/config/WalletCoreConfigImpl.
Dev Flavor Configuration:
override val vciConfig: List<OpenId4VciManager.Config>
get() = listOf(
// First issuer - for regular credentials
OpenId4VciManager.Config.Builder()
.withIssuerUrl(issuerUrl = "https://dev.issuer.eudiw.dev")
.withClientId(clientId = "wallet-dev")
.withAuthFlowRedirectionURI(BuildConfig.ISSUE_AUTHORIZATION_DEEPLINK)
.withParUsage(OpenId4VciManager.Config.ParUsage.NEVER)
.withUseDPoPIfSupported(false)
.build(),
// Second issuer - for passport scanning credentials
OpenId4VciManager.Config.Builder()
.withIssuerUrl(issuerUrl = "https://issuer.dev.ageverification.dev")
.withClientId(clientId = "wallet-dev")
.withAuthFlowRedirectionURI(BuildConfig.ISSUE_AUTHORIZATION_DEEPLINK)
.withParUsage(OpenId4VciManager.Config.ParUsage.NEVER)
.withUseDPoPIfSupported(false)
.build()
)
Demo Flavor Configuration:
override val vciConfig: List<OpenId4VciManager.Config>
get() = listOf(
// First issuer - for regular credentials
OpenId4VciManager.Config.Builder()
.withIssuerUrl(issuerUrl = "https://issuer.ageverification.dev")
.withClientAuthenticationType(
OpenId4VciManager.ClientAuthenticationType.None(
clientId = "wallet-dev"
)
)
.withAuthFlowRedirectionURI(BuildConfig.ISSUE_AUTHORIZATION_DEEPLINK)
.withParUsage(OpenId4VciManager.Config.ParUsage.NEVER)
.withDPoPUsage(OpenId4VciManager.Config.DPoPUsage.Disabled)
.build(),
// Second issuer - for passport scanning credentials
OpenId4VciManager.Config.Builder()
.withIssuerUrl(issuerUrl = "https://issuer.dev.ageverification.dev")
.withClientAuthenticationType(
OpenId4VciManager.ClientAuthenticationType.None(
clientId = "wallet-dev"
)
)
.withAuthFlowRedirectionURI(BuildConfig.ISSUE_AUTHORIZATION_DEEPLINK)
.withParUsage(OpenId4VciManager.Config.ParUsage.NEVER)
.withDPoPUsage(OpenId4VciManager.Config.DPoPUsage.Disabled)
.build()
)
The passport scanning issuer configuration is optional. If the vciConfig list contains only one
issuer, passport scanning issuance will not be available.
Important Note: PAR (Pushed Authorization Request) and DPoP (Demonstration of Proof-of-Possession) are not supported in the AV profile. Both features must be disabled by setting:
parUsage = OpenId4VciManager.Config.ParUsage.NEVERuseDPoPIfSupported = false
Face Match Configuration
The application uses AI models for face liveness detection and face matching during passport
verification flows. These models are configured via the faceMatchConfig property in the
WalletCoreConfig interface.
The configuration is flavor-specific and defined in src/demo/config/WalletCoreConfigImpl and src/dev/config/WalletCoreConfigImpl.
Configuration Structure:
data class FaceMatchConfig(
val faceDetectorModel: String, // Model for detecting faces in images
val embeddingExtractorModel: String, // Model for extracting face embeddings
val livenessModel0: String, // First liveness detection model
val livenessModel1: String, // Second liveness detection model
val livenessThreshold: Double, // Threshold for liveness detection (0.0-1.0)
val matchingThreshold: Double, // Threshold for face matching (0.0-1.0)
)
Example Configuration:
override val faceMatchConfig: FaceMatchConfig = FaceMatchConfig(
faceDetectorModel = "https://github.com/your-org/models/releases/download/v1.0/face_detector.tflite",
embeddingExtractorModel = "https://github.com/your-org/models/releases/download/v1.0/embedding_extractor.tflite",
livenessModel0 = "https://github.com/your-org/models/releases/download/v1.0/liveness_model_0.tflite",
livenessModel1 = "https://github.com/your-org/models/releases/download/v1.0/liveness_model_1.tflite",
livenessThreshold = 0.85, // 85% confidence for liveness
matchingThreshold = 0.75, // 75% confidence for face matching
)
Model Path Options:
The model paths can be specified as:
- Remote URLs (HTTP/HTTPS) for downloading models at runtime
- Local asset paths (e.g.,
file:///android_asset/models/face_detector.tflite) - Local file paths on device storage
Threshold Configuration:
livenessThreshold: Controls the sensitivity of liveness detection. Higher values (e.g., 0.9) are more strict and may reject more legitimate faces, while lower values (e.g., 0.7) are more lenient but may accept more spoofing attempts.matchingThreshold: Controls the sensitivity of face matching between the passport photo and selfie. Higher values require a closer match, while lower values are more forgiving of lighting and angle differences.
⚠️ SECURITY WARNING:
The models referenced in the default configuration are currently hosted on GitHub Releases for development and testing purposes only. This is NOT recommended for production environments.
How to work with self-signed certificates
This section describes configuring the application to interact with services utilizing self-signed certificates.
To enable support for self-signed certificates, you must customize the existing Ktor HttpClient
used by the application.
- Open the
NetworkModule.ktfile of thenetwork-logicmodule. -
Add the following imports:
import android.annotation.SuppressLint import java.security.SecureRandom import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager import javax.security.cert.CertificateException -
Replace the
provideHttpClientfunction with the following:@SuppressLint("TrustAllX509TrustManager", "CustomX509TrustManager") @Single fun provideHttpClient(json: Json): HttpClient { val trustAllCerts = arrayOf<TrustManager>( object : X509TrustManager { @Throws(CertificateException::class) override fun checkClientTrusted( chain: Array<java.security.cert.X509Certificate>, authType: String ) { } @Throws(CertificateException::class) override fun checkServerTrusted( chain: Array<java.security.cert.X509Certificate>, authType: String ) { } override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> { return arrayOf() } } ) return HttpClient(Android) { install(Logging) install(ContentNegotiation) { json( json = json, contentType = ContentType.Application.Json ) } engine { requestConfig sslManager = { httpsURLConnection -> httpsURLConnection.sslSocketFactory = SSLContext.getInstance("TLS").apply { init(null, trustAllCerts, SecureRandom()) }.socketFactory httpsURLConnection.hostnameVerifier = HostnameVerifier { _, _ -> true } } } } }
Batch Document Issuance Configuration
The app is configured to use batch document issuance by default, requesting a batch of credentials at once and handling them according to a defined policy.
You can configure the following aspects of batch document issuance in DocumentIssuanceRule:
- number of credentials (formerly batch size) - The number of credentials to be issued for the document at once
- Credential policy - whether to use each credential once or rotate through them
These settings are configured in your flavor's implementation of WalletCoreConfigImpl. For
example, in the demo flavor:
internal class WalletCoreConfigImpl(
private val context: Context
) : WalletCoreConfig {
// ...other configuration...
val documentIssuanceConfig: DocumentIssuanceConfig
get() = DocumentIssuanceConfig(
defaultRule = DocumentIssuanceRule(
policy = CredentialPolicy.OneTimeUse,
numberOfCredentials = 30
),
documentSpecificRules = mapOf()
)
}
Note that the batch size will be limited by the issuer's metadata configuration, so you may not be
able to request a batch larger than what the issuer allows. To understand the issuer's
configuration, you can check the issuer's metadata endpoint, which is usually available at
https://<issuer-url>/.well-known/openid-configuration. Specifically, look for the
credential_batch_size field in the metadata response.
Note that the batch size will be limited by the issuer's metadata configuration, so you may not be
able to request a batch larger than what the issuer allows. to understand the issuer's
configuration, you can check the issuer's metadata endpoint, which is usually available at
https://<issuer-url>/.well-known/openid-configuration. specifically, look for
the credential_batch_size field in the metadata response.
Theme configuration
The application allows the configuration of:
- Colors
- Images
- Shape
- Fonts
- Dimension
Via ThemeManager.Builder().
Pin Storage configuration
The application allows the configuration of the PIN storage. You can configure the following:
- Where the pin will be stored
- From where the pin will be retrieved
- Pin matching and validity
Via the StorageConfig inside the authentication-logic module.
interface StorageConfig {
val pinStorageProvider: PinStorageProvider
val biometryStorageProvider: BiometryStorageProvider
}
You can provide your storage implementation by implementing the PinStorageProvider interface and then setting it as the default to the StorageConfigImpl pinStorageProvider variable. The project utilizes Koin for Dependency Injection (DI), thus requiring adjustment of the LogicAuthenticationModule graph to provide the configuration.
Implementation Example:
class PrefsPinStorageProvider(
private val prefsController: PrefsController,
private val cryptoController: CryptoController
) : PinStorageProvider {
override fun retrievePin(): String = decryptedAndLoad()
override fun setPin(pin: String) {
encryptAndStore(pin)
}
override fun isPinValid(pin: String): Boolean = retrievePin() == pin
}
Config Example:
class StorageConfigImpl(
private val pinImpl: PinStorageProvider,
private val biometryImpl: BiometryStorageProvider
) : StorageConfig {
override val pinStorageProvider: PinStorageProvider
get() = pinImpl
override val biometryStorageProvider: BiometryStorageProvider
get() = biometryImpl
}
Config Construction via Koin DI Example:
@Single
fun provideStorageConfig(
prefsController: PrefsController,
cryptoController: CryptoController
): StorageConfig = StorageConfigImpl(
pinImpl = PrefsPinStorageProvider(prefsController, cryptoController),
biometryImpl = PrefsBiometryStorageProvider(prefsController)
)
Analytics configuration
The application allows the configuration of multiple analytics providers. You can configure the following:
- Initializing the provider (e.g., Firebase, Appcenter, etc)
- Screen logging
- Event logging
Via the AnalyticsConfig inside the analytics-logic module.
interface AnalyticsConfig {
val analyticsProviders: Map<String, AnalyticsProvider>
get() = emptyMap()
}
You can provide your implementation by implementing the AnalyticsProvider interface and then adding it to your AnalyticsConfigImpl analyticsProviders variable.
You will also need the provider's token/key, thus requiring a Map
Implementation Example:
object AppCenterAnalyticsProvider : AnalyticsProvider {
override fun initialize(context: Application, key: String) {
AppCenter.start(
context,
key,
Analytics::class.java
)
}
override fun logScreen(name: String, arguments: Map<String, String>) {
logEvent(name, arguments)
}
override fun logEvent(event: String, arguments: Map<String, String>) {
if (Analytics.isEnabled().get()) {
Analytics.trackEvent(event, arguments)
}
}
}
Config Example:
class AnalyticsConfigImpl : AnalyticsConfig {
override val analyticsProviders: Map<String, AnalyticsProvider>
get() = mapOf("YOUR_OWN_KEY" to AppCenterAnalyticsProvider)
}
Config Construction via Koin DI Example:
@Single
fun provideAnalyticsConfig(): AnalyticsConfig = AnalyticsConfigImpl()