Skip to main content
Version: v0.13

OID4VP Holder

The OID4VP holder component enables wallet applications to receive and respond to credential presentation requests. This guide covers parsing authorization requests, resolving verifier identity, creating presentations, and submitting responses.

Obtaining the Holder

val holder: Oid4vpHolder = session.component.oid4vpHolder

Processing Authorization Requests

When a user scans a QR code or follows a deep link, the wallet receives an authorization request URI. Processing involves parsing, resolving, and validating the request.

Parsing the Request

// Parse the incoming authorization request
val parseResult = holder.parseAuthorizationRequest(
requestUri = "openid4vp://authorize?client_id=https://verifier.example.com&...",
walletConfig = WalletConfig(
audience = "https://wallet.example.com",
decryptionKey = null // For encrypted request objects (JWE)
)
)

if (parseResult.isOk) {
val authRequest = parseResult.value

// Access request details
val clientId = authRequest.clientId
val nonce = authRequest.nonce
val state = authRequest.state
val responseUri = authRequest.responseUri
}

Resolving the Request

After parsing, resolve the request to fetch verifier metadata and validate the client identity:

val resolveResult = holder.resolveAuthorizationRequest(authRequest)

if (resolveResult.isOk) {
val resolved = resolveResult.value

// Access the DCQL query
val dcqlQuery = resolved.dcqlQuery

// Get verifier information for UI display
val verifierInfo = resolved.verifierInfo
println("Verifier: ${verifierInfo.displayName}")
println("Client ID: ${verifierInfo.clientId}")
println("Scheme: ${verifierInfo.clientIdScheme}")
println("Valid: ${verifierInfo.clientIdValid}")

// Check for validation errors
if (verifierInfo.clientIdValidationErrors.isNotEmpty()) {
println("Warnings: ${verifierInfo.clientIdValidationErrors}")
}
}

Use the resolved request to build the consent UI showing what the verifier is requesting:

// Display verifier information
val verifierInfo = resolved.verifierInfo
displayVerifierCard(
name = verifierInfo.displayName ?: verifierInfo.clientId,
logo = verifierInfo.logoUri,
trusted = verifierInfo.clientIdValid
)

// Display requested credentials from DCQL query
resolved.dcqlQuery?.credentials?.forEach { credentialQuery ->
println("Credential: ${credentialQuery.id}")
println("Format: ${credentialQuery.format}")

// Show requested claims
credentialQuery.claims?.forEach { claim ->
println(" Claim: ${claim.path.joinToString(".")}")
}
}

Creating the Response

After user consent, create the authorization response with selected credentials:

// Build list of selected credentials
val selectedCredentials = listOf(
SelectedCredential(
credentialQueryId = "identity_credential", // Matches DCQL query ID
credentialId = "wallet-cred-001", // Local wallet ID
presentation = sdJwtPresentation, // Serialized presentation
format = "dc+sd-jwt",
disclosedClaims = mapOf(
"given_name" to "John",
"family_name" to "Doe"
)
)
)

// Create the authorization response
val responseResult = holder.createAuthorizationResponse(
request = resolved,
selectedCredentials = selectedCredentials
)

if (responseResult.isOk) {
val response = responseResult.value
// response.vpToken contains the VP token for submission
}

Submitting the Response

Send the authorization response to the verifier:

val submitResult = holder.submitAuthorizationResponse(
resolvedRequest = resolved,
response = response,
responseMode = ResponseMode.DIRECT_POST // or null to use request default
)

if (submitResult.isOk) {
when (val result = submitResult.value) {
is SubmissionResult.Success -> {
// Presentation accepted
result.redirectUri?.let { uri ->
// Redirect user if verifier provided a redirect
openUrl(uri)
}
}
is SubmissionResult.Error -> {
// Verifier returned an error
showError("Error: ${result.error} - ${result.errorDescription}")
}
is SubmissionResult.Redirect -> {
// For fragment/query response modes
openUrl(result.redirectUri)
}
}
}

Complete Flow Example

class WalletPresentationHandler(
private val holder: Oid4vpHolder,
private val credentialStore: CredentialStore,
private val sdJwtService: SdJwtService
) {
suspend fun handlePresentationRequest(requestUri: String): PresentationResult {
// 1. Parse the authorization request
val parseResult = holder.parseAuthorizationRequest(
requestUri = requestUri,
walletConfig = WalletConfig(audience = "https://wallet.example.com")
)

if (!parseResult.isOk) {
return PresentationResult.Error("Failed to parse request")
}

// 2. Resolve request (fetch metadata, validate client)
val resolveResult = holder.resolveAuthorizationRequest(parseResult.value)

if (!resolveResult.isOk) {
return PresentationResult.Error("Failed to resolve request")
}

val resolved = resolveResult.value

// 3. Check if we have matching credentials
val matchingCredentials = findMatchingCredentials(
resolved.dcqlQuery,
credentialStore.getAllCredentials()
)

if (matchingCredentials.isEmpty()) {
return PresentationResult.NoMatchingCredentials
}

// 4. Show consent UI and wait for user decision
val userConsent = showConsentUI(
verifier = resolved.verifierInfo,
requestedCredentials = resolved.dcqlQuery,
availableCredentials = matchingCredentials
)

if (!userConsent.approved) {
return PresentationResult.UserDeclined
}

// 5. Create presentations for selected credentials
val selectedCredentials = userConsent.selectedCredentials.map { selection ->
val presentation = when (selection.format) {
"dc+sd-jwt" -> createSdJwtPresentation(
credential = selection.credential,
claims = selection.disclosedClaims,
audience = resolved.verifierInfo.clientId,
nonce = resolved.request.nonce
)
"mso_mdoc" -> createMdocPresentation(
credential = selection.credential,
claims = selection.disclosedClaims
)
else -> throw UnsupportedOperationException("Unknown format: ${selection.format}")
}

SelectedCredential(
credentialQueryId = selection.queryId,
credentialId = selection.credential.id,
presentation = presentation,
format = selection.format,
disclosedClaims = selection.disclosedClaims
)
}

// 6. Create and submit response
val responseResult = holder.createAuthorizationResponse(
request = resolved,
selectedCredentials = selectedCredentials
)

if (!responseResult.isOk) {
return PresentationResult.Error("Failed to create response")
}

val submitResult = holder.submitAuthorizationResponse(
resolvedRequest = resolved,
response = responseResult.value
)

return when {
!submitResult.isOk -> PresentationResult.Error("Failed to submit")
submitResult.value is SubmissionResult.Success -> {
PresentationResult.Success(
(submitResult.value as SubmissionResult.Success).redirectUri
)
}
else -> PresentationResult.Error("Submission rejected")
}
}

private suspend fun createSdJwtPresentation(
credential: StoredCredential,
claims: Map<String, String>,
audience: String,
nonce: String?
): String {
val result = sdJwtService.presentSdJwt(
PresentSdJwtArgs(
sdJwt = credential.serialized,
disclosureSelection = SdMap(
fields = claims.keys.associateWith { SdField(sd = true) }
),
audience = audience,
nonce = nonce,
holderKey = credential.holderKeyInfo
)
)
return result.value.presentation
}
}

sealed class PresentationResult {
data class Success(val redirectUri: String?) : PresentationResult()
data class Error(val message: String) : PresentationResult()
object NoMatchingCredentials : PresentationResult()
object UserDeclined : PresentationResult()
}

Data Types

SelectedCredential

data class SelectedCredential(
val credentialQueryId: String, // Must match DCQL credential query ID
val credentialId: String, // Local wallet credential identifier
val presentation: String, // Serialized credential presentation
val format: String, // e.g., "dc+sd-jwt", "mso_mdoc"
val disclosedClaims: Map<String, String>? = null
)

VerifierInfo

data class VerifierInfo(
val clientId: String,
val clientIdScheme: ClientIdScheme,
val clientIdValid: Boolean = true,
val clientIdValidationErrors: List<ClientIdValidationError> = emptyList(),
val displayName: String? = null,
val logoUri: String? = null,
val trustRoot: String? = null
)

SubmissionResult

sealed interface SubmissionResult {
data class Success(
val redirectUri: String? = null,
val responseBody: JsonObject? = null
) : SubmissionResult

data class Error(
val error: String,
val errorDescription: String? = null
) : SubmissionResult

data class Redirect(
val redirectUri: String
) : SubmissionResult
}