Server-side SDK for Relying Parties (RPs) consuming SIOPv2 id_tokens
from the OpenVTC browser plugin (window.vtaWallet.login).
window.vtaWallet.login() POSTs a SIOPv2 self-issued id_token to
your /auth/ endpoint. The token is a compact EdDSA JWS signed by
the wallet's holder did:key. The signature is not optional —
without verifying it, any page can forge a login as any DID.
This SDK is the audited verification path. verifyIdToken:
- pins
algtoEdDSA(no algorithm substitution), - enforces SIOPv2
iss === sub, - pins
audto your RP DID (no leniency), - pins
nonceto the challenge you issued (constant-time match), - checks
iat/expwithin a configurable clock-skew window, - resolves the issuer DID and verifies the JWS signature against the resolved Ed25519 verification method.
Failure modes surface as IdTokenVerificationError with a typed
reason — log it so operators can distinguish misconfigured
audience from a forged token.
The browser-plugin demo skips verification — it trusts whatever the wallet POSTs. The demo is widely copy-pasted into production code, inheriting the gap. The May 2026 OpenVTC security review flagged this as a high-severity issue (H2). This SDK is the fix.
npm install @openvtc/rp-sdkimport { verifyIdToken, KeyResolver } from "@openvtc/rp-sdk";
const resolver = new KeyResolver(); // did:key only; see below
const verified = await verifyIdToken({
idToken: req.body.id_token,
audience: process.env.RP_DID!,
nonce: sessionStore.challengeFor(req.body.session_id),
resolver,
});
// verified.subject is the wallet's holder DID; bind your session to it.
console.log(`logged in: ${verified.subject}`);import { establishSession } from "@openvtc/rp-sdk";
const accessToken = await myJwtMinter.mint({ sub: verified.subject });
const { subject, cookie } = establishSession(verified, accessToken);
res.cookie(cookie.name, cookie.value, cookie.options);
// SDK sets HttpOnly + Secure + SameSite=Strict by default.The bundled KeyResolver handles did:key:z6Mk… (Ed25519 multikey)
in-process — no network round-trip, no cache concerns.
For did:peer:2 (the wallet's default for inbound RP-initiated
flows), did:webvh, or did:web — implement the DidResolver
interface against your preferred resolver. A thin wrapper around
affinidi-did-resolver-cache-sdk covers all of them.
import type { DidResolver } from "@openvtc/rp-sdk";
class MultiMethodResolver implements DidResolver {
async resolveAuthenticationKey(did: string): Promise<Uint8Array> {
if (did.startsWith("did:key:")) return keyResolver.resolveAuthenticationKey(did);
// ... did:peer / did:webvh / did:web cases
}
}Planned for follow-up minor versions:
requireStepUp()middleware — gates routes behindacr=aal2.refreshProxy()middleware — drop-in/auth/refreshproxy.- Express + Fastify + Hono framework adapters.
- DIDComm-transport variant for RPs that prefer the wallet's authcrypt flow over the REST SIOPv2 flow.
Apache-2.0