bles real-time verification at scale.
Core Solution
Implementing attribute-based verification requires rethinking the verification pipeline. The flow moves from asynchronous document processing to synchronous cryptographic presentation exchange. Below is a production-ready implementation pattern using the OpenEUDI SDK.
Step 1: Initialize the Verifier with Trust Anchors
The verifier must be configured with trusted issuer certificates and attribute definitions. Trust anchors are typically sourced from ETSI Trusted Lists or national WRPAC (Wallet Provider and Relying Party Accreditation) registries.
import { VerifierEngine, AttributeSchema, TrustAnchorRegistry } from '@openeudi/core';
import { OpenId4VpClient } from '@openeudi/openid4vp';
const trustRegistry = new TrustAnchorRegistry({
source: 'etsi_tl',
refreshInterval: '24h',
fallback: 'local_pinned_certs',
});
const attributeCatalog: Record<string, AttributeSchema> = {
age_over_18: { type: 'boolean', mandatory: true },
country_residence: { type: 'string', format: 'ISO_3166-1_alpha-2' },
legal_name: { type: 'string', optional: true },
};
const verifier = new VerifierEngine({
trustRegistry,
attributeCatalog,
presentationMode: 'qr_code',
sessionTTL: '5m',
});
Architecture Rationale: Trust anchors are decoupled from the verification logic. This allows certificate rotation without redeploying the application. The attribute catalog enforces schema validation at the protocol level, preventing malformed or unexpected claims from entering your business logic.
Step 2: Generate Presentation Request and QR Payload
Instead of redirecting users to a third-party hosted page, your server generates a presentation request that the wallet resolves locally.
import express from 'express';
import { v4 as uuidv4 } from 'uuid';
const router = express.Router();
router.post('/identity/verify', async (req, res) => {
const sessionId = uuidv4();
const requestedAttributes = ['age_over_18', 'country_residence'];
const presentationRequest = await verifier.createPresentationRequest({
sessionId,
attributes: requestedAttributes,
callbackEndpoint: `/identity/verify/${sessionId}/stream`,
});
res.json({
sessionId,
qrPayload: presentationRequest.uri,
expiresAt: presentationRequest.expiry,
});
});
Architecture Rationale: The QR payload contains a standardized OpenID4VP request URI. The wallet resolves it locally, signs the presentation with the user's private key, and returns a Verifiable Presentation (VP). This eliminates third-party redirects and keeps the verification flow within your control plane.
Step 3: Stream Results via Server-Sent Events
Traditional KYC relies on webhooks with retry queues and idempotency keys. OpenID4VP uses Server-Sent Events (SSE) for real-time, stateless delivery.
router.get('/identity/verify/:id/stream', async (req, res) => {
const { id } = req.params;
const session = verifier.getSession(id);
if (!session) {
res.status(404).json({ error: 'Session expired or invalid' });
return;
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const streamTimeout = setTimeout(() => {
res.write(`event: timeout\ndata: {"message": "Verification window closed"}\n\n`);
res.end();
}, 300_000); // 5 minutes
session.onPresentationReceived(async (vp) => {
try {
const validated = await verifier.validatePresentation(vp);
// Extract only requested attributes
const claims = validated.claims.reduce((acc, curr) => {
acc[curr.attributeId] = curr.value;
return acc;
}, {} as Record<string, unknown>);
res.write(`event: verified\ndata: ${JSON.stringify({ claims, verifiedAt: new Date().toISOString() })}\n\n`);
clearTimeout(streamTimeout);
res.end();
} catch (err) {
res.write(`event: error\ndata: {"message": "Cryptographic validation failed"}\n\n`);
res.end();
}
});
req.on('close', () => {
clearTimeout(streamTimeout);
session.detach();
});
});
Architecture Rationale: SSE eliminates webhook infrastructure entirely. The connection is stateless, client-driven, and naturally handles network drops without retry queues. Cryptographic validation occurs server-side using the issuer's public key from the trust registry. Only the requested attributes are extracted; no document images or full PII are generated.
Step 4: Map Claims to Internal User State
The final step translates cryptographic assertions into application state without storing raw claims.
async function provisionUserSession(claims: Record<string, unknown>, sessionId: string) {
const isAdult = claims.age_over_18 === true;
const jurisdiction = claims.country_residence as string;
// Audit log without PII
await auditLogger.record({
event: 'identity_verified',
sessionId,
attributesRequested: ['age_over_18', 'country_residence'],
attributesGranted: Object.keys(claims),
timestamp: new Date().toISOString(),
});
return {
verified: isAdult,
jurisdiction,
sessionToken: generateSecureToken(),
};
}
Architecture Rationale: Audit logs record metadata (what was requested, what was granted, when) without storing PII. This satisfies compliance requirements while maintaining data minimization. Session tokens are generated independently, decoupling identity proof from authentication state.
Pitfall Guide
1. Over-Requesting Attributes
Explanation: Developers often request full name, DOB, and address alongside age verification. This violates GDPR data minimization, increases user friction, and negates the privacy benefits of verifiable credentials.
Fix: Request only the boolean or categorical attributes required for the business rule. Use age_over_18 instead of date_of_birth. Map jurisdiction codes to compliance rules server-side.
2. Skipping Cryptographic Signature Validation
Explanation: Assuming the presentation payload is trustworthy because it arrived via SSE. Without validating the issuer's signature against the trust anchor registry, you risk accepting forged or expired credentials.
Fix: Always run verifier.validatePresentation(vp) before extracting claims. Pin trust anchors in production and implement certificate rotation monitoring.
3. Ignoring SSE Connection Lifecycle
Explanation: Treating SSE as a fire-and-forget channel. Clients disconnect, proxies timeout, and mobile networks drop connections. Unhandled stream closures leak server resources and orphan verification sessions.
Fix: Implement explicit timeout handlers, detach session listeners on req.on('close'), and use connection keep-alive headers. Monitor active streams with Prometheus/Grafana.
4. Assuming Global Coverage
Explanation: Building a verification pipeline that exclusively relies on EUDI Wallets. Currently, wallet issuance is limited to EU member states. Non-EU users will encounter dead ends.
Fix: Implement a fallback router. Detect user jurisdiction or wallet availability, then route to traditional KYC or alternative verification providers for non-EU traffic.
5. Mismanaging Trust Anchor Updates
Explanation: Hardcoding issuer certificates or failing to refresh ETSI Trusted Lists. Expired or revoked certificates cause valid presentations to fail validation, creating false negatives.
Fix: Automate trust anchor synchronization. Use the refreshInterval configuration, implement fallback pinned certificates, and alert on registry sync failures.
6. Treating Verifiable Presentations as Session Tokens
Explanation: Storing the VP or using it directly as an authentication cookie. VPs are cryptographic assertions, not session identifiers. Reusing them exposes you to replay attacks.
Fix: Extract claims, validate them, then issue a short-lived session token. Never persist the raw VP in your database.
7. Neglecting Immutable Audit Trails
Explanation: Assuming that because no PII is stored, compliance logging is unnecessary. Regulators and auditors require proof of verification events, attribute requests, and cryptographic validation outcomes.
Fix: Implement structured audit logging that records session IDs, requested/granted attributes, validation status, and timestamps. Use append-only storage or blockchain-anchored logs for tamper evidence.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| EU age-gated checkout | EUDI Wallet (attribute-only) | 2β5 sec latency, zero PII storage, protocol-enforced minimization | Free SDK + ~EUR 49/mo managed service |
| Financial onboarding (EU) | EUDI Wallet + jurisdiction routing | Meets MiCA/AML requirements with cryptographic proof, reduces compliance overhead | Lower per-user cost vs traditional KYC |
| Global SaaS onboarding | Hybrid: EUDI for EU, traditional KYC fallback | Covers non-EU markets while optimizing EU conversion | Combined cost, but higher overall conversion |
| High-volume automated checks | EUDI Wallet | Real-time validation, no webhook queues, scalable SSE architecture | Predictable infrastructure cost |
| In-person verification | EUDI Wallet (proximity/NFC) | Offline capability, cryptographic proof without internet dependency | Zero additional cost vs QR flow |
| Document-level notarization | Traditional KYC | EUDI Wallets do not issue document images or notarized copies | Higher cost, necessary for legal compliance |
Configuration Template
// verifier.config.ts
import { VerifierConfig, TrustAnchorSource } from '@openeudi/core';
export const verifierConfig: VerifierConfig = {
mode: 'production',
trustAnchors: {
source: TrustAnchorSource.ETSI_TRUSTED_LIST,
refreshCron: '0 */6 * * *',
pinnedFallback: ['./certs/issuer_root.pem'],
maxAgeDays: 365,
},
presentation: {
mode: 'qr_code',
sessionTTLSeconds: 300,
maxRetries: 0, // Stateless SSE, no retry queue
timeoutSeconds: 300,
},
attributes: {
required: ['age_over_18', 'country_residence'],
optional: ['legal_name'],
schemaValidation: true,
},
audit: {
enabled: true,
storage: 'append_only_log',
piiMasking: true,
retentionDays: 2555, // 7 years for compliance
},
fallback: {
enabled: true,
trigger: 'non_eu_jurisdiction || wallet_unavailable',
provider: 'legacy_kyc_api',
},
};
Quick Start Guide
- Install dependencies:
npm install @openeudi/core @openeudi/openid4vp express uuid
- Initialize verifier: Import
VerifierEngine, load verifierConfig, and start trust anchor synchronization
- Create presentation endpoint: Implement
/identity/verify to generate QR payloads and /identity/verify/:id/stream for SSE delivery
- Validate and provision: Run cryptographic validation on received VPs, extract claims, log audit metadata, and issue session tokens
- Deploy with monitoring: Add Prometheus metrics for stream latency, validation success rate, and trust anchor sync status. Configure alerting on certificate expiration or sync failures.