OpenID4VCI (issuer)
The framework's SD-JWT VC primitive (b.auth.sdJwtVc) handles credential signing + sealed-claim disclosures. OID4VCI sits one layer above: it standardises HOW a wallet asks an issuer for a credential, and how the issuer announces what it can issue.
This module ships the issuer-side glue (issuer-initiated + wallet-initiated flows):
- credential_offer: issuer mints a one-shot offer + pre-authorized_code; emits a openid-credential-offer://... deep link the wallet scans / clicks. - /token (pre-authorized_code grant): holder POSTs the pre-auth code (+ optional tx_code) and gets an access token scoped to a specific credential identifier. - /credential: holder POSTs the access token + a proof JWT (signed by the holder key the wallet wants the credential bound to). The issuer mints + returns the SD-JWT VC with that key in cnf. - /.well-known/openid-credential-issuer: discovery metadata document describing supported credentials.
The issuer composes: - b.auth.sdJwtVc.issuer for the actual SD-JWT VC minting - b.cache for the pre-auth code → user-binding map (TTL defaults to 5 minutes per OID4VCI §5.1.1) - b.crypto.verify for the holder proof-JWT signature
Operators wire three routes (the framework gives the parsing + minting shape; HTTP-binding stays operator-side so the existing middleware stack — auth, rate-limit, CSRF — applies normally):
POST /token → ciba-style /token shared with the OAuth client (or a separate handler that calls issuer.exchangePreAuthorizedCode) POST /credential → issuer.issueCredential(req) GET /.well-known/ → issuer.metadata() openid-credential-issuer
b.auth.oid4vci.issuer.create(opts) #
{
{
credentialIssuerUrl: string, // required — used as `iss` and proof `aud`
credentialEndpoint: string, // public URL for the /credential endpoint
tokenEndpoint: string, // public URL for /token (re-used by the pre-auth flow)
sdJwtIssuer: , // mints the SD-JWT VC
supportedCredentials: { [id]: { format, vct, claims, ... } },
proofAlgorithms: string[], // default ["ES256", "ES384", "EdDSA"]
resolveKid?: function(kid, header), // resolve a kid-only proof's holder key (JWK | KeyObject); without it, kid-only proofs are refused
validateX5c?: function(chainDerBuffers, header), // x5c (RFC 7515 §4.1.6) chain-trust policy; throw to refuse. Absent → leaf-cert SPKI binds at the same self-asserted trust as inline `jwk`
preAuthCodeTtlMs?: number, // default 5m
accessTokenTtlMs?: number, // default 15m
cNonceTtlMs?: number, // default 5m
codeStore?: b.cache instance,
accessTokenStore?: b.cache instance,
cNonceStore?: b.cache instance,
}
}
Build an OID4VCI issuer over a configured b.auth.sdJwtVc.issuer. Returns route handlers for credential_offer, /token (pre-authorized grant), and /credential, plus a metadata() accessor for the /.well-known/openid-credential-issuer document.
var sdJwtIssuer = b.auth.sdJwtVc.issuer.create({ issuerUrl: "https://issuer.example.com", keys: [{ kid: "k1", privateKey: pem, algorithm: "ES256" }] });
var oid4vci = b.auth.oid4vci.issuer.create({
credentialIssuerUrl: "https://issuer.example.com",
credentialEndpoint: "https://issuer.example.com/credential",
tokenEndpoint: "https://issuer.example.com/token",
sdJwtIssuer: sdJwtIssuer,
supportedCredentials: {
"id-card-1": {
format: "vc+sd-jwt",
vct: "https://example.com/vct/identity",
claims: { given_name: {}, family_name: {}, birthdate: {} },
},
},
});
b.auth.oid4vci.issuer.createCredentialOffer(opts) #
{
{
subject: string,
credentialIds: string[],
txCode?: { value: string, length?: number, input_mode?: string, description?: string },
}
}
Mint a credential_offer + pre-authorized_code bound to a specific subject (the user the issuer has already authenticated out-of- band — kiosk, helpdesk identity proof, etc.). Returns the openid-credential-offer:// deep link the wallet scans.
var offer = await oid4vci.createCredentialOffer({
subject: "user-42",
credentialIds: ["id-card-1"],
});
// → { offer, preAuthCode, deepLink, offerUri }
b.auth.oid4vci.issuer.exchangePreAuthorizedCode(opts) #
{
{
preAuthCode: string,
txCode?: string,
}
}
Token-endpoint helper for the pre-authorized_code grant. Returns an access token + c_nonce the wallet uses on /credential. The underlying access token's scope is the credential_configuration_ids the offer was bound to.
var tokens = await oid4vci.exchangePreAuthorizedCode({
preAuthCode: req.body["pre-authorized_code"],
txCode: req.body.tx_code,
});
// → { access_token, token_type, expires_in, c_nonce, ... }
b.auth.oid4vci.issuer.issueCredential(opts) #
{
{
accessToken: string,
credentialIdentifier: string,
proof: string, // openid4vci-proof+jwt
claims: object, // operator-supplied user data
selectivelyDisclosed?: string[],
ttlMs?: number,
}
}
The /credential endpoint handler. Validates the access token, verifies the holder proof JWT (binding the holder key the wallet controls to a fresh c_nonce), mints the SD-JWT VC via the configured sdJwtIssuer, and rotates the c_nonce so the next request gets a fresh challenge. Returns the credential string + the new c_nonce.
Operators supply claims per call (the issuer's own user-data lookup keyed off the access-token's subject); the framework doesn't store user attributes itself.
var rv = await oid4vci.issueCredential({
accessToken: accessTokenFromBearerHeader,
credentialIdentifier: "id-card-1",
proof: req.body.proof.jwt,
claims: { given_name: "Alice", family_name: "Smith" },
});
// → { format: "vc+sd-jwt", credential, c_nonce, c_nonce_expires_in }
b.auth.oid4vci.issuer.metadata() #
Returns the /.well-known/openid-credential-issuer JSON document describing the issuer's supported credentials, endpoints, and proof types. Operators serve the result verbatim.
app.get("/.well-known/openid-credential-issuer", function (req, res) {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(oid4vci.metadata()));
});
Last updated 2026-08-08T16:39:49.652Z by seeder.