Identity Verification (IDV)
The IDV system provides composable verification workflows where multiple identity proofing methods are orchestrated as a directed graph. Nodes in the graph represent verification steps, OIDC login, wallet credential presentation, document scanning, biometric checks, OTP challenges, and the graph structure defines how they combine.
IDV is a standalone subsystem, it operates independently of wallets, OID4VP, and auth bridges. Any system that needs to verify an identity can use IDV. The reconciliation orchestrator and auth bridge are consumers of IDV, not the other way around.
Ownership Split
IDV spans three layers:
| Layer | Owns | Key contents |
|---|---|---|
| IDK | Portable contracts + OIDC/Wallet drivers | IdvNode graph model, IdvMethodDriver SPI, command interfaces, store interfaces, OidcMethodDriver, WalletMethodDriver |
| EDK | Execution engine + enterprise drivers | Graph compiler, node dispatcher, join/failure policy evaluator, execution state machine, materialization engine, OTP/biometric/document/REST drivers, PostgreSQL persistence |
| VDX | Product-facing integration | Admin CRUD APIs, tenant management, onboarding compositions, UI adapters |
IDV materializes into the existing identity graph (Party → Identity → CorrelationIdentifier → IdentityMatch), it does not introduce a parallel identity model.
Graph Composition
Rather than encoding verification logic as procedural code, IDV represents workflows as composable node graphs. This means a tenant's onboarding flow, "first log in with your institution, then scan your ID card, then do a liveness check", is a data structure, not a hardcoded sequence. Graphs can be stored in configuration, versioned per tenant, and evaluated by the engine without writing new code.
Verification workflows are defined as trees of IdvNode instances. Four composite node types control execution flow:
SequenceNode executes children in order. If any step fails, the entire sequence fails. Use this when steps must happen in a specific order, for example, an institutional OIDC login must complete before a document scan because the document verification needs the user's name from the OIDC claims.
ParallelNode executes children concurrently. This is useful when verification steps are independent, a document scan and a biometric check can run at the same time. The join policy controls how many children need to succeed, and the failure policy controls whether to cancel remaining children when one fails.
| Join Policy | Behavior |
|---|---|
All | All children must succeed, strictest, appropriate for high-assurance |
Any | At least one child must succeed, flexible, good for offering alternatives |
BestEffort | Complete as many as possible without ever failing the node, useful for enrichment |
| Failure Policy | Behavior |
|---|---|
FailFast | Cancel remaining children on first failure, saves time and cost |
WaitForAll | Wait for all children before reporting, collects all errors for better diagnostics |
ContinueOnFailure | Keep running regardless, appropriate when combined with BestEffort join |
ChoiceNode presents alternatives. The user or the system selects one child to execute. This handles cases like "verify with passport OR ID card" where the user picks their preferred method.
| Selection Policy | Behavior |
|---|---|
UserSelect | Present options to the user, they pick |
HighestAssurance | Automatically pick the method with highest LoA |
LowestCost | Automatically pick the cheapest method |
FirstAvailable | Pick the first available method |
ThresholdNode requires at least N children to succeed. This is the most flexible composition, "2-of-3: OIDC, document, biometric" means the user can fail one method and still pass. This is useful for inclusive flows where not all users have access to every verification method.
Leaf nodes are MethodNode instances, each referencing a verification method driver by ID.
Example Workflows
A customer onboarding flow that requires email verification, then a document scan, then a biometric liveness check:
val customerOnboarding = sequenceNode {
methodNode("email-verify", methodId = "otp-email")
choiceNode(selectionPolicy = UserSelect) {
methodNode("passport-scan", methodId = "document-passport")
methodNode("id-card-scan", methodId = "document-id-card")
}
methodNode("liveness-check", methodId = "biometric-iproov")
}
A lighter employee onboarding that accepts either institutional OIDC login or a wallet presentation:
val employeeOnboarding = choiceNode(selectionPolicy = UserSelect) {
methodNode("institutional-login", methodId = "oidc-corporate-ad")
methodNode("wallet-presentation", methodId = "wallet-eudiw-pid")
}
A high-assurance flow where at least 2 of 3 methods must succeed:
val highAssurance = thresholdNode(minimumSuccesses = 2) {
methodNode("oidc-login", methodId = "oidc-eidas-node")
methodNode("document-scan", methodId = "document-passport-nfc")
methodNode("biometric", methodId = "biometric-face")
}
Method Drivers
The graph defines the structure of the workflow, but the actual verification logic lives in method drivers. A driver is a pluggable implementation that knows how to talk to a specific external system, an OIDC provider, a document scanning service, a biometric API, or a wallet. The IDV engine dispatches work to drivers and collects results without knowing the details of any particular verification method.
This separation means adding a new verification provider (say, a new biometric vendor) requires implementing one driver interface, not changing the engine or the graph model.
Each verification method is implemented as an IdvMethodDriver:
interface IdvMethodDriver {
val methodType: IdvMethodType
suspend fun dispatch(work: DispatchWork): IdkResult<DispatchOutcome, IdvError>
suspend fun submit(work: SubmitWork): IdkResult<SubmitOutcome, IdvError>
suspend fun callback(work: CallbackWork): IdkResult<CallbackOutcome, IdvError>
suspend fun poll(work: PollWork): IdkResult<PollOutcome, IdvError>
suspend fun cancel(work: CancelWork): IdkResult<CancelOutcome, IdvError>
}
Supported Methods
| Type | Driver | Description |
|---|---|---|
OIDC | OidcMethodDriver | Federated OpenID Connect authentication with PKCE, userInfo, and attribute mapping |
WALLET | WalletMethodDriver | OID4VP wallet credential presentation with DCQL queries and trusted issuer validation |
DOCUMENT | Pluggable | Document verification, OCR, liveness, NFC read. Providers: Onfido, Jumio, ReadID |
BIOMETRIC | Pluggable | Biometric verification, face matching, fingerprint. Providers: iProov, Onfido, Jumio |
OTP | Pluggable | One-time passcode via email, SMS, or authenticator app |
CLAIM_MATCH | Built-in | Verify attributes against the identity matching store without external calls |
REST_API | Built-in | Delegate to a custom REST endpoint for verification logic |
Driver Lifecycle
Each driver method maps to a phase of the verification step:
dispatch: Start the verification. Returns either aPendingDispatch(redirect URL, QR code, form) or anImmediateResultif no user interaction is needed.submit: Handle user input (form submission, OTP entry).callback: Process async callback from an external provider (OIDC redirect, wallet response, webhook).poll: Check status of an in-progress verification.cancel: Clean up resources for a cancelled verification.
OIDC Method
The OidcMethodDriver dispatches an authorization request with PKCE and processes the callback:
data class OidcMethodDefinition(
override val id: String,
val discoveryUrl: String,
val clientIdRef: ConfigRef,
val clientSecretRef: SecretRef?,
val scopes: Set<String> = setOf("openid", "profile", "email"),
val attributeMappings: List<AttributeMapping>,
val subjectBinding: SubjectBinding?,
// ...
)
On callback, it exchanges the authorization code for tokens, validates the JWT, extracts claims via the configured attribute mappings, and returns identifiers and assurance metadata.
Wallet Method
The WalletMethodDriver creates an OID4VP authorization request using DCQL queries:
data class WalletMethodDefinition(
override val id: String,
val dcqlQuery: DcqlQuery,
val trustedIssuers: List<TrustedIssuer>,
val credentialTypes: Set<String>,
val acceptedLoAs: Set<String>,
// ...
)
It dispatches a QR code or deep link, waits for the wallet to present credentials, verifies them against trusted issuers, and extracts disclosed attributes.
Execution Model
When a use case is triggered, the engine creates an IdvExecution, a persistent, resumable representation of the running workflow. The execution tracks per-node state, accumulated attributes from completed nodes, pending user actions, and errors. Because the execution is persisted, the workflow survives server restarts and can be resumed asynchronously, the user can close their browser, return hours later, and continue where they left off.
The execution uses optimistic concurrency (version field) to prevent conflicting updates when multiple callbacks arrive simultaneously.
IdvExecution
data class IdvExecution(
val executionId: IdvExecutionId,
val useCaseId: IdvUseCaseId,
val tenantId: String,
val subject: IdvSubjectRef,
val status: IdvExecutionStatus,
val nodeStates: Map<IdvNodeId, IdvNodeState>,
val currentPendingActions: List<NodePendingAction>,
val accumulatedAttributes: AttributeBag,
val results: List<IdvNodeResult>,
val errors: List<IdvError>,
val createdAt: Instant,
val expiresAt: Instant,
)
Status Lifecycle
CREATED → RUNNING → AWAITING_ACTION → COMPLETED
↘ FAILED / EXPIRED / CANCELLED
AWAITING_ACTION means the execution is waiting for user interaction, a redirect return, form submission, or wallet scan. The currentPendingActions list describes what's needed:
| Action | Meaning |
|---|---|
RedirectAction | User must be redirected to an external URL (OIDC provider, document scanner) |
UserInputAction | User must fill in a form (OTP code, personal details) |
PollAction | Backend is polling an external service; client should retry after interval |
WaitForCallbackAction | Waiting for an async webhook or wallet response |
Attribute Flow
Attributes flow through the graph via AttributeBinding:
data class AttributeBinding(
val source: AttributeSource, // ContextAttribute, PriorNodeAttribute, UserInputAttribute
val target: AttributePath
)
A later node can bind its input to an earlier node's output, for example, the biometric node can receive the photo extracted by the document node.
Node Results
Each completed node produces an IdvNodeResult containing:
- Identifiers: Resolved identifiers (subject ID, email, DID)
- Attributes: Extracted and mapped attributes
- Assurance: LoA achieved by this method
- AMR: Authentication method references (e.g.,
mfa,face,hwk) - Evidence: Audit record with provider, method type, timestamp, and metadata
Results are aggregated across the graph into an IdvExecutionResult with the highest assurance level, combined AMR, and all resolved identifiers.
Assurance & Compliance
Identity verification is not just about confirming someone's identity, it's about doing so at a specific level of trust. A self-declared email address is a different level of confidence than a government-issued PID verified through an eIDAS node with biometric liveness. The IDV system tracks this through assurance levels and compliance profiles.
Every method definition declares the maximum assurance it can produce, and every use case declares the minimum assurance it requires. The engine enforces the threshold: if the combined assurance from completed nodes doesn't meet the use case's minimum, the execution fails with an AssuranceError even if all methods succeeded technically.
Assurance Levels
| Framework | Levels | Typical use |
|---|---|---|
| eIDAS | LOW, SUBSTANTIAL, HIGH | EU regulatory compliance, cross-border identity |
| NIST 800-63A (AAL) | AAL1, AAL2, AAL3 | US federal systems, enterprise authentication |
Evidence Strength
Each method also declares the strength of the evidence it produces, following NIST 800-63A terminology:
| Level | Meaning | Example |
|---|---|---|
FAIR | Basic electronic verification | Email OTP, self-declared attributes |
STRONG | Multi-factor or document-backed | Passport NFC read, institutional OIDC with MFA |
SUPERIOR | In-person or hardware-backed | eIDAS high with certified hardware token |
Compliance Profiles
For regulated environments, each method definition can declare a compliance profile. This controls which methods are available in which jurisdictions and ensures the system collects required consent before processing biometric data or transferring data across borders.
Profiles specify:
- Regulatory frameworks:
EIDAS,NIST_800_63A,UK_DIATF,DE_AML, so the engine knows which rules apply - Proofing scenario:
FACE_TO_FACE,REMOTE_ASSISTED,UNATTENDED_REMOTE, which affects the maximum achievable assurance - Consent requirements:
BIOMETRIC_PROCESSING,DATA_SHARING,CROSS_BORDER_TRANSFER, the engine blocks execution until required consent is collected - Territory restrictions: per-country rules that prohibit certain methods or require additional legal basis for specific identifiers (e.g., BSN in the Netherlands)
The execution result is evaluated against the use case's minimum assurance and compliance requirements. If a method produces evidence that doesn't meet the regulatory framework's requirements for the target assurance level, the engine treats it as insufficient.
Materialization
Verification alone doesn't change any persistent state. The results of a successful IDV execution, resolved identifiers, verified attributes, evidence records, need to be written into the identity graph. Materialization rules declare what should happen after the execution completes, as typed instructions rather than procedural code.
This declarative approach matters because materialization rules can be inspected before they run (for policy review), replayed if a write fails (for resilience), and projected differently per deployment (a test environment might skip party creation while production creates real records).
Each use case definition includes a list of IdvMaterializationRule instances:
| Rule | What it creates |
|---|---|
CreateIdentifiersMaterialization | CorrelationIdentifier records for resolved identifiers (email, subject ID, DID) |
CreateIdentityMatchMaterialization | HMAC-hashed IdentityMatch records linking external identifiers to the internal identity |
CreateRelationshipMaterialization | Links between verified entities (e.g., student → institution) |
CreateRegistrationMaterialization | Service-specific registration records |
AttachEvidenceMaterialization | Persists evidence artifacts (provider responses, timestamps, method metadata) for audit trails |
MarkVerifiedMaterialization | Sets the verified flag and assurance level on the identity |
Materializations are what connect IDV results to the identity matching layer, the CreateIdentityMatchMaterialization rule is how a verified identity gets its HMAC-hashed binding, enabling fast-path recognition on subsequent visits.
Commands
| Command | ID | Description |
|---|---|---|
ResolveIdvUseCase | idv.usecase.resolve | Select a use case definition by trigger context |
CompileIdvGraph | idv.graph.compile | Prepare a graph for execution |
StartIdvExecution | idv.execution.start | Initialize a verification session |
DispatchIdvNode | idv.node.dispatch | Invoke a method driver for a graph node |
SubmitIdvNode | idv.node.submit | Handle user form submission |
HandleIdvNodeCallback | idv.node.callback | Process async callback (OIDC redirect, wallet response) |
PollIdvNode | idv.node.poll | Check status of an in-progress node |
GetIdvExecution | idv.execution.get | Fetch current execution state |
CancelIdvExecution | idv.execution.cancel | Cancel with reason |
ApplyIdvMaterialization | idv.materialization.apply | Create post-IDV artifacts |
Configuration
IDV has two configuration layers that are managed separately:
Method definitions describe individual verification systems, an OIDC provider, an OTP email channel, a document scanning service, a biometric API. Methods are configured once and reused across multiple use cases. They can be scoped to APP level (available to all tenants) or TENANT level (tenant-specific overrides or additions).
Use case definitions compose methods into verification scenarios, "customer onboarding requires email OTP then document scan" or "employee registration accepts either institutional OIDC or wallet PID." Each use case references methods by ID, defines the graph structure, and declares assurance and compliance requirements.
This separation means an organization can configure its OIDC providers, OTP channels, and document scanning integrations once, then compose them into different use cases for different tenants or scenarios.
JSON Configuration
Definitions are loaded via IdvDefinitionConfigBinder. The _type discriminator enables polymorphic deserialization of different method types:
{
"methods": [
{
"_type": "OidcMethodDefinition",
"id": "corporate-ad",
"type": "oidc",
"discoveryUrl": "https://login.microsoftonline.com/tenant/v2.0/.well-known/openid-configuration",
"clientIdRef": { "key": "idv.oidc.corporate.client-id" },
"clientSecretRef": { "key": "idv.oidc.corporate.client-secret" },
"scopes": ["openid", "profile", "email"],
"attributeMappings": [
{ "sourceAttribute": "sub", "targetAttribute": "subject.id", "identifierType": "SUBJECT_ID" },
{ "sourceAttribute": "email", "targetAttribute": "contact.email", "identifierType": "EMAIL" }
]
},
{
"_type": "WalletMethodDefinition",
"id": "wallet-pid",
"type": "wallet",
"credentialTypes": ["eu.europa.ec.eudi.pid.1"],
"acceptedLoAs": ["substantial", "high"]
},
{
"_type": "OtpMethodDefinition",
"id": "email-otp",
"type": "otp",
"channel": "email",
"codeLength": 6,
"codeTtlSeconds": 300,
"maxAttempts": 3
},
{
"_type": "DocumentMethodDefinition",
"id": "passport-nfc",
"type": "document",
"provider": "readid",
"documentTypes": ["passport"],
"nfcEnabled": true
}
],
"useCases": [
{
"id": "customer-onboarding",
"tenantId": "default",
"graph": {
"_type": "SequenceNode",
"children": [
{ "_type": "MethodNode", "nodeId": "step-email", "methodId": "email-otp" },
{ "_type": "ChoiceNode", "selectionPolicy": "UserSelect", "children": [
{ "_type": "MethodNode", "nodeId": "step-passport", "methodId": "passport-nfc" },
{ "_type": "MethodNode", "nodeId": "step-wallet", "methodId": "wallet-pid" }
]}
]
},
"policy": {
"minimumAssurance": "substantial",
"sessionTtlSeconds": 3600
}
},
{
"id": "employee-registration",
"tenantId": "default",
"graph": {
"_type": "MethodNode",
"nodeId": "step-oidc",
"methodId": "corporate-ad"
},
"policy": {
"minimumAssurance": "low",
"sessionTtlSeconds": 600
}
}
]
}
Secret references (clientIdRef, clientSecretRef) are resolved through the EDK's configuration system, so they can come from environment variables, cloud config providers, or vault, sensitive credentials are never stored in the JSON definition itself.