Skip to main content
Version: v0.25.0 (Latest)

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:

LayerOwnsKey contents
IDKPortable contracts + OIDC/Wallet driversIdvNode graph model, IdvMethodDriver SPI, command interfaces, store interfaces, OidcMethodDriver, WalletMethodDriver
EDKExecution engine + enterprise driversGraph compiler, node dispatcher, join/failure policy evaluator, execution state machine, materialization engine, OTP/biometric/document/REST drivers, PostgreSQL persistence
VDXProduct-facing integrationAdmin CRUD APIs, tenant management, onboarding compositions, UI adapters

IDV materializes into the existing identity graph (PartyIdentityCorrelationIdentifierIdentityMatch), 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 PolicyBehavior
AllAll children must succeed, strictest, appropriate for high-assurance
AnyAt least one child must succeed, flexible, good for offering alternatives
BestEffortComplete as many as possible without ever failing the node, useful for enrichment
Failure PolicyBehavior
FailFastCancel remaining children on first failure, saves time and cost
WaitForAllWait for all children before reporting, collects all errors for better diagnostics
ContinueOnFailureKeep 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 PolicyBehavior
UserSelectPresent options to the user, they pick
HighestAssuranceAutomatically pick the method with highest LoA
LowestCostAutomatically pick the cheapest method
FirstAvailablePick 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

TypeDriverDescription
OIDCOidcMethodDriverFederated OpenID Connect authentication with PKCE, userInfo, and attribute mapping
WALLETWalletMethodDriverOID4VP wallet credential presentation with DCQL queries and trusted issuer validation
DOCUMENTPluggableDocument verification, OCR, liveness, NFC read. Providers: Onfido, Jumio, ReadID
BIOMETRICPluggableBiometric verification, face matching, fingerprint. Providers: iProov, Onfido, Jumio
OTPPluggableOne-time passcode via email, SMS, or authenticator app
CLAIM_MATCHBuilt-inVerify attributes against the identity matching store without external calls
REST_APIBuilt-inDelegate to a custom REST endpoint for verification logic

Driver Lifecycle

Each driver method maps to a phase of the verification step:

  1. dispatch: Start the verification. Returns either a PendingDispatch (redirect URL, QR code, form) or an ImmediateResult if no user interaction is needed.
  2. submit: Handle user input (form submission, OTP entry).
  3. callback: Process async callback from an external provider (OIDC redirect, wallet response, webhook).
  4. poll: Check status of an in-progress verification.
  5. 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:

ActionMeaning
RedirectActionUser must be redirected to an external URL (OIDC provider, document scanner)
UserInputActionUser must fill in a form (OTP code, personal details)
PollActionBackend is polling an external service; client should retry after interval
WaitForCallbackActionWaiting 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

FrameworkLevelsTypical use
eIDASLOW, SUBSTANTIAL, HIGHEU regulatory compliance, cross-border identity
NIST 800-63A (AAL)AAL1, AAL2, AAL3US federal systems, enterprise authentication

Evidence Strength

Each method also declares the strength of the evidence it produces, following NIST 800-63A terminology:

LevelMeaningExample
FAIRBasic electronic verificationEmail OTP, self-declared attributes
STRONGMulti-factor or document-backedPassport NFC read, institutional OIDC with MFA
SUPERIORIn-person or hardware-backedeIDAS 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:

RuleWhat it creates
CreateIdentifiersMaterializationCorrelationIdentifier records for resolved identifiers (email, subject ID, DID)
CreateIdentityMatchMaterializationHMAC-hashed IdentityMatch records linking external identifiers to the internal identity
CreateRelationshipMaterializationLinks between verified entities (e.g., student → institution)
CreateRegistrationMaterializationService-specific registration records
AttachEvidenceMaterializationPersists evidence artifacts (provider responses, timestamps, method metadata) for audit trails
MarkVerifiedMaterializationSets 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

CommandIDDescription
ResolveIdvUseCaseidv.usecase.resolveSelect a use case definition by trigger context
CompileIdvGraphidv.graph.compilePrepare a graph for execution
StartIdvExecutionidv.execution.startInitialize a verification session
DispatchIdvNodeidv.node.dispatchInvoke a method driver for a graph node
SubmitIdvNodeidv.node.submitHandle user form submission
HandleIdvNodeCallbackidv.node.callbackProcess async callback (OIDC redirect, wallet response)
PollIdvNodeidv.node.pollCheck status of an in-progress node
GetIdvExecutionidv.execution.getFetch current execution state
CancelIdvExecutionidv.execution.cancelCancel with reason
ApplyIdvMaterializationidv.materialization.applyCreate 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.