Skip to main content
Version: v0.13

OID4VP Verifier

The OID4VP verifier component enables relying party applications to request and verify credential presentations. This guide covers creating authorization requests, processing responses, and validating presented credentials.

Obtaining the Verifier Service

val rp: Oid4vpRpService = session.component.oid4vpRpService

Creating Authorization Requests

Build an authorization request specifying what credentials you need from the holder using DCQL.

// Build the DCQL query
val dcqlQuery = DcqlQuery(
credentials = listOf(
DcqlCredentialQuery(
id = "identity_credential",
format = "mso_mdoc",
meta = buildJsonObject {
put("doctype_value", "org.iso.18013.5.1.mDL")
},
claims = listOf(
DcqlClaimQuery(path = listOf("org.iso.18013.5.1", "family_name")),
DcqlClaimQuery(path = listOf("org.iso.18013.5.1", "given_name")),
DcqlClaimQuery(path = listOf("org.iso.18013.5.1", "birth_date"))
)
)
)
)

// Create the authorization request
val createResult = rp.createAuthorizationRequest(
CreateAuthorizationRequestArgs(
dcqlQuery = dcqlQuery,
clientId = "https://verifier.example.com",
responseUri = "https://verifier.example.com/callback",
responseMode = ResponseMode.DIRECT_POST,
nonce = generateSecureNonce(), // Minimum 8 characters
state = generateSessionId(),
clientIdScheme = ClientIdScheme.REDIRECT_URI
)
)

if (createResult.isOk) {
val created = createResult.value
val authRequest = created.request
val sessionId = created.sessionId // For correlating responses
}

Building the Request URI

Generate the URI for QR code or redirect:

val uriResult = rp.buildAuthorizationRequestUri(
BuildAuthorizationRequestUriArgs(
request = authRequest,
scheme = Oid4vpUriScheme.OPENID4VP // openid4vp://
)
)

if (uriResult.isOk) {
val requestUri = uriResult.value
// requestUri: openid4vp://authorize?client_id=...&response_uri=...&dcql_query=...

// Generate QR code for cross-device flow
val qrBitmap = QRCodeGenerator.generate(requestUri)

// Or use as deep link for same-device flow
openWallet(requestUri)
}

Signed Request Objects

For enhanced security, create a signed authorization request:

val signedResult = rp.createSignedAuthorizationRequest(
CreateSignedAuthorizationRequestArgs(
request = authRequest,
signingKey = verifierKeyInfo,
clientIdScheme = ClientIdScheme.X509_SAN_DNS
)
)

if (signedResult.isOk) {
val signedRequest = signedResult.value
// signedRequest.jwt contains the signed request object (JAR)

// Build URI with signed request
val uriResult = rp.buildAuthorizationRequestUri(
BuildAuthorizationRequestUriArgs(
request = signedRequest,
scheme = Oid4vpUriScheme.OPENID4VP
)
)
}

Parsing Authorization Responses

Handle the response from the holder's wallet:

// For direct_post response mode - handle POST to your callback endpoint
fun handleCallback(request: HttpRequest): HttpResponse {
val responseParams = mapOf(
"vp_token" to request.formParam("vp_token"),
"state" to request.formParam("state")
)

// Parse the response
val parseResult = rp.parseAuthorizationResponse(
ParseAuthorizationResponseArgs(
responseParams = responseParams,
originalRequest = authRequest // The original request for correlation
)
)

if (parseResult.isOk) {
val parsed = parseResult.value
val vpToken = parsed.vpToken
val state = parsed.state
}
}

Validating Authorization Responses

Validate the response against the original request:

val validateResult = rp.validateAuthorizationResponse(
ValidateAuthorizationResponseArgs(
parsedResponse = parsed,
originalRequest = authRequest,
dcqlQuery = dcqlQuery,
expectedNonce = originalNonce
)
)

if (validateResult.isOk) {
val validation = validateResult.value

if (validation.valid) {
// Access matched credentials
validation.matchedCredentials.forEach { matched ->
println("Credential: ${matched.credentialQueryId}")
println("Format: ${matched.format}")

// Access disclosed claims
matched.disclosedClaims.forEach { (key, value) ->
println(" $key: $value")
}
}
} else {
// Handle validation errors
validation.errors.forEach { error ->
println("Validation error: $error")
}
}
}

Verifying Holder Binding

Verify cryptographic holder binding for each credential:

// For each matched credential, verify holder binding
for (matched in validation.matchedCredentials) {
val bindingResult = rp.verifyHolderBinding(
VerifyHolderBindingArgs(
presentation = matched.presentation,
format = matched.format,
expectedNonce = originalNonce,
expectedAudience = "https://verifier.example.com"
)
)

if (bindingResult.isOk) {
val binding = bindingResult.value

if (binding.verified) {
println("Holder binding verified for ${matched.credentialQueryId}")
println("Signature valid: ${binding.signatureValid}")
println("Nonce valid: ${binding.nonceValid}")
println("Audience valid: ${binding.audienceValid}")

// For SD-JWT, also check sd_hash
binding.sdHashValid?.let { sdHashValid ->
println("SD Hash valid: $sdHashValid")
}
} else {
println("Holder binding failed: ${binding.errors}")
}
}
}

Response Code Protection

For direct_post mode, use response codes to protect against response theft:

// Backend receives direct_post response
fun handleDirectPost(request: HttpRequest): HttpResponse {
val responseParams = mapOf(
"vp_token" to request.formParam("vp_token"),
"state" to request.formParam("state")
)

// Handle response and generate response code
val handleResult = rp.handleDirectPostResponse(
HandleDirectPostResponseArgs(
responseParams = responseParams,
originalRequest = authRequest,
redirectUri = "https://frontend.example.com/callback",
responseCodeTtlSeconds = 300 // 5 minutes
)
)

if (handleResult.isOk) {
val handled = handleResult.value
// Return redirect with response_code
// Browser redirects to: https://frontend.example.com/callback?response_code=xxx
return HttpResponse.redirect(handled.redirectUri)
}
}

// Frontend retrieves the actual response using the code
fun retrieveResponse(responseCode: String) {
val retrieveResult = rp.retrieveAuthorizationResponse(
RetrieveAuthorizationResponseArgs(
responseCode = responseCode,
markAsUsed = true // Single use
)
)

if (retrieveResult.isOk) {
val response = retrieveResult.value
// Now validate the response
}
}

Complete Flow Example

class VerifierService(
private val rp: Oid4vpRpService
) {
private val sessionStore = mutableMapOf<String, VerificationSession>()

// Step 1: Create authorization request
suspend fun startVerification(): String {
val nonce = generateSecureNonce()
val state = generateSessionId()

val dcqlQuery = DcqlQuery(
credentials = listOf(
DcqlCredentialQuery(
id = "identity",
format = "dc+sd-jwt",
claims = listOf(
DcqlClaimQuery(path = listOf("given_name")),
DcqlClaimQuery(path = listOf("family_name"))
)
)
)
)

val createResult = rp.createAuthorizationRequest(
CreateAuthorizationRequestArgs(
dcqlQuery = dcqlQuery,
clientId = "https://verifier.example.com",
responseUri = "https://verifier.example.com/callback",
responseMode = ResponseMode.DIRECT_POST,
nonce = nonce,
state = state
)
)

val request = createResult.value.request

// Store session for later validation
sessionStore[state] = VerificationSession(
request = request,
dcqlQuery = dcqlQuery,
nonce = nonce
)

// Build URI for QR code
val uriResult = rp.buildAuthorizationRequestUri(
BuildAuthorizationRequestUriArgs(
request = request,
scheme = Oid4vpUriScheme.OPENID4VP
)
)

return uriResult.value
}

// Step 2: Handle callback
suspend fun handleCallback(vpToken: String, state: String): VerificationResult {
val session = sessionStore[state]
?: return VerificationResult.Error("Unknown session")

// Parse response
val parseResult = rp.parseAuthorizationResponse(
ParseAuthorizationResponseArgs(
responseParams = mapOf("vp_token" to vpToken, "state" to state),
originalRequest = session.request
)
)

if (!parseResult.isOk) {
return VerificationResult.Error("Failed to parse response")
}

// Validate response
val validateResult = rp.validateAuthorizationResponse(
ValidateAuthorizationResponseArgs(
parsedResponse = parseResult.value,
originalRequest = session.request,
dcqlQuery = session.dcqlQuery,
expectedNonce = session.nonce
)
)

if (!validateResult.isOk || !validateResult.value.valid) {
return VerificationResult.Error("Validation failed")
}

// Verify holder binding for each credential
for (matched in validateResult.value.matchedCredentials) {
val bindingResult = rp.verifyHolderBinding(
VerifyHolderBindingArgs(
presentation = matched.presentation,
format = matched.format,
expectedNonce = session.nonce,
expectedAudience = "https://verifier.example.com"
)
)

if (!bindingResult.isOk || !bindingResult.value.verified) {
return VerificationResult.Error("Holder binding verification failed")
}
}

// Clean up session
sessionStore.remove(state)

return VerificationResult.Success(
claims = validateResult.value.matchedCredentials
.flatMap { it.disclosedClaims.entries }
.associate { it.key to it.value }
)
}
}

sealed class VerificationResult {
data class Success(val claims: Map<String, Any?>) : VerificationResult()
data class Error(val message: String) : VerificationResult()
}

data class VerificationSession(
val request: AuthorizationRequest,
val dcqlQuery: DcqlQuery,
val nonce: String
)

Data Types

CreateAuthorizationRequestArgs

data class CreateAuthorizationRequestArgs(
val dcqlQuery: DcqlQuery,
val clientId: String,
val responseUri: String? = null,
val redirectUri: String? = null,
val responseMode: ResponseMode = ResponseMode.DIRECT_POST,
val nonce: String, // Required, minimum 8 characters
val state: String? = null,
val clientMetadata: ClientMetadata? = null,
val clientIdScheme: ClientIdScheme = ClientIdScheme.REDIRECT_URI
)

ValidationResult

data class ValidationResult(
val valid: Boolean,
val matchedCredentials: List<MatchedCredential> = emptyList(),
val errors: List<String> = emptyList()
)

data class MatchedCredential(
val credentialQueryId: String,
val format: String,
val presentation: String,
val disclosedClaims: Map<String, Any?> = emptyMap()
)

HolderBindingResult

data class HolderBindingResult(
val verified: Boolean,
val holderKey: String? = null,
val bindingMethod: String? = null,
val signatureValid: Boolean = false,
val nonceValid: Boolean = false,
val audienceValid: Boolean = false,
val sdHashValid: Boolean? = null,
val errors: List<String> = emptyList()
)