@vizualkei/sophid-server-sdk
v0.33.0
Published
SophID Web Server SDK - Biometric authentication helpers for server endpoints
Readme
SophID Server SDK
Server-side TypeScript SDK for partner web applications integrating SophID biometric authentication. It runs in your backend API and handles:
- Issuing short-lived Biometric Session Tokens (BST) with HMAC signatures and optional encrypted user binding (ebuid).
- Validating signed Biometric Result Tokens (BRT) returned by the SophID server.
- Enforcing expiry, replay protection, and operation allowlists.
You need a partner API key from SophID and the SophID server's ES256 public key.
Installation
pnpm add @vizualkei/sophid-server-sdkQuick Start (Recommended)
Use the SophidServerHelper singleton for the simplest integration:
import { sophidServerHelper } from '@vizualkei/sophid-server-sdk';
// Initialize once at server startup
const sophidHelper = sophidServerHelper.init({
apiKey: process.env.SOPHID_API_KEY!,
publicKeyPem: process.env.SOPHID_JWT_PUBLIC_KEY!,
});
// In your BST endpoint (POST /api/biometric-session):
const { bst, expiresAt } = await sophidHelper.createBiometricSessionToken(biometricUserId);
// In your BRT endpoint (POST /api/biometric-result):
const { claims, op } = await sophidHelper.verifyBiometricResultToken(brt, {
requireBst: true,
expectedBuid: session.user.biometricUserId,
requireSuccess: true,
});SophidServerHelper (Recommended)
The SophidServerHelper is the recommended integration surface. It wraps the lower-level pool and token classes, providing a clean two-method API for BST issuance and BRT validation.
Initialization
import { sophidServerHelper } from '@vizualkei/sophid-server-sdk';
const sophidHelper = sophidServerHelper.init({
apiKey: '<partner-api-key>',
publicKeyPem: '<sophid-es256-public-key-pem>',
issuer: 'sophid', // optional, JWT issuer check
audience: 'your-app', // optional, JWT audience check
sessionConfig: {
ttlMs: 5 * 60 * 1000, // BST TTL (default: 5 minutes)
maxFutureSkewMs: 30 * 1000, // clock skew tolerance (default: 30s)
},
defaultAllowedOps: ['enroll', 'unenroll', 'authenticate', 'restore'],
bstFailureStatus: 403, // HTTP status for BST failures
});SophidServerHelperConfig
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| apiKey | string | Yes | — | Partner API key for HMAC-signing BSTs |
| publicKeyPem | string | Yes | — | SophID server ES256 public key (PEM) |
| issuer | string | No | 'sophid' | Expected JWT iss claim |
| audience | string | No | — | Expected JWT aud claim |
| sessionConfig.ttlMs | number | No | 300000 | BST time-to-live (ms) |
| sessionConfig.maxFutureSkewMs | number | No | 30000 | Max future clock skew (ms) |
| defaultAllowedOps | Iterable<string> | No | — | Default operation allowlist |
| bstFailureStatus | number | No | 403 | HTTP status code for BST failures |
createBiometricSessionToken
Issue a BST for a biometric operation.
const { bst, expiresAt, token, entry } = await sophidHelper.createBiometricSessionToken(
biometricUserId, // optional — omit for enrollment
metadata // optional — attached to pool entry
);Parameters:
| Name | Type | Description |
|------|------|-------------|
| biometricUserId | string? | User ID to bind via ebuid. Omit for enrollment. |
| metadata | Record<string, unknown>? | Custom metadata stored with the session entry |
Returns: BiometricSessionIssueResult
| Field | Type | Description |
|-------|------|-------------|
| bst | string | Serialized BST JSON string |
| expiresAt | number | Expiration timestamp (ms) |
| token | BiometricSessionTokenInterface | Token object |
| entry | BiometricSessionPoolEntry | Pool entry |
verifyBiometricResultToken
Validate a BRT and optionally consume the embedded BST.
const { claims, op, bst } = await sophidHelper.verifyBiometricResultToken(brt, {
requireBst: true,
expectedBuid: session.user.biometricUserId,
requireSuccess: true,
allowedOps: ['enroll', 'authenticate', 'restore', 'unenroll'],
});Parameters:
| Name | Type | Description |
|------|------|-------------|
| brt | string | The BRT JWT string |
| options | VerifyBiometricResultOptions | Validation options |
VerifyBiometricResultOptions:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| requireBst | boolean | true | Validate and consume BST from claims |
| expectedBuid | string \| null | — | Expected biometric user ID (matched against decrypted ebuid) |
| requireSuccess | boolean | false | Require claims.success === true |
| allowedOps | Set<string> \| string[] | defaultAllowedOps | Allowed operation types |
| useClaimsError | boolean | false | Use claims.error as the failure message |
| failureMessage | string | — | Custom failure message |
| bstFailureStatus | number | 403 | HTTP status for BST failures |
Returns: { claims: BiometricResultClaims; op: string; bst: string }
Throws: Error with .status property for HTTP status codes.
Singleton Access
import { sophidServerHelper } from '@vizualkei/sophid-server-sdk';
// After init(), use directly:
const { bst } = await sophidServerHelper.createBiometricSessionToken(userId);
const { claims } = await sophidServerHelper.verifyBiometricResultToken(brt, opts);
// Or get the instance:
const helper = sophidServerHelper.get();Integration Examples
Example: Next.js API Routes
// app/api/_lib/SophidServerHelper.ts
import { sophidServerHelper } from '@vizualkei/sophid-server-sdk';
export const sophidHelper = sophidServerHelper.init({
apiKey: process.env.SOPHID_API_KEY!,
publicKeyPem: process.env.SOPHID_JWT_PUBLIC_KEY!,
sessionConfig: { ttlMs: 5 * 60 * 1000, maxFutureSkewMs: 30 * 1000 },
});
// app/api/biometric-session/route.ts
import { sophidHelper } from '../_lib/SophidServerHelper';
export async function POST(req: Request) {
const session = await getSession(req);
const biometricUserId = session.user.biometricUserId; // undefined for new users
const { bst, expiresAt } = await sophidHelper.createBiometricSessionToken(biometricUserId);
return Response.json({ bst, expiresAt: new Date(expiresAt).toISOString() });
}
// app/api/biometric-result/route.ts
import { sophidHelper } from '../_lib/SophidServerHelper';
export async function POST(req: Request) {
const session = await getSession(req);
const { brt } = await req.json();
const { claims, op } = await sophidHelper.verifyBiometricResultToken(brt, {
requireBst: true,
expectedBuid: session.user.biometricUserId,
requireSuccess: true,
allowedOps: new Set(['enroll', 'unenroll', 'authenticate', 'restore']),
});
switch (op) {
case 'enroll':
await updateUser(session.user.id, { biometricUserId: claims.userId });
break;
case 'unenroll':
await updateUser(session.user.id, { biometricUserId: null });
break;
}
return Response.json({ success: true, operation: op });
}Example: Express.js Routes
import express from 'express';
import { sophidServerHelper } from '@vizualkei/sophid-server-sdk';
const router = express.Router();
const sophidHelper = sophidServerHelper.init({
apiKey: process.env.SOPHID_API_KEY!,
publicKeyPem: process.env.SOPHID_JWT_PUBLIC_KEY!,
issuer: 'sophid',
defaultAllowedOps: ['enroll', 'unenroll', 'authenticate', 'restore', 'checkin'],
});
router.post('/biometric-session', requireAuth, async (req, res) => {
const biometricUserId = req.user.biometricUserId;
const { bst, expiresAt } = await sophidHelper.createBiometricSessionToken(biometricUserId);
res.json({ bst, expiresAt: new Date(expiresAt).toISOString() });
});
router.post('/biometric-result', requireAuth, async (req, res) => {
try {
const { claims, op } = await sophidHelper.verifyBiometricResultToken(req.body.brt, {
requireBst: true,
expectedBuid: req.user.biometricUserId,
requireSuccess: true,
});
// Process based on op...
res.json({ success: true, operation: op });
} catch (err) {
res.status(err.status || 500).json({ error: err.message });
}
});
// Station check-in (no BST — trusted client)
router.post('/biometric-checkin', requireCheckinApiKey, async (req, res) => {
try {
const { claims, op } = await sophidHelper.verifyBiometricResultToken(req.body.brt, {
requireBst: false, // Station is trusted, no BST
requireSuccess: true,
allowedOps: ['checkin'],
});
// Update ticket records...
res.json({ success: true, operation: op });
} catch (err) {
res.status(err.status || 500).json({ error: err.message });
}
});Required Server Endpoints
Partner servers must expose two endpoints:
1. POST /api/biometric-session
Mint a BST for a single biometric operation.
Response:
{
"bst": "{\"sid\":\"<uuid>\",\"ts\":1700000000000,\"ebuid\":\"<base64url>\",\"sig\":\"<base64url>\"}",
"expiresAt": "2026-01-01T00:05:00.000Z"
}2. POST /api/biometric-result
Receive and validate a BRT from the webapp.
Request:
{ "brt": "<signed-jwt>" }Response:
{ "success": true, "operation": "enroll" }3. POST /api/biometric-checkin (optional)
For station/kiosk apps. Validates BRT without BST (trusted client model).
Auth: Authorization: Bearer <checkinApiKey>
Request:
{ "brt": "<signed-jwt>" }Low-Level API
For advanced use cases, you can use the individual classes directly.
BiometricSessionIdPool
Manages BST generation with HMAC signing, ebuid encryption, and replay protection.
import { BiometricSessionIdPool } from '@vizualkei/sophid-server-sdk';
const pool = new BiometricSessionIdPool({
apiKey: process.env.SOPHID_API_KEY!,
ttlMs: 5 * 60 * 1000,
});
// Generate a BST
const { token, entry } = await pool.generate(biometricUserId);
const bstString = token.toTokenString();
// Remove/consume a session
const consumed = await pool.remove(sessionId);
// Decrypt ebuid
const userId = pool.decryptBioUserId(ebuid);BiometricSessionToken
Parse and validate a BST string.
import { BiometricSessionToken } from '@vizualkei/sophid-server-sdk';
const bst = new BiometricSessionToken(bstJsonString);
const sessionId = bst.getSessionId();
const timestamp = bst.getTimeStamp();
const ebuid = bst.getEbuid();
const serialized = bst.toTokenString();
// Validate against pool
const result = await bst.validate(pool, { expectedBuid: 'user-123' });
if (!result.ok) {
console.error(result.reason, result.message);
}Validation reasons: invalid_format, future_timestamp, expired, missing_api_key, invalid_signature, not_found, mismatch, ebuid_mismatch, ebuid_decrypt_failed, expected_buid_mismatch
BiometricResultToken
Validate a BRT JWT.
import { BiometricResultToken } from '@vizualkei/sophid-server-sdk';
const verifier = new BiometricResultToken({
publicKey: process.env.SOPHID_JWT_PUBLIC_KEY!,
issuer: 'sophid',
audience: 'your-app',
});
const claims = await verifier.validate(brt, {
sessionPool: pool, // enables BST validation + consumption
requireBst: true,
expectedBuid: 'user-123',
});Concepts
BST (Biometric Session Token)
A short-lived JSON string authorizing a single biometric operation.
{"sid":"<uuid>","ts":1700000000000,"ebuid":"<base64url>","sig":"<base64url>"}| Field | Description |
|-------|-------------|
| sid | Session UUID |
| ts | Timestamp (ms since epoch) |
| ebuid | Encrypted biometric user ID (optional, empty for enrollment) |
| sig | HMAC-SHA256 of <sid>.<ts>.<ebuid> using the partner API key |
- TTL: 5 minutes (configurable)
- Single-use: consumed from the pool on validation
ebuid (Encrypted Biometric User ID)
Binds a session to a specific user, preventing session hijacking.
- Encryption: AES-256-GCM, key = SHA-256(partner API key)
- Format:
base64url(iv[12] + authTag[16] + ciphertext) - Plaintext:
<biometricUserId>:<randomPadding> - Required for: authenticate, authenticate-on-device, unenroll
- Empty for: enroll
- Optional for: restore
BRT (Biometric Result Token)
An ES256 JWT signed by the SophID server.
Common claims: success, op, callbackId, bst, userId, enrollmentId, userName, enrolledAt, authenticatedAt, eventId, redeemedTickets, error, reason, message, cancelled, iss, aud, jti, iat, exp
Thread Safety
The BiometricSessionIdPool uses async-mutex for thread-safe access. All operations (generate, remove, cleanup) are serialized. Safe for concurrent Node.js async request handlers and Worker threads.
Multi-process limitation: The built-in pool is in-memory. For multi-process deployments (cluster mode, PM2, multiple replicas), implement a custom BiometricSessionIdPoolInterface backed by Redis or a database.
Types Reference
BiometricSessionTokenPayload
type BiometricSessionTokenPayload = {
sessionId: string;
timeStamp: number;
ebuid?: string;
signature?: string;
};BiometricSessionPoolEntry
type BiometricSessionPoolEntry = {
sessionId: string;
issuedAt: number;
expiresAt: number;
ebuid?: string;
metadata?: Record<string, unknown>;
};BiometricSessionIssue
type BiometricSessionIssue = {
token: BiometricSessionTokenInterface;
entry: BiometricSessionPoolEntry;
};BiometricSessionIssueResult
type BiometricSessionIssueResult = {
bst: string;
token: BiometricSessionTokenInterface;
entry: BiometricSessionPoolEntry;
expiresAt: number;
};BiometricSessionValidationResult
type BiometricSessionValidationResult = {
ok: boolean;
reason?: string;
message?: string;
entry?: BiometricSessionPoolEntry;
};BiometricResultClaims
type BiometricResultClaims = Record<string, any>;Configuration
Config Types
// BST validation config
type BiometricSessionTokenConfig = {
ttlMs?: number; // default: 300000 (5 min)
maxFutureSkewMs?: number; // default: 30000 (30 sec)
logger?: Logger;
};
// Pool config (extends token config)
type BiometricSessionPoolConfig = BiometricSessionTokenConfig & {
apiKey?: string;
};
// BRT validation config
type BiometricResultTokenConfig = {
publicKey: string; // required — ES256 PEM
issuer?: string;
audience?: string;
logger?: Logger;
};