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
- Android/Kotlin
- iOS/Swift
val holder: Oid4vpHolder = session.component.oid4vpHolder
let 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
- Android/Kotlin
- iOS/Swift
// 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
}
// Parse the incoming authorization request
let parseResult = try await holder.parseAuthorizationRequest(
requestUri: "openid4vp://authorize?client_id=https://verifier.example.com&...",
walletConfig: WalletConfig(
audience: "https://wallet.example.com",
decryptionKey: nil // For encrypted request objects (JWE)
)
)
if parseResult.isOk {
let authRequest = parseResult.value
// Access request details
let clientId = authRequest.clientId
let nonce = authRequest.nonce
let state = authRequest.state
let responseUri = authRequest.responseUri
}
Resolving the Request
After parsing, resolve the request to fetch verifier metadata and validate the client identity:
- Android/Kotlin
- iOS/Swift
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}")
}
}
let resolveResult = try await holder.resolveAuthorizationRequest(
request: authRequest
)
if resolveResult.isOk {
let resolved = resolveResult.value
// Access the DCQL query
let dcqlQuery = resolved.dcqlQuery
// Get verifier information for UI display
let verifierInfo = resolved.verifierInfo
print("Verifier: \(verifierInfo.displayName ?? "")")
print("Client ID: \(verifierInfo.clientId)")
print("Scheme: \(verifierInfo.clientIdScheme)")
print("Valid: \(verifierInfo.clientIdValid)")
// Check for validation errors
if !verifierInfo.clientIdValidationErrors.isEmpty {
print("Warnings: \(verifierInfo.clientIdValidationErrors)")
}
}
Building the Consent UI
Use the resolved request to build the consent UI showing what the verifier is requesting:
- Android/Kotlin
- iOS/Swift
// 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(".")}")
}
}
// Display verifier information
let verifierInfo = resolved.verifierInfo
displayVerifierCard(
name: verifierInfo.displayName ?? verifierInfo.clientId,
logo: verifierInfo.logoUri,
trusted: verifierInfo.clientIdValid
)
// Display requested credentials from DCQL query
for credentialQuery in resolved.dcqlQuery?.credentials ?? [] {
print("Credential: \(credentialQuery.id)")
print("Format: \(credentialQuery.format ?? "")")
// Show requested claims
for claim in credentialQuery.claims ?? [] {
print(" Claim: \(claim.path.joined(separator: "."))")
}
}
Creating the Response
After user consent, create the authorization response with selected credentials:
- Android/Kotlin
- iOS/Swift
// 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
}
// Build list of selected credentials
let selectedCredentials = [
SelectedCredential(
credentialQueryId: "identity_credential", // Matches DCQL query ID
credentialId: "wallet-cred-001", // Local wallet ID
presentation: sdJwtPresentation, // Serialized presentation
format: "dc+sd-jwt",
disclosedClaims: [
"given_name": "John",
"family_name": "Doe"
]
)
]
// Create the authorization response
let responseResult = try await holder.createAuthorizationResponse(
request: resolved,
selectedCredentials: selectedCredentials
)
if responseResult.isOk {
let response = responseResult.value
// response.vpToken contains the VP token for submission
}
Submitting the Response
Send the authorization response to the verifier:
- Android/Kotlin
- iOS/Swift
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)
}
}
}
let submitResult = try await holder.submitAuthorizationResponse(
resolvedRequest: resolved,
response: response,
responseMode: .directPost // or nil to use request default
)
if submitResult.isOk {
switch submitResult.value {
case let success as SubmissionResult.Success:
// Presentation accepted
if let redirectUri = success.redirectUri {
openUrl(url: redirectUri)
}
case let error as SubmissionResult.Error:
// Verifier returned an error
showError(message: "\(error.error): \(error.errorDescription ?? "")")
case let redirect as SubmissionResult.Redirect:
// For fragment/query response modes
openUrl(url: redirect.redirectUri)
default:
break
}
}
Complete Flow Example
- Android/Kotlin
- iOS/Swift
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()
}
class WalletPresentationHandler {
private let holder: Oid4vpHolder
private let credentialStore: CredentialStore
private let sdJwtService: SdJwtService
init(holder: Oid4vpHolder, credentialStore: CredentialStore, sdJwtService: SdJwtService) {
self.holder = holder
self.credentialStore = credentialStore
self.sdJwtService = sdJwtService
}
func handlePresentationRequest(requestUri: String) async throws -> PresentationResult {
// 1. Parse the authorization request
let parseResult = try await holder.parseAuthorizationRequest(
requestUri: requestUri,
walletConfig: WalletConfig(audience: "https://wallet.example.com")
)
guard parseResult.isOk else {
return .error("Failed to parse request")
}
// 2. Resolve request (fetch metadata, validate client)
let resolveResult = try await holder.resolveAuthorizationRequest(
request: parseResult.value
)
guard resolveResult.isOk else {
return .error("Failed to resolve request")
}
let resolved = resolveResult.value
// 3. Check if we have matching credentials
let matchingCredentials = findMatchingCredentials(
query: resolved.dcqlQuery,
credentials: credentialStore.getAllCredentials()
)
guard !matchingCredentials.isEmpty else {
return .noMatchingCredentials
}
// 4. Show consent UI and wait for user decision
let userConsent = await showConsentUI(
verifier: resolved.verifierInfo,
requestedCredentials: resolved.dcqlQuery,
availableCredentials: matchingCredentials
)
guard userConsent.approved else {
return .userDeclined
}
// 5. Create presentations for selected credentials
var selectedCredentials: [SelectedCredential] = []
for selection in userConsent.selectedCredentials {
let presentation: String
switch selection.format {
case "dc+sd-jwt":
presentation = try await createSdJwtPresentation(
credential: selection.credential,
claims: selection.disclosedClaims,
audience: resolved.verifierInfo.clientId,
nonce: resolved.request.nonce
)
case "mso_mdoc":
presentation = try await createMdocPresentation(
credential: selection.credential,
claims: selection.disclosedClaims
)
default:
throw NSError(domain: "Unsupported format", code: -1)
}
selectedCredentials.append(SelectedCredential(
credentialQueryId: selection.queryId,
credentialId: selection.credential.id,
presentation: presentation,
format: selection.format,
disclosedClaims: selection.disclosedClaims
))
}
// 6. Create and submit response
let responseResult = try await holder.createAuthorizationResponse(
request: resolved,
selectedCredentials: selectedCredentials
)
guard responseResult.isOk else {
return .error("Failed to create response")
}
let submitResult = try await holder.submitAuthorizationResponse(
resolvedRequest: resolved,
response: responseResult.value
)
guard submitResult.isOk else {
return .error("Failed to submit")
}
if let success = submitResult.value as? SubmissionResult.Success {
return .success(redirectUri: success.redirectUri)
} else {
return .error("Submission rejected")
}
}
}
enum PresentationResult {
case success(redirectUri: String?)
case error(String)
case noMatchingCredentials
case userDeclined
}
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
}