@bedrock/vc-delivery
Version:
Bedrock Verifiable Credential Delivery
840 lines (748 loc) • 30.1 kB
JavaScript
/*!
* Copyright (c) 2022-2026 Digital Bazaar, Inc.
*/
import * as bedrock from '@bedrock/core';
import * as draft13 from './oid4vciDraft13.js';
import {issue as defaultIssue, getIssueRequestsParams} from '../issue.js';
import {getWorkflowIssuerInstances, setVariable} from '../helpers.js';
import {importJWK, SignJWT} from 'jose';
import {timingSafeEqual, randomUUID as uuid} from 'node:crypto';
import {
authorizationDetails as authorizationDetailsSchema
} from '../../schemas/bedrock-vc-workflow.js';
import {checkAccessToken} from '@bedrock/oauth2-verifier';
import {compile} from '@bedrock/validation';
import {ExchangeProcessor} from '../ExchangeProcessor.js';
import {getStepAuthorizationRequest} from './oid4vp.js';
import {verifyCredentialRequestProof} from '../verify.js';
const {util: {BedrockError}} = bedrock;
const PRE_AUTH_GRANT_TYPE =
'urn:ietf:params:oauth:grant-type:pre-authorized_code';
const VALIDATORS = {
authorizationDetails: null
};
bedrock.events.on('bedrock.init', () => {
// create validators for x-www-form-urlencoded parsed data
VALIDATORS.authorizationDetails = compile({
schema: authorizationDetailsSchema()
});
});
export async function getAuthorizationServerConfig({req}) {
// note that technically, we should not need to serve any credential
// issuer metadata, but we do for backwards compatibility purposes as
// previous versions of OID4VCI required it
return getCredentialIssuerConfig({req});
}
export async function getCredentialIssuerConfig({req}) {
const {config: workflow} = req.serviceObject;
const exchangeRecord = await req.getExchange();
const {exchange} = exchangeRecord;
_assertOID4VCISupported({exchange});
// use exchange processor get current step of the exchange
const exchangeProcessor = new ExchangeProcessor({workflow, exchangeRecord});
const step = await exchangeProcessor.getStep();
// fetch credential configurations for the step
const credential_configurations_supported =
_getSupportedCredentialConfigurations({workflow, exchange, step});
const exchangeId = `${workflow.id}/exchanges/${exchange.id}`;
return {
authorization_details_types_supported: ['openid_credential'],
batch_credential_endpoint: `${exchangeId}/openid/batch_credential`,
credential_configurations_supported,
credential_endpoint: `${exchangeId}/openid/credential`,
credential_issuer: exchangeId,
issuer: exchangeId,
jwks_uri: `${exchangeId}/openid/jwks`,
'pre-authorized_grant_anonymous_access_supported': true,
nonce_endpoint: `${exchangeId}/openid/nonce`,
token_endpoint: `${exchangeId}/openid/token`
};
}
export async function getCredentialOffer({req}) {
const {config: workflow} = req.serviceObject;
const exchangeRecord = await req.getExchange();
const {exchange} = exchangeRecord;
_assertOID4VCISupported({exchange});
// start building OID4VCI credential offer
const exchangeId = `${workflow.id}/exchanges/${exchange.id}`;
const offer = {
credential_issuer: exchangeId,
grants: {
'urn:ietf:params:oauth:grant-type:pre-authorized_code': {
'pre-authorized_code': exchange.openId.preAuthorizedCode
}
}
};
// use exchange processor get current step of the exchange
const exchangeProcessor = new ExchangeProcessor({workflow, exchangeRecord});
const step = await exchangeProcessor.getStep();
// fetch credential configurations for the step
const credential_configurations_supported =
_getSupportedCredentialConfigurations({workflow, exchange, step});
// offer all configuration IDs and support both spec version ID-1 with
// `credentials` and draft 13 with `credential_configuration_ids`
const configurationIds = Object.keys(credential_configurations_supported);
offer.credentials = configurationIds;
offer.credential_configuration_ids = configurationIds;
return offer;
}
export async function getJwks({req}) {
const {exchange} = await req.getExchange();
_assertOID4VCISupported({exchange});
return [exchange.openId.oauth2.keyPair.publicKeyJwk];
}
export function getOID4VCIProtocols({workflow, exchange}) {
if(!supportsOID4VCI({exchange})) {
return {};
}
// OID4VCI supported; add credential offer URL
const exchangeId = `${workflow.id}/exchanges/${exchange.id}`;
const searchParams = new URLSearchParams();
const uri = `${exchangeId}/openid/credential-offer`;
searchParams.set('credential_offer_uri', uri);
return {OID4VCI: `openid-credential-offer://?${searchParams}`};
}
export async function initExchange({workflow, exchange} = {}) {
if(!supportsOID4VCI({exchange})) {
return;
}
// either issuer instances or a single issuer zcap be given if
// any expected credential requests are given
const {expectedCredentialRequests} = exchange.openId;
if(expectedCredentialRequests &&
!(workflow.issuerInstances || workflow.zcaps.issue)) {
throw new BedrockError(
'Credential requests are not supported by this workflow.', {
name: 'DataError',
details: {httpStatusCode: 400, public: true}
});
}
}
export async function processAccessTokenRequest({req}) {
// parse `authorization_details` from request
let authorizationDetails;
if(req.body.authorization_details) {
authorizationDetails = JSON.parse(req.body.authorization_details);
_validate(VALIDATORS.authorizationDetails, authorizationDetails);
}
// check exchange
const exchangeRecord = await req.getExchange();
const {exchange} = exchangeRecord;
_assertOID4VCISupported({exchange});
/* Examples of types of token requests:
pre-authz code:
grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code
&pre-authorized_code=SplxlOBeZQQYbYS6WxSbIA
&tx_code=493536
&authorization_details=<URI-component-encoded JSON array>
authz code:
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&redirect_uri=https%3A%2F%2FWallet.example.org%2Fcb */
const {config: workflow} = req.serviceObject;
const {
grant_type: grantType,
'pre-authorized_code': preAuthorizedCode
} = req.body;
if(grantType !== PRE_AUTH_GRANT_TYPE) {
// unsupported grant type
// FIXME: throw proper oauth2 formatted error
throw new Error('Unsupported grant type.');
}
// validate grant type
const {openId: {preAuthorizedCode: expectedCode}} = exchange;
if(expectedCode) {
// ensure expected pre-authz code matches
if(!timingSafeEqual(
Buffer.from(expectedCode, 'utf8'),
Buffer.from(preAuthorizedCode, 'utf8'))) {
// FIXME: throw proper oauth2 formatted error
throw new Error('invalid pre-authorized-code or user pin');
}
}
// if authorization details provided in request, generate the set of
// requested credential configuration IDs
let requestedIds;
if(authorizationDetails) {
// populate token authorization details by matching each requested
// credential configuration ID with its credential IDs
requestedIds = new Set(authorizationDetails.map(
detail => detail.credential_configuration_id));
}
// process exchange to generate supported credential requests
let supportedCredentialRequestsStored;
let supportedCredentialRequests;
const exchangeProcessor = new ExchangeProcessor({
workflow, exchangeRecord,
async prepareStep({exchange, step}) {
// do not generate any VPR yet
step.verifiablePresentationRequest = undefined;
// if not generated, generate supported credential requests and store
// in step results
const stepResults = exchange.variables.results[exchange.step];
supportedCredentialRequests = stepResults
?.openId?.supportedCredentialRequests;
if(supportedCredentialRequests) {
supportedCredentialRequestsStored = true;
} else {
// get all possible supported credential requests
supportedCredentialRequests = _createSupportedCredentialRequests({
workflow, exchange, step
});
// if specific credential configuration IDs were requested, filter
// the supported requests by that list, limiting them to what the
// client is interested in, thereby disallowing any others in this
// exchange and establishing completion conditions for it
if(requestedIds) {
supportedCredentialRequests = supportedCredentialRequests
.filter(r => requestedIds.has(r.credentialConfigurationId));
// throw an error if *none* of the requested IDs are available
if(supportedCredentialRequests.length === 0) {
throw new BedrockError(
'None of the requested credential(s) are available.', {
name: 'NotAllowedError',
details: {httpStatusCode: 403, public: true}
});
}
}
// store supported requests
exchange.variables.results[exchange.step] = {
...exchange.variables.results[exchange.step],
openId: {
...exchange.variables.results[exchange.step]?.openId,
supportedCredentialRequests
}
};
}
},
inputRequired({step}) {
// if the supported credential requests haven't been stored in the
// step result yet, then input is not required but issuance needs to
// be disabled to allow them to be stored and then the step reprocessed
if(!supportedCredentialRequestsStored) {
step.issueRequests = [];
step.verifiablePresentation = undefined;
return false;
}
// requests stored and input is now required via credential endpoint
return true;
},
isStepComplete() {
// getting an access token never completes the step
return false;
}
});
await exchangeProcessor.process();
// process `authorizationDetails` request, if any, to create token
// authorization details
let tokenAuthorizationDetails;
if(authorizationDetails) {
// create map of unprocessed credential configuration ID => credential IDs
const idMap = new Map();
for(const request of supportedCredentialRequests) {
if(!supportedCredentialRequests.processed) {
let credentialIds = idMap.get(request.credentialConfigurationId);
if(!credentialIds) {
credentialIds = [];
idMap.set(request.credentialConfigurationId, credentialIds);
}
credentialIds.push(request.credentialIdentifier);
}
}
// populate token authorization details by matching each requested
// credential configuration ID with its credential IDs
tokenAuthorizationDetails = [];
for(const credentialConfigurationId of requestedIds) {
const credentialIds = idMap.get(credentialConfigurationId);
if(credentialIds) {
tokenAuthorizationDetails.push({
type: 'openid_credential',
credential_configuration_id: credentialConfigurationId,
credential_identifiers: credentialIds
});
}
}
}
// create access token
const {accessToken, ttl} = await _createExchangeAccessToken({
workflow, exchangeRecord
});
// return token info
const tokenInfo = {
access_token: accessToken,
token_type: 'bearer',
expires_in: ttl
};
if(tokenAuthorizationDetails) {
tokenInfo.authorization_details = tokenAuthorizationDetails;
}
return tokenInfo;
}
export async function processCredentialRequests({req, res, isBatchRequest}) {
const {config: workflow} = req.serviceObject;
const exchangeRecord = await req.getExchange();
const {exchange} = exchangeRecord;
_assertOID4VCISupported({exchange});
// ensure oauth2 access token is valid
await _checkAuthz({req, workflow, exchange});
// process exchange and capture values to return
let didProofRequired = false;
let format;
let issueResult;
let matchingCredentialIdentifiers;
let supportedCredentialRequests;
const exchangeProcessor = new ExchangeProcessor({
workflow, exchangeRecord,
async prepareStep({exchange, step}) {
// get `supportedCredentialRequests` from step results
supportedCredentialRequests = await _getSupportedCredentialRequests({
exchangeProcessor, workflow, exchange, step
});
// if the issue result has been generated, return early and allow
// `isInputRequired()` to handle further processing
if(issueResult) {
return;
}
// fetch credential configurations for the step
const supportedCredentialConfigurations =
_getSupportedCredentialConfigurations({workflow, exchange, step});
// get credential requests (only more than one w/`isBatchRequest=true`)
let credentialRequests = isBatchRequest ?
req.body.credential_requests : [req.body];
// normalize draft 13 requests
if(isBatchRequest || req.body?.format) {
credentialRequests = draft13.normalizeCredentialRequestsToVersion1({
credentialRequests,
supportedCredentialConfigurations
});
// set `format`
const configuration = supportedCredentialConfigurations[
credentialRequests[0].credential_configuration_id];
({format} = configuration);
}
// map each credential request to an appropriate supported credential
// request; for OID4VCI 1.0+ clients, there will be a single request
// with a `credential_identifier`; for draft 13, one or more requests,
// each with `credential_configuration_id` set will be present and
// these must map to *every* matching supported request
const unprocessed = supportedCredentialRequests.filter(r => !r.processed);
matchingCredentialIdentifiers = new Set();
for(const credentialRequest of credentialRequests) {
const {
credential_identifier, credential_configuration_id
} = credentialRequest;
// OID4VCI 1.0+ case
if(credentialRequest.credential_identifier) {
const match = unprocessed.find(
r => r.credentialIdentifier === credential_identifier);
if(match) {
matchingCredentialIdentifiers.add(credential_identifier);
}
break;
}
// draft 13 case
unprocessed.filter(
r => r.credentialConfigurationId === credential_configuration_id)
.forEach(
r => matchingCredentialIdentifiers.add(r.credentialIdentifier));
}
// handle no match case
if(matchingCredentialIdentifiers.size === 0) {
throw new BedrockError(
'The requested credential(s) have already been delivered.', {
name: 'NotAllowedError',
details: {httpStatusCode: 403, public: true}
});
}
// check to see if step supports OID4VP during OID4VCI
if(step.openId) {
// Note: either OID4VCI 1.1+ w/IAE (interactive authz endpoint) or
// OID4VCI-1.0/draft13+OID4VP will have received VP results which will
// be stored with this step; OID4VCI 1.0- does not have IAE so if this
// call is made, presume such a client and return an error with the
// OID4VP request, OID4VCI 1.1+ clients will know to use IAE instead
// if there is no verified presentation yet, request one
const {results} = exchange.variables;
if(!results[exchange.step]?.verifyPresentationResults?.verified) {
// note: only the "default" `clientProfileId` is supported at this
// time because there isn't presently a defined way to specify
// alternatives
const clientProfileId = step.openId.clientProfiles ?
'default' : undefined;
// get authorization request
const {authorizationRequest} = await getStepAuthorizationRequest({
workflow, exchange, step, clientProfileId
});
return _requestOID4VP({authorizationRequest, res});
}
return;
}
// check to see if step requires a DID proof
if(step.divpDidProofRequest || step.jwtDidProofRequest) {
// handle OID4VCI specialized DI VP / JWT DID Proof request...
// `proof` must be in every credential request; if any request is
// missing `proof` then request a DID proof
const acceptableProofTypes = new Set();
if(step.divpDidProofRequest) {
acceptableProofTypes.add('di_vp');
}
if(step.jwtDidProofRequest) {
acceptableProofTypes.add('jwt');
}
const hasAcceptableProofType =
cr => Object.keys(cr.proofs ?? {}).some(
k => acceptableProofTypes.has(k));
if(credentialRequests.some(cr => !hasAcceptableProofType(cr))) {
didProofRequired = true;
return _requestDidProof({res, exchangeRecord});
}
// verify every DID proof and get resulting DIDs
const results = await Promise.all(
credentialRequests.map(async credentialRequest => {
return verifyCredentialRequestProof({
credentialRequest, workflow, exchange
});
}));
// require `did` to be the same for every proof
// FIXME: determine if this needs to be more flexible
const did = results[0];
if(results.some(d => did !== d)) {
// FIXME: improve error
throw new Error('every DID must be the same');
}
// store did results in variables associated with current step
exchange.variables.results[exchange.step] = {
...exchange.variables.results[exchange.step],
// common use case of DID Authentication; provide `did` for ease
// of use in templates
did
};
}
},
inputRequired({exchange, step}) {
// if issue result has been generated...
if(issueResult) {
// reapply any stored credentials in case the exchange was concurrently
// updated by another credential request call
const {storedCredentials, matchingCredentialIdentifiers} = issueResult;
if(issueResult.storedCredentials) {
for(const stored of storedCredentials) {
setVariable({
variables: exchange.variables,
name: stored.name,
value: stored.value
});
}
}
// mark all matching `supportedCredentialRequests` as processed
supportedCredentialRequests
.filter(r =>
matchingCredentialIdentifiers.has(r.credentialIdentifier))
.forEach(r => r.processed = true);
// do not generate any VPR or issue anything else, additional requests
// must be made when using OID4VCI
step.verifiablePresentationRequest = undefined;
step.verifiablePresentation = undefined;
step.issueRequests = [];
// if any supported credential requests has not yet been processed,
// then input is required in the form of another credential request
return supportedCredentialRequests.some(r => !r.processed);
}
// otherwise, input is required if:
// 1. a `divp|jwtDidProofRequest` is required and hasn't been provided
// 2. OID4VP is enabled and no OID4VP result has been stored yet
return didProofRequired || (step.openId && !exchange.variables
.results[exchange.step]?.openId?.authorizationRequest);
},
async issue({
workflow, exchange, step, issueRequestsParams,
verifiablePresentation
}) {
// issue result already generated, skip
if(issueResult) {
return issueResult;
}
// filter `supportedCredentialRequests` using matching credential
// identifiers and map to only those `issueRequestsParams` that are to
// be issued now
issueRequestsParams = supportedCredentialRequests
.filter(r => matchingCredentialIdentifiers.has(r.credentialIdentifier))
.map(r => issueRequestsParams[r.issueRequestsParamsIndex])
.filter(p => !!p);
// perform issuance and capture result to return it to the client and
// to prevent subsequent reissuance if a concurrent request is made for
// other credentials
issueResult = await defaultIssue({
workflow, exchange, step, issueRequestsParams,
verifiablePresentation, format
});
issueResult.matchingCredentialIdentifiers =
matchingCredentialIdentifiers;
return issueResult;
},
isStepComplete() {
// step complete if all supported credential requests have been processed
return supportedCredentialRequests.every(r => r.processed);
}
});
// always allow retrying with OID4VCI; issued VCs will be stored in memory
// and an assumption is made that workflow steps WILL NOT change issue
// requests in a step once `supportedCredentialRequests` has been created
exchangeProcessor.canRetry = true;
await exchangeProcessor.process();
// use `issueResult` response
const response = issueResult?.response;
if(!response?.verifiablePresentation) {
return null;
}
return {response, format};
}
export function supportsOID4VCI({exchange}) {
// FIXME: might want something more explicit/or check in `workflow` and not
// exchange
return exchange.openId?.preAuthorizedCode !== undefined;
}
function _assertOID4VCISupported({exchange}) {
if(!supportsOID4VCI({exchange})) {
throw new BedrockError('OID4VCI is not supported by this exchange.', {
name: 'NotSupportedError',
details: {httpStatusCode: 400, public: true}
});
}
}
async function _checkAuthz({req, workflow, exchange}) {
// optional oauth2 options
const {oauth2} = exchange.openId;
const {maxClockSkew} = oauth2;
// audience is always the `exchangeId` and cannot be configured; this
// prevents attacks where access tokens could otherwise be generated
// if the AS keys were compromised; the `exchangeId` must also be known
const exchangeId = `${workflow.id}/exchanges/${req.params.exchangeId}`;
const audience = exchangeId;
// `issuerConfigUrl` is always based off of the `exchangeId` as well
const parsedIssuer = new URL(exchangeId);
const issuerConfigUrl =
`${parsedIssuer.origin}/.well-known/oauth-authorization-server` +
parsedIssuer.pathname;
// FIXME: `allowedAlgorithms` should be computed from `oauth2.keyPair`
// const allowedAlgorithms =
// ensure access token is valid
await checkAccessToken({req, issuerConfigUrl, maxClockSkew, audience});
}
async function _createExchangeAccessToken({workflow, exchangeRecord}) {
// FIXME: set `exp` to max of 15 minutes / configured max minutes
const expires = exchangeRecord.meta.expires;
const exp = Math.floor(expires.getTime() / 1000);
// create access token
const {exchange} = exchangeRecord;
const {openId: {oauth2: {keyPair: {privateKeyJwk}}}} = exchange;
const exchangeId = `${workflow.id}/exchanges/${exchange.id}`;
const {accessToken, ttl} = await _createOAuth2AccessToken({
privateKeyJwk, audience: exchangeId, action: 'write', target: exchangeId,
exp, iss: exchangeId
});
return {accessToken, ttl};
}
function _createSupportedCredentialRequests({
workflow, exchange, step
}) {
let supportedCredentialRequests;
const issueRequestsParams = getIssueRequestsParams({
workflow, exchange, step
});
// determine if issue request params is legacy or modern
const isDraft13 = issueRequestsParams.some(
p => !p?.oid4vci?.credentialConfigurationId);
if(isDraft13) {
supportedCredentialRequests =
draft13.createSupportedCredentialRequests({
workflow, exchange, issueRequestsParams, step
});
} else {
supportedCredentialRequests = [];
for(const [index, params] of issueRequestsParams.entries()) {
const {credentialConfigurationId} = params.oid4vci;
supportedCredentialRequests.push({
credentialConfigurationId,
credentialIdentifier: uuid(),
issueRequestsParamsIndex: index,
processed: false
});
}
}
return supportedCredentialRequests;
}
async function _createOAuth2AccessToken({
privateKeyJwk, audience, action, target, exp, iss, nbf, typ = 'at+jwt'
}) {
const alg = _getAlgFromPrivateKey({privateKeyJwk});
const scope = `${action}:${target}`;
const builder = new SignJWT({scope})
.setProtectedHeader({alg, typ})
.setIssuer(iss)
.setAudience(audience);
let ttl;
if(exp !== undefined) {
builder.setExpirationTime(exp);
ttl = Math.max(0, exp - Math.floor(Date.now() / 1000));
} else {
// default to 15 minute expiration time
builder.setExpirationTime('15m');
ttl = Math.floor(Date.now() / 1000) + 15 * 60;
}
if(nbf !== undefined) {
builder.setNotBefore(nbf);
}
const key = await importJWK({...privateKeyJwk, alg});
const accessToken = await builder.sign(key);
return {accessToken, ttl};
}
function _getAlgFromPrivateKey({privateKeyJwk}) {
if(privateKeyJwk.alg) {
return privateKeyJwk.alg;
}
if(privateKeyJwk.kty === 'EC' && privateKeyJwk.crv) {
if(privateKeyJwk.crv.startsWith('P-')) {
return `ES${privateKeyJwk.crv.slice(2)}`;
}
if(privateKeyJwk.crv === 'secp256k1') {
return 'ES256K';
}
}
if(privateKeyJwk.kty === 'OKP' && privateKeyJwk.crv?.startsWith('Ed')) {
return 'EdDSA';
}
if(privateKeyJwk.kty === 'RSA') {
return 'PS256';
}
return 'invalid';
}
function _getSupportedCredentialConfigurations({workflow, exchange, step}) {
// get all OID4VCI credential configuration IDs from issue requests in step
const issueRequestsParams = getIssueRequestsParams({
workflow, exchange, step
});
const credentialConfigurationIds = new Set([
...issueRequestsParams
.map(p => p?.oid4vci?.credentialConfigurationId)
.filter(id => id !== undefined)
]);
// get all issuer instances for the workflow
const issuerInstances = getWorkflowIssuerInstances({workflow});
// in modern workflows, credential configuration IDs are explicitly provided
if(credentialConfigurationIds.size > 0) {
// map each ID to a credential configuration in an issuer instance
const supported = new Map();
for(const id of credentialConfigurationIds) {
const match = issuerInstances.find(
ii => ii.oid4vci?.supportedCredentialConfigurations?.[id]);
if(match) {
supported.set(id, match.oid4vci.supportedCredentialConfigurations[id]);
}
}
return Object.fromEntries(supported.entries());
}
// no explicit IDs; create legacy supported credential configurations
return draft13.createSupportedCredentialConfigurations({
exchange, issuerInstances, step
});
}
async function _getSupportedCredentialRequests({
exchangeProcessor, workflow, exchange, step
}) {
// get `supportedCredentialRequests` from step results
const stepResults = exchange.variables.results[exchange.step];
let supportedCredentialRequests = stepResults
?.openId?.supportedCredentialRequests;
// if `supportedCredentialRequests` is not set, create it; this can only
// happen in the degenerate case that an older version of the software
// provided the access token to the client
if(!supportedCredentialRequests) {
supportedCredentialRequests = _createSupportedCredentialRequests({
workflow, exchange, step
});
exchange.variables.results[exchange.step] = {
...exchange.variables.results[exchange.step],
openId: {
...exchange.variables.results[exchange.step]?.openId,
supportedCredentialRequests
}
};
// explicitly update exchange to ensure `supportedCredentialRequests`
// are committed
await exchangeProcessor.updateExchange({step});
}
return supportedCredentialRequests;
}
async function _requestDidProof({res, exchangeRecord}) {
/* `9.4 Credential Issuer-provided nonce` allows the credential
issuer infrastructure to provide the nonce via an error:
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
{
"error": "invalid_or_missing_proof"
"error_description":
"Credential issuer requires proof element in Credential Request"
"c_nonce": "8YE9hCnyV2",
"c_nonce_expires_in": 86400
}*/
/* OID4VCI exchanges themselves are not replayable and single-step, so the
challenge to be signed is just the exchange ID itself. An exchange cannot
be reused and neither can a challenge. */
const {exchange, meta: {expires}} = exchangeRecord;
const ttl = Math.floor((expires.getTime() - Date.now()) / 1000);
_sendOID4Error({
res,
error: 'invalid_proof',
description:
'Credential issuer requires proof element in Credential Request',
// use exchange ID
c_nonce: exchange.id,
// use exchange expiration period
c_nonce_expires_in: ttl
});
}
async function _requestOID4VP({authorizationRequest, res}) {
/* Error thrown when OID4VP is required to complete OID4VCI:
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
{
"error": "presentation_required"
"error_description":
"Credential issuer requires presentation before Credential Request"
"authorization_request": {
"response_type": "vp_token",
"presentation_definition": {
id: "<urn:uuid>",
input_descriptors: {...}
},
"response_mode": "direct_post"
}
}*/
/* OID4VCI exchanges themselves are not replayable and single-step, so the
challenge to be signed is just the exchange ID itself. An exchange cannot
be reused and neither can a challenge. */
_sendOID4Error({
res,
error: 'presentation_required',
description:
'Credential issuer requires presentation before Credential Request',
authorization_request: authorizationRequest
});
}
function _sendOID4Error({res, error, description, status = 400, ...rest}) {
res.status(status).json({
error,
error_description: description,
...rest
});
}
function _validate(validator, data) {
const result = validator(data);
if(!result.valid) {
throw result.error;
}
}