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
- Android/Kotlin
- iOS/Swift
val rp: Oid4vpRpService = session.component.oid4vpRpService
let rp: Oid4vpRpService = session.component.oid4vpRpService
Creating Authorization Requests
Build an authorization request specifying what credentials you need from the holder using DCQL.
- Android/Kotlin
- iOS/Swift
// 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
}
// Build the DCQL query
let dcqlQuery = DcqlQuery(
credentials: [
DcqlCredentialQuery(
id: "identity_credential",
format: "mso_mdoc",
meta: ["doctype_value": "org.iso.18013.5.1.mDL"],
claims: [
DcqlClaimQuery(path: ["org.iso.18013.5.1", "family_name"]),
DcqlClaimQuery(path: ["org.iso.18013.5.1", "given_name"]),
DcqlClaimQuery(path: ["org.iso.18013.5.1", "birth_date"])
]
)
]
)
// Create the authorization request
let createResult = try await rp.createAuthorizationRequest(
args: CreateAuthorizationRequestArgs(
dcqlQuery: dcqlQuery,
clientId: "https://verifier.example.com",
responseUri: "https://verifier.example.com/callback",
responseMode: .directPost,
nonce: generateSecureNonce(), // Minimum 8 characters
state: generateSessionId(),
clientIdScheme: .redirectUri
)
)
if createResult.isOk {
let created = createResult.value
let authRequest = created.request
let sessionId = created.sessionId // For correlating responses
}
Building the Request URI
Generate the URI for QR code or redirect:
- Android/Kotlin
- iOS/Swift
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)
}
let uriResult = try await rp.buildAuthorizationRequestUri(
args: BuildAuthorizationRequestUriArgs(
request: authRequest,
scheme: .openid4vp // openid4vp://
)
)
if uriResult.isOk {
let requestUri = uriResult.value
// requestUri: openid4vp://authorize?client_id=...&response_uri=...&dcql_query=...
// Generate QR code for cross-device flow
let qrImage = generateQRCode(from: requestUri)
// Or use as deep link for same-device flow
openWallet(uri: requestUri)
}
Signed Request Objects
For enhanced security, create a signed authorization request:
- Android/Kotlin
- iOS/Swift
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
)
)
}
let signedResult = try await rp.createSignedAuthorizationRequest(
args: CreateSignedAuthorizationRequestArgs(
request: authRequest,
signingKey: verifierKeyInfo,
clientIdScheme: .x509SanDns
)
)
if signedResult.isOk {
let signedRequest = signedResult.value
// signedRequest.jwt contains the signed request object (JAR)
// Build URI with signed request
let uriResult = try await rp.buildAuthorizationRequestUri(
args: BuildAuthorizationRequestUriArgs(
request: signedRequest,
scheme: .openid4vp
)
)
}
Parsing Authorization Responses
Handle the response from the holder's wallet:
- Android/Kotlin
- iOS/Swift
// 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
}
}
// For direct_post response mode - handle POST to your callback endpoint
func handleCallback(request: HttpRequest) async throws -> HttpResponse {
let responseParams = [
"vp_token": request.formParam(name: "vp_token"),
"state": request.formParam(name: "state")
]
// Parse the response
let parseResult = try await rp.parseAuthorizationResponse(
args: ParseAuthorizationResponseArgs(
responseParams: responseParams,
originalRequest: authRequest // The original request for correlation
)
)
if parseResult.isOk {
let parsed = parseResult.value
let vpToken = parsed.vpToken
let state = parsed.state
}
}
Validating Authorization Responses
Validate the response against the original request:
- Android/Kotlin
- iOS/Swift
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")
}
}
}
let validateResult = try await rp.validateAuthorizationResponse(
args: ValidateAuthorizationResponseArgs(
parsedResponse: parsed,
originalRequest: authRequest,
dcqlQuery: dcqlQuery,
expectedNonce: originalNonce
)
)
if validateResult.isOk {
let validation = validateResult.value
if validation.valid {
// Access matched credentials
for matched in validation.matchedCredentials {
print("Credential: \(matched.credentialQueryId)")
print("Format: \(matched.format)")
// Access disclosed claims
for (key, value) in matched.disclosedClaims {
print(" \(key): \(value)")
}
}
} else {
// Handle validation errors
for error in validation.errors {
print("Validation error: \(error)")
}
}
}
Verifying Holder Binding
Verify cryptographic holder binding for each credential:
- Android/Kotlin
- iOS/Swift
// 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}")
}
}
}
// For each matched credential, verify holder binding
for matched in validation.matchedCredentials {
let bindingResult = try await rp.verifyHolderBinding(
args: VerifyHolderBindingArgs(
presentation: matched.presentation,
format: matched.format,
expectedNonce: originalNonce,
expectedAudience: "https://verifier.example.com"
)
)
if bindingResult.isOk {
let binding = bindingResult.value
if binding.verified {
print("Holder binding verified for \(matched.credentialQueryId)")
print("Signature valid: \(binding.signatureValid)")
print("Nonce valid: \(binding.nonceValid)")
print("Audience valid: \(binding.audienceValid)")
// For SD-JWT, also check sd_hash
if let sdHashValid = binding.sdHashValid {
print("SD Hash valid: \(sdHashValid)")
}
} else {
print("Holder binding failed: \(binding.errors)")
}
}
}
Response Code Protection
For direct_post mode, use response codes to protect against response theft:
- Android/Kotlin
- iOS/Swift
// 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
}
}
// Backend receives direct_post response
func handleDirectPost(request: HttpRequest) async throws -> HttpResponse {
let responseParams = [
"vp_token": request.formParam(name: "vp_token"),
"state": request.formParam(name: "state")
]
// Handle response and generate response code
let handleResult = try await rp.handleDirectPostResponse(
args: HandleDirectPostResponseArgs(
responseParams: responseParams,
originalRequest: authRequest,
redirectUri: "https://frontend.example.com/callback",
responseCodeTtlSeconds: 300 // 5 minutes
)
)
if handleResult.isOk {
let handled = handleResult.value
// Return redirect with response_code
return HttpResponse.redirect(url: handled.redirectUri)
}
}
// Frontend retrieves the actual response using the code
func retrieveResponse(responseCode: String) async throws {
let retrieveResult = try await rp.retrieveAuthorizationResponse(
args: RetrieveAuthorizationResponseArgs(
responseCode: responseCode,
markAsUsed: true // Single use
)
)
if retrieveResult.isOk {
let response = retrieveResult.value
// Now validate the response
}
}
Complete Flow Example
- Android/Kotlin
- iOS/Swift
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
)
class VerifierService {
private let rp: Oid4vpRpService
private var sessionStore: [String: VerificationSession] = [:]
init(rp: Oid4vpRpService) {
self.rp = rp
}
// Step 1: Create authorization request
func startVerification() async throws -> String {
let nonce = generateSecureNonce()
let state = generateSessionId()
let dcqlQuery = DcqlQuery(
credentials: [
DcqlCredentialQuery(
id: "identity",
format: "dc+sd-jwt",
claims: [
DcqlClaimQuery(path: ["given_name"]),
DcqlClaimQuery(path: ["family_name"])
]
)
]
)
let createResult = try await rp.createAuthorizationRequest(
args: CreateAuthorizationRequestArgs(
dcqlQuery: dcqlQuery,
clientId: "https://verifier.example.com",
responseUri: "https://verifier.example.com/callback",
responseMode: .directPost,
nonce: nonce,
state: state
)
)
let request = createResult.value.request
// Store session for later validation
sessionStore[state] = VerificationSession(
request: request,
dcqlQuery: dcqlQuery,
nonce: nonce
)
// Build URI for QR code
let uriResult = try await rp.buildAuthorizationRequestUri(
args: BuildAuthorizationRequestUriArgs(
request: request,
scheme: .openid4vp
)
)
return uriResult.value
}
// Step 2: Handle callback
func handleCallback(vpToken: String, state: String) async throws -> VerificationResult {
guard let session = sessionStore[state] else {
return .error("Unknown session")
}
// Parse response
let parseResult = try await rp.parseAuthorizationResponse(
args: ParseAuthorizationResponseArgs(
responseParams: ["vp_token": vpToken, "state": state],
originalRequest: session.request
)
)
guard parseResult.isOk else {
return .error("Failed to parse response")
}
// Validate response
let validateResult = try await rp.validateAuthorizationResponse(
args: ValidateAuthorizationResponseArgs(
parsedResponse: parseResult.value,
originalRequest: session.request,
dcqlQuery: session.dcqlQuery,
expectedNonce: session.nonce
)
)
guard validateResult.isOk, validateResult.value.valid else {
return .error("Validation failed")
}
// Verify holder binding for each credential
for matched in validateResult.value.matchedCredentials {
let bindingResult = try await rp.verifyHolderBinding(
args: VerifyHolderBindingArgs(
presentation: matched.presentation,
format: matched.format,
expectedNonce: session.nonce,
expectedAudience: "https://verifier.example.com"
)
)
guard bindingResult.isOk, bindingResult.value.verified else {
return .error("Holder binding verification failed")
}
}
// Clean up session
sessionStore.removeValue(forKey: state)
var claims: [String: Any] = [:]
for matched in validateResult.value.matchedCredentials {
for (key, value) in matched.disclosedClaims {
claims[key] = value
}
}
return .success(claims: claims)
}
}
enum VerificationResult {
case success(claims: [String: Any])
case error(String)
}
struct VerificationSession {
let request: AuthorizationRequest
let dcqlQuery: DcqlQuery
let 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()
)