Skip to content

Commit

Permalink
Add step.issueRequests feature.
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed Oct 15, 2024
1 parent a4c2047 commit 32bc180
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 64 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# bedrock-vc-delivery ChangeLog

## 6.3.0 - 2024-10-dd

### Added
- Add `issueRequests` feature for expressing parameters for issuing VCs
in a particular step. The `issueRequest` value must be an array, with
each element containing parameters for issuing a VC. The parameters
must minimally include a credential template ID or index that
references a credential template from the associated workflow. The
parameters may optionally specify alternative variables to use when
evaluating the template, either via an object or a string, where
the string includes the name of a variable from the workflow's
main `variables`.

## 6.2.0 - 2024-10-02

### Changed
Expand Down
36 changes: 34 additions & 2 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ const ALLOWED_ERROR_KEYS = [
];

export async function evaluateTemplate({
workflow, exchange, typedTemplate
workflow, exchange, typedTemplate, variables
} = {}) {
// run jsonata compiler; only `jsonata` template type is supported and this
// assumes only this template type will be passed in
const {template} = typedTemplate;
variables = variables ?? getTemplateVariables({workflow, exchange});
return jsonata(template).evaluate(variables, variables);
}

export function getTemplateVariables({workflow, exchange} = {}) {
const {variables = {}} = exchange;
// always include `globals` as keyword for self-referencing exchange info
variables.globals = {
Expand All @@ -38,7 +43,7 @@ export async function evaluateTemplate({
id: exchange.id
}
};
return jsonata(template).evaluate(variables, variables);
return variables;
}

export function getWorkflowId({routePrefix, localId} = {}) {
Expand Down Expand Up @@ -207,6 +212,33 @@ export async function unenvelopePresentation({
return {presentation, ...result};
}

export async function validateStep({step} = {}) {
// FIXME: use `ajv` and do JSON schema check
if(Object.keys(step).length === 0) {
throw new BedrockError('Empty exchange step detected.', {
name: 'DataError',
details: {httpStatusCode: 500, public: true}
});
}
if(step.issueRequests !== undefined && !Array.isArray(step.issueRequests)) {
throw new BedrockError(
'Invalid "issueRequests" in step.', {
name: 'DataError',
details: {httpStatusCode: 500, public: true}
});
}
// use of `jwtDidProofRequest` and `openId` together is prohibited
const {jwtDidProofRequest, openId} = step;
if(jwtDidProofRequest && openId) {
throw new BedrockError(
'Invalid workflow configuration; only one of ' +
'"jwtDidProofRequest" and "openId" is permitted in a step.', {
name: 'DataError',
details: {httpStatusCode: 500, public: true}
});
}
}

function _getEnvelope({envelope, format}) {
const isString = typeof envelope === 'string';
if(isString) {
Expand Down
95 changes: 80 additions & 15 deletions lib/issue.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,115 @@
/*!
* Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
*/
import * as bedrock from '@bedrock/core';
import {
evaluateTemplate,
getTemplateVariables,
getWorkflowIssuerInstances,
getZcapClient
} from './helpers.js';
import {createPresentation} from '@digitalbazaar/vc';

const {util: {BedrockError}} = bedrock;

export async function issue({
workflow, exchange, format = 'application/vc'
workflow, exchange, step, format = 'application/vc'
} = {}) {
// use any templates from workflow and variables from exchange to produce
// credentials to be issued; issue via the configured issuer instance
const verifiableCredential = [];
const {credentialTemplates = []} = workflow;
if(!credentialTemplates || credentialTemplates.length === 0) {
// nothing to issue
return {response: {}};
}

// evaluate template
const issueRequests = await Promise.all(credentialTemplates.map(
typedTemplate => evaluateTemplate({workflow, exchange, typedTemplate})));
// generate all issue requests for current step in exchange
const issueRequests = await _createIssueRequests({
workflow, exchange, step, credentialTemplates
});
// issue all VCs
const vcs = await _issue({workflow, issueRequests, format});
verifiableCredential.push(...vcs);

// generate VP to return VCs
const verifiablePresentation = createPresentation();
// FIXME: add any encrypted VCs to VP

// add any issued VCs to VP
if(verifiableCredential.length > 0) {
verifiablePresentation.verifiableCredential = verifiableCredential;
if(vcs.length > 0) {
verifiablePresentation.verifiableCredential = vcs;
}
return {response: {verifiablePresentation}, format};
}

async function _createIssueRequests({
workflow, exchange, step, credentialTemplates
}) {
// if step does not define `issueRequests`, then use all for templates for
// backwards compatibility
let params;
if(!step.issueRequests) {
params = credentialTemplates.map(typedTemplate => ({typedTemplate}));
} else {
// resolve all issue requests params in parallel
const variables = getTemplateVariables({workflow, exchange});
params = await Promise.all(step.issueRequests.map(async r => {
// find the typed template to use
let typedTemplate;
if(r.credentialTemplateIndex !== undefined) {
typedTemplate = credentialTemplates[r.credentialTemplateIndex];
} else if(r.credentialTemplateId !== undefined) {
typedTemplate = credentialTemplates.find(
t => t.id === r.credentialTemplateId);
}
if(typedTemplate === undefined) {
throw new BedrockError(
'Credential template ' +
`"${r.credentialTemplateIndex ?? r.credentialTemplateId}" ` +
'not found.', {
name: 'DataError',
details: {httpStatusCode: 500, public: true}
});
}

// allow different variables to be specified for the typed template
let vars = variables;
if(r.variables !== undefined) {
if(typeof r.variables === 'string') {
vars = variables[r.variables];
} else {
vars = r.variables;
}
if(!(vars && typeof vars === 'object')) {
throw new BedrockError(
`Issue request variables "${r.variables}" not found or invalid.`, {
name: 'DataError',
details: {httpStatusCode: 500, public: true}
});
}
}
return {
typedTemplate,
variables: {
// always include globals but allow local override
globals: variables.globals,
...r.variables
}
};
}));
}

// evaluate all issue requests
return Promise.all(params.map(({typedTemplate, variables}) =>
evaluateTemplate({workflow, exchange, typedTemplate, variables})));
}

function _getIssueZcap({workflow, zcaps, format}) {
const issuerInstances = getWorkflowIssuerInstances({workflow});
const {zcapReferenceIds: {issue: issueRefId}} = issuerInstances.find(
({supportedFormats}) => supportedFormats.includes(format));
return zcaps[issueRefId];
}

async function _issue({workflow, issueRequests, format} = {}) {
// create zcap client for issuing VCs
const {zcapClient, zcaps} = await getZcapClient({workflow});
Expand Down Expand Up @@ -74,10 +146,3 @@ async function _issue({workflow, issueRequests, format} = {}) {

return issuedVCs;
}

function _getIssueZcap({workflow, zcaps, format}) {
const issuerInstances = getWorkflowIssuerInstances({workflow});
const {zcapReferenceIds: {issue: issueRefId}} = issuerInstances.find(
({supportedFormats}) => supportedFormats.includes(format));
return zcaps[issueRefId];
}
27 changes: 6 additions & 21 deletions lib/oid4/oid4vci.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import * as bedrock from '@bedrock/core';
import * as exchanges from '../exchanges.js';
import {
deepEqual, evaluateTemplate, getWorkflowIssuerInstances
deepEqual, evaluateTemplate, getWorkflowIssuerInstances, validateStep
} from '../helpers.js';
import {importJWK, SignJWT} from 'jose';
import {checkAccessToken} from '@bedrock/oauth2-verifier';
Expand Down Expand Up @@ -427,33 +427,18 @@ async function _processExchange({
});

// process exchange step if present
let step;
const currentStep = exchange.step;
if(currentStep) {
let step = workflow.steps[exchange.step];
step = workflow.steps[exchange.step];
if(step.stepTemplate) {
// generate step from the template; assume the template type is
// `jsonata` per the JSON schema
step = await evaluateTemplate(
{workflow, exchange, typedTemplate: step.stepTemplate});
if(Object.keys(step).length === 0) {
throw new BedrockError('Could not create exchange step.', {
name: 'DataError',
details: {httpStatusCode: 500, public: true}
});
}
}

// do late workflow configuration validation
const {jwtDidProofRequest, openId} = step;
// use of `jwtDidProofRequest` and `openId` together is prohibited
if(jwtDidProofRequest && openId) {
throw new BedrockError(
'Invalid workflow configuration; only one of ' +
'"jwtDidProofRequest" and "openId" is permitted in a step.', {
name: 'DataError',
details: {httpStatusCode: 500, public: true}
});
}
await validateStep({step});
const {jwtDidProofRequest} = step;

// check to see if step supports OID4VP during OID4VCI
if(step.openId) {
Expand Down Expand Up @@ -518,7 +503,7 @@ async function _processExchange({
// replay attack detected) after exchange has been marked complete

// issue VCs
return issue({workflow, exchange, format});
return issue({workflow, exchange, step, format});
} catch(e) {
if(e.name === 'InvalidStateError') {
throw e;
Expand Down
11 changes: 4 additions & 7 deletions lib/oid4/oid4vp.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
*/
import * as bedrock from '@bedrock/core';
import * as exchanges from '../exchanges.js';
import {evaluateTemplate, unenvelopePresentation} from '../helpers.js';
import {
evaluateTemplate, unenvelopePresentation, validateStep
} from '../helpers.js';
import {
presentationSubmission as presentationSubmissionSchema,
verifiablePresentation as verifiablePresentationSchema
Expand Down Expand Up @@ -50,13 +52,8 @@ export async function getAuthorizationRequest({req}) {
// `jsonata` per the JSON schema
step = await evaluateTemplate(
{workflow, exchange, typedTemplate: step.stepTemplate});
if(Object.keys(step).length === 0) {
throw new BedrockError('Could not create authorization request.', {
name: 'DataError',
details: {httpStatusCode: 500, public: true}
});
}
}
await validateStep({step});

// step must have `openId` to perform OID4VP
if(!step.openId) {
Expand Down
11 changes: 3 additions & 8 deletions lib/vcapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as bedrock from '@bedrock/core';
import * as exchanges from './exchanges.js';
import {createChallenge as _createChallenge, verify} from './verify.js';
import {
evaluateTemplate, generateRandom, unenvelopePresentation
evaluateTemplate, generateRandom, unenvelopePresentation, validateStep
} from './helpers.js';
import {exportJWK, generateKeyPair, importJWK} from 'jose';
import {compile} from '@bedrock/validation';
Expand Down Expand Up @@ -121,13 +121,8 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
// `jsonata` per the JSON schema
step = await evaluateTemplate(
{workflow, exchange, typedTemplate: step.stepTemplate});
if(Object.keys(step).length === 0) {
throw new BedrockError('Empty step detected.', {
name: 'DataError',
details: {httpStatusCode: 500, public: true}
});
}
}
await validateStep({step});

// if next step is the same as the current step, throw an error
if(step.nextStep === currentStep) {
Expand Down Expand Up @@ -272,7 +267,7 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {

// issue any VCs; may return an empty response if the step defines no
// VCs to issue
const {response} = await issue({workflow, exchange});
const {response} = await issue({workflow, exchange, step});

// if last `step` has a redirect URL, include it in the response
if(step?.redirectUrl) {
Expand Down
Loading

0 comments on commit 32bc180

Please sign in to comment.