A powerful client package for implementing OIDC4VP (OpenID Connect for Verifiable Presentations) verifier functionality in Node.js applications. This library simplifies the process of verifying decentralized identities (DIDs) and verifiable credentials (VCs) through integration with the Empe SSI (Self-Sovereign Identity) ecosystem.
- Installation
- Quick Start
- Configuration Options
- Advanced Usage
- API Endpoints
- Callback Handling
- Rate Limiting
- SSE (Server-Sent Events)
- VP Query Authorization Requests
- Error Handling
- Security Considerations
npm install @empe/verifier-client
# or
yarn add @empe/verifier-client
Set up a basic verifier service with Express in just a few steps:
import express from 'express';
import { VerifierClient } from '@empe/verifier-client';
// Create Express app
const app = express();
// Configure the verifier client
const verifierClient = new VerifierClient(app, {
baseUrl: 'https://your-app-domain.com', // Your application's base URL
verifierServiceUrl: 'https://verifier-service.empe.io', // Empe verifier service URL
clientSecret: 'your-client-secret', // Your client secret for authentication
verificationFlows: [
{
name: 'basic-identity', // A unique name for this verification flow
vpQuery: [
{
fields: [
{
path: ['$.type'],
filter: {
type: 'array',
contains: {
const: 'BasicIdentityCredential',
},
},
},
{
path: ['$.credentialSubject.id'],
filter: {
type: 'string',
pattern: '^did:empe:.*$',
},
},
],
},
],
handleVerificationResult: async data => {
// Process the verification result
console.log('Verification result:', data);
// The verified presentation data is already parsed and available
const vpData = data.vp;
// Return data to be sent to the client via SSE
return {
status: 'success',
message: 'Identity verified successfully',
userData: data.vp,
};
},
},
],
});
// Initialize the verifier client
verifierClient.initialize();
// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Verifier server running on port ${PORT}`);
});
The VerifierClient
constructor accepts the following configuration options:
type VerifierConfiguration = {
// Your application's base URL - used to construct callback URLs
baseUrl: string;
// The URL of the Empe verifier service
verifierServiceUrl: string;
// Client secret for authentication with the verifier service
clientSecret: string;
// Array of verification flow configurations
verificationFlows?: VerifierEndpoint[];
};
type VerifierEndpoint = {
// A unique name for the verification flow
name: string;
// Callback function to process verification results
handleVerificationResult: (data: VerifierCallbackRequest) => Promise<any>;
// Array of verification query parameters defining credential requirements
vpQuery?: VPQueryParams[];
// Custom path for QR code endpoint
// (default: '/api/verifier/{flow-name}/v1/authorize-qr-code')
pathQRCode?: string;
// Custom path for SSE connection endpoint
// (default: '/api/verifier/{flow-name}/v1/connection')
pathOpenConnection?: string;
// Custom path for verifier redirect URI
// (default: '/api/verifier/{flow-name}/v1/callback')
verifierRedirectUri?: string;
// Validity period of the verification request in seconds (default: 300)
validity?: number;
// Timeout for SSE connections in milliseconds (default: 15 minutes)
sseTimeout?: number;
// Require an active SSE connection when processing verification results
sseRequired?: boolean;
// Rate limiting options for the endpoints
rateLimitOptions?: Partial<Options>;
};
You can define multiple verification flows with different credential requirements:
const verifierClient = new VerifierClient(app, {
baseUrl: 'https://your-app-domain.com',
verifierServiceUrl: 'https://verifier-service.empe.io',
clientSecret: 'your-client-secret',
verificationFlows: [
{
name: 'basic-identity',
vpQuery: [
{
fields: [
{
path: ['$.type'],
filter: {
type: 'array',
contains: {
const: 'BasicIdentityCredential',
},
},
},
],
},
],
handleVerificationResult: async data => {
// Handle basic identity verification
return { status: 'success', type: 'basic-identity' };
},
},
{
name: 'membership',
vpQuery: [
{
fields: [
{
path: ['$.type'],
filter: {
type: 'array',
contains: {
const: 'MembershipCredential',
},
},
},
],
},
],
handleVerificationResult: async data => {
// Handle membership verification
return { status: 'success', type: 'membership' };
},
},
],
});
The verifier-client supports dynamic VP queries, allowing you to create verification requests on-the-fly with custom credential requirements:
// Client code to create a dynamic VP query
const response = await axios.post(
'http://localhost:3000/verifier/vp-query/v1/authorize-qr-code',
{
vpQuery: [
{
fields: [
{
path: ['$.type'],
filter: {
type: 'array',
contains: { const: 'CustomCredential' },
},
},
{
path: ['$.credentialSubject.age'],
filter: {
type: 'number',
minimum: 18,
},
},
],
},
],
validity: 300, // 5 minutes
},
{
headers: {
'x-client-secret': 'your-client-secret',
},
}
);
// The response contains the same data as a regular QR code endpoint
const { qr_code_url, state, request_uri } = response.data;
You can integrate the verifier service with a frontend application using the following approach:
// Backend (Express)
app.get('/api/start-verification', async (req, res) => {
try {
// Fetch QR code data from your verifier endpoint
const response = await axios.post(
'http://localhost:3000/api/verifier/basic-identity/v1/authorize-qr-code',
{},
{
headers: {
'x-client-secret': 'your-client-secret',
},
}
);
res.json(response.data);
} catch (error) {
res.status(500).json({ error: 'Failed to start verification' });
}
});
// Frontend (JavaScript)
async function startVerification() {
// 1. Fetch verification data including QR code URL
const response = await fetch('/api/start-verification');
const data = await response.json();
// 2. Display QR code to the user
displayQRCode(data.qr_code_url);
// 3. Open SSE connection to receive verification result
const eventSource = new EventSource(`/api/verifier/basic-identity/v1/connection/${data.state}`);
// 4. Handle verification result
eventSource.onmessage = event => {
const result = JSON.parse(event.data);
// Process verification result
console.log('Verification completed:', result);
eventSource.close();
};
eventSource.onerror = () => {
console.error('SSE connection error');
eventSource.close();
};
}
You can integrate with mobile wallets using deeplinks:
// After getting the request_uri from the QR code endpoint
const deepLinkUrl = `empewallet://presentation?presentation_url=${encodeURIComponent(requestUri)}`;
// Create a button that opens the mobile wallet
document.getElementById('open-wallet-btn').addEventListener('click', () => {
window.location.href = deepLinkUrl;
});
This allows users to open their Empe wallet directly from your application to complete the verification process.
For each verification flow (e.g., 'basic-identity'), the following endpoints are created:
-
QR Code Generation:
POST /api/verifier/{flow-name}/v1/authorize-qr-code
- Returns data for QR code generation including state, nonce, request_uri, and qr_code_url
- Requires client secret in header:
x-client-secret
-
SSE Connection:
GET /api/verifier/{flow-name}/v1/connection/:state
- Opens a Server-Sent Events connection to receive verification results
- The
:state
parameter must match the state from the QR code generation response
-
Verifier Callback:
POST /api/verifier/{flow-name}/v1/callback
- Receives verification results from the Empe verifier service
- This endpoint is called by the verifier service, not your client application
The handleVerificationResult
function receives the following data:
type VerifierCallbackRequest = {
// Unique identifier for this verification session
state: string;
// Verification status: 'verified' or 'failed'
verification_status: string;
// Additional information about the verification
additional_info: string;
// Unique identifier for the verification query
query_id: string;
// Verifiable Presentation
vp: string;
// Nonce used in the verification to prevent replay attacks
nonce: string;
};
The result of handleVerificationResult
is sent to the client via the SSE connection.
The package includes built-in rate limiting protection. You can customize rate limits for each verification flow:
{
name: 'basic-identity',
// ... other configuration ...
rateLimitOptions: {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
}
}
The package uses Server-Sent Events to push verification results from the server to the client in real-time. SSE connections time out after 15 minutes by default, but you can customize this:
{
name: 'basic-identity',
// ... other configuration ...
sseTimeout: 30 * 60 * 1000, // 30 minutes
sseRequired: true, // Require an active SSE connection when processing verification results
}
When sseRequired
is set to true
, the verification result will only be processed if there is an active SSE connection for the corresponding state parameter.
In addition to dynamic presentation definitions, the verifier client also supports pre-defined VP queries identified by a VP query ID:
// Frontend code
async function startVpQueryVerification() {
// Request authorization using a pre-defined VP query
const response = await fetch('/api/verifier/vp-query/v1/authorize-qr-code', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-client-secret': 'your-client-secret',
},
body: JSON.stringify({
vp_query_id: 'my-predefined-query-id',
}),
});
const data = await response.json();
// Continue with QR code display and SSE connection as in standard flow
}
This approach allows for more complex verification requirements that are managed server-side.
The package provides built-in error handling for API endpoints. Errors are properly formatted and returned with appropriate status codes. For custom error handling, you can catch errors in your handleVerificationResult
function:
handleVerificationResult: async data => {
try {
// Process verification
return { status: 'success' };
} catch (error) {
console.error('Verification error:', error);
return {
status: 'error',
message: error.message || 'Verification processing failed',
};
}
};
- Client Secret: Protect your client secret and never expose it in client-side code.
- Timing-Safe Comparison: The client secret validation uses timing-safe comparison to prevent timing attacks.
- Rate Limiting: Configure appropriate rate limits to prevent abuse.
- Input Validation: All incoming data is validated using Joi schemas.
- Error Messages: Error messages are designed to not leak sensitive information.
The @empe/verifier-client
package is designed to work seamlessly with other packages in the Empe ecosystem:
The @empe/verifier-common
package provides the core functionality for credential verification, including presentation definitions and validation:
import { buildPresentationDefinition, VPQueryParams } from '@empe/verifier-common';
// Create a presentation definition for your verification flow
const presentationDefinition = buildPresentationDefinition([
{
fields: [
{
path: ['$.type'],
filter: {
type: 'array',
contains: { const: 'IdentityCredential' },
},
},
{
path: ['$.credentialSubject.nationality'],
filter: {
type: 'string',
const: 'Germany',
},
},
],
},
]);
The @empe/empe-did-resolver
package is used internally to resolve and validate DIDs in the verification process:
import { DidResolver, VpValidator } from '@empe/empe-did-resolver';
import { VerifiablePresentation } from '@empe/identity';
// Create a resolver and validator
const resolver = new DidResolver(['empe']);
const validator = new VpValidator(resolver);
// Validate a presentation
const presentation = VerifiablePresentation.fromJSON(vpJson);
await validator.validateVp(presentation);
MIT