UNPKG

@bedrock/vc-delivery

Version:
574 lines (452 loc) 20.1 kB
# @bedrock/vc-delivery A [Bedrock][] module that provides a **Verifiable Credential (VC) Workflow Service** for issuing and exchanging VCs using multiple protocols. It enables configurable, multi-step credential exchange workflows with support for the VC-API, OpenID for Verifiable Credential Issuance (OID4VCI), OpenID for Verifiable Presentations (OID4VP), and an Invite Request protocol. ## Table of Contents - [Background](#background) - [Features](#features) - [Requirements](#requirements) - [Installation](#installation) - [Quick Start](#quick-start) - [Configuration](#configuration) - [Workflow Config](#workflow-config) - [Credential Templates](#credential-templates) - [Steps](#steps) - [Issuer Instances](#issuer-instances) - [OID4VCI Options](#oid4vci-options) - [OID4VP Client Profiles](#oid4vp-client-profiles) - [HTTP API](#http-api) - [Exchanges](#exchanges) - [Protocols](#protocols) - [OID4VCI Endpoints](#oid4vci-endpoints) - [OID4VP Endpoints](#oid4vp-endpoints) - [Invite Request Endpoints](#invite-request-endpoints) - [Protocols](#supported-protocols) - [VC-API](#vc-api) - [OID4VCI](#oid4vci) - [OID4VP](#oid4vp) - [Invite Request](#invite-request) - [Exchange Lifecycle](#exchange-lifecycle) - [Multi-Step Workflows](#multi-step-workflows) - [Security](#security) - [Backwards Compatibility](#backwards-compatibility) - [License](#license) ## Background This module implements the server-side infrastructure for **Verifiable Credential delivery**. A *workflow* is a reusable configuration that defines how credentials are issued and/or verified. An *exchange* is a single instance of that workflow — a live, stateful session between a workflow service and an exchange client (e.g. a digital wallet). Workflows are registered with an associated set of authorization capabilities (zcaps) that grant the service permission to issue and verify credentials on behalf of the workflow owner. ## Features - **Multi-protocol support**: VC-API, OID4VCI v1.0, OID4VP 1.0, and Invite Request (with backwards compatibility for OID4VCI Draft 13 and OID4VP Draft 18). - **Multi-step exchanges**: Define sequential steps that mix presentation verification and credential issuance within a single exchange. - **JSONata credential templates**: Dynamically generate credential content from exchange variables using [JSONata][] expressions. - **Multiple issuer instances**: Configure up to 10 independent issuer instances per workflow, each with its own zcap and supported credential formats. - **OID4VP client profiles**: Configure up to 10 OID4VP client profiles per workflow, including support for signed authorization requests and mDL/ISO 18013-7 presentation flows. - **OAuth2 / zcap authorization**: Workflow management endpoints support both zcap-based and OAuth2-based authorization. - **Configurable exchange TTL**: Exchanges expire automatically (default 15 minutes, maximum 48 hours). ## Requirements - Node.js >= 20 - MongoDB (via [`@bedrock/mongodb`][]) - A running [Bedrock][] application with the required peer dependencies (see `peerDependencies` in `package.json`) ## Installation ```sh npm install @bedrock/vc-delivery ``` Then import the module in your Bedrock application entry point: ```js import '@bedrock/vc-delivery'; ``` The module self-registers with Bedrock on import and will set up all routes and storage on startup. ## Quick Start ### 1. Create a workflow configuration ```js // POST /workflows { "sequence": 0, "controller": "did:key:z6Mk...", "credentialTemplates": [{ "type": "jsonata", "template": "{ '@context': ['https://www.w3.org/2018/credentials/v1'], 'type': ['VerifiableCredential'], 'credentialSubject': {'id': subject} }" }], "initialStep": "issue", "steps": { "issue": { // you can use static step parameters like this, but also see // `stepTemplate` below for more powerful dynamic steps instead! "issueRequests": [{ "credentialTemplateIndex": 0 }] } }, "zcaps": { "issue": { /* delegated issue zcap */ } } } ``` ### 2. Create an exchange ```js // POST /workflows/:workflowId/exchanges { "ttl": 900, "variables": { "subject": "did:example:holder123" } } // Response: 204 No Content; Location header contains exchange URL ``` ### 3. Execute the exchange (VC-API) ```js // POST /workflows/:workflowId/exchanges/:exchangeId {} // Response: { "verifiablePresentation": { /* VP containing issued VCs */ } } ``` ## Configuration ### Workflow Config Workflows are created and managed via the [`@bedrock/service-core`][] HTTP API at `/workflows`. The configuration body accepts the following custom properties in addition to standard service-core fields: | Property | Type | Required | Description | |----------|------|----------|-------------| | `credentialTemplates` | Array | No | List of JSONata credential templates to issue | | `steps` | Object | No | Named step definitions for multi-step exchanges | | `initialStep` | String | Required if `steps` present | Name of the first step to execute | | `issuerInstances` | Array | No | Up to 10 issuer instance configurations | | `zcaps` | Object | No | Authorization capability references | If `credentialTemplates` are provided, either a top-level `zcaps.issue` or at least one `issuerInstances` entry with its own issue zcap is required. ### Credential Templates Credential templates use [JSONata][] to dynamically produce credential JSON. Exchange `variables` and built-in `globals` are available as template bindings. ```js "credentialTemplates": [{ "type": "jsonata", "id": "myTemplate", // optional, for referencing in issueRequests "template": "{ ... }" // JSONata expression returning a VC object }] ``` **Built-in template globals:** | Variable | Description | |----------|-------------| | `globals.exchange.id` | The local exchange ID | | `globals.exchangeId` | The full exchange URL | | `globals.workflow.id` | The workflow ID | ### Steps Steps define the interactive logic of an exchange. Each step can request a Verifiable Presentation, issue credentials, or both. ```js "steps": { "verify": { "verifiablePresentationRequest": { /* VPR object */ }, "nextStep": "issue" }, "issue": { "issueRequests": [ { "credentialTemplateIndex": 0 } ] } }, "initialStep": "verify" ``` **Step fields:** | Field | Description | |-------|-------------| | `issueRequests` | Array of credential issue request parameters | | `verifiablePresentationRequest` | VPR to send to the client | | `verifiablePresentation` | VP to return immediately without client input | | `createChallenge` | `true` to generate and attach a challenge nonce to the VPR | | `nextStep` | Name of the step to transition to after this one completes | | `openId` | OID4VP options for this step | | `jwtDidProofRequest` | Request a DID-bound JWT proof | | `divpDidProofRequest` | Request a DI-VP DID proof | | `callback.url` | URL to call when exchange state updates | | `presentationSchema` | JSON Schema to validate received presentations | | `verifyPresentationOptions` | Additional options for presentation verification | | `verifyPresentationResultSchema` | JSON Schema to validate verification results | | `allowUnprotectedPresentation` | Allow presentations without a proof | | `redirectUrl` | URL to redirect the exchange client upon completion | Steps may also be defined as **templated steps**, where a JSONata `stepTemplate` expression dynamically generates the step configuration from exchange variables at runtime: ```js "steps": { "issue": { "stepTemplate": { "type": "jsonata", "template": "{ 'issueRequests': [{ 'credentialTemplateIndex': 0 }] }" } } } ``` ### Issuer Instances `issuerInstances` allows a workflow to configure multiple issuers, each with different capabilities and supported credential media types. Up to 10 instances are supported. ```js "issuerInstances": [{ "id": "myIssuer", // optional identifier "supportedMediaTypes": ["application/vc"], "zcapReferenceIds": { "issue": "myIssuerZcap" }, "oid4vci": { "supportedCredentialConfigurations": { // optional explicit OID4VCI config "MyCredential_ldp_vc": { /* credential configuration */ } } } }] ``` Each issuer instance references a zcap in the workflow's `zcaps` map by its `zcapReferenceIds.issue` key. ### OID4VCI Options When creating an exchange with OID4VCI support, pass an `openId` object in the exchange creation body: ```js // POST /workflows/:workflowId/exchanges { "openId": { "preAuthorizedCode": "abc123", "oauth2": { "generateKeyPair": { "algorithm": "ES256" } // or provide an existing key pair: // "keyPair": { "privateKeyJwk": {...}, "publicKeyJwk": {...} } } } } ``` > **Deprecated:** `expectedCredentialRequests` is deprecated and should not be > used in new workflows. Credential configurations are inferred from > `issuerInstances` instead. ### OID4VP Client Profiles OID4VP client profiles allow fine-grained control over how authorization requests are constructed and which wallet/verifier protocols are used. Up to 10 profiles are supported per workflow step. ```js // in a step: "openId": { "clientProfiles": { "myProfile": { "createAuthorizationRequest": "/authzReqVar", "client_id": "https://verifier.example", "client_id_scheme": "redirect_uri", "response_mode": "direct_post", "response_uri": "https://verifier.example/callback", "protocolUrlParameters": { "name": "OID4VP", "scheme": "openid4vp", "version": "OID4VP-1.0" // or "OID4VP-draft18" } } } } ``` > **Deprecated:** `presentation_definition` (Presentation Exchange) is a > pre-OID4VP 1.0 mechanism and is deprecated. Prefer `dcql_query` for new > implementations. Supported `response_mode` values include `direct_post`, `direct_post.jwt`, `dc_api`, and `dc_api.jwt`. ## HTTP API All endpoints support CORS. Authorization uses zcaps or OAuth2 — not cookies — so CSRF is not a concern. ### Exchanges #### Create Exchange ``` POST /workflows/:workflowId/exchanges ``` **Body** (`application/json`): | Field | Type | Description | |-------|------|-------------| | `ttl` | Number | Seconds until expiry (default: 900, max: 172800) | | `expires` | String | ISO 8601 expiry date (alternative to `ttl`) | | `variables` | Object | Key/value pairs available in credential templates and step templates | | `openId` | Object | OID4VCI configuration (see [OID4VCI Options](#oid4vci-options)) | > **Note:** The schema uses `additionalProperties: false`; fields not listed above (including `step`) will be rejected by the validator. **Response**: `204 No Content` with a `Location` header pointing to the exchange URL. #### Get Exchange ``` GET /workflows/:workflowId/exchanges/:exchangeId ``` Returns the current exchange state. Private keys and secrets are never included in the response. #### Use Exchange (VC-API) ``` POST /workflows/:workflowId/exchanges/:exchangeId ``` **Body** (`application/json`): | Field | Type | Description | |-------|------|-------------| | `verifiablePresentation` | Object | VP to submit (if requested by the current step) | **Response**: A JSON object, which may contain: - `verifiablePresentation` — a VP with issued credentials - `verifiablePresentationRequest` — a VPR requiring the client to submit a VP ### Protocols #### Get Supported Protocols ``` GET /workflows/:workflowId/exchanges/:exchangeId/protocols ``` Returns a `{"protocols": {...}}` object listing all interaction URLs supported by the exchange. Key names are: - `vcapi` — always this literal string when VC-API is supported - `inviteRequest` — always this literal string when an invite request step is active - OID4VCI — always `OID4VCI` - OID4VP — defaults to `OID4VP` but is controlled by the `protocolUrlParameters.name` field on each OID4VP client profile, so named profiles may produce arbitrary keys (e.g. a profile with `"name": "bar"` produces a `bar` key) ### OID4VCI Endpoints The well-known metadata endpoints are served at **two URL shapes** for maximum wallet compatibility: | Method | Path | Description | |--------|------|-------------| | `GET` | `/.well-known/openid-credential-issuer/workflows/:wfId/exchanges/:id` | Credential issuer metadata (prefix form) | | `GET` | `/workflows/:wfId/exchanges/:id/.well-known/openid-credential-issuer` | Credential issuer metadata (suffix form) | | `GET` | `/.well-known/oauth-authorization-server/workflows/:wfId/exchanges/:id` | AS metadata (prefix form) | | `GET` | `/workflows/:wfId/exchanges/:id/.well-known/oauth-authorization-server` | AS metadata (suffix form) | | `GET` | `…/exchanges/:id/openid/credential-offer` | Credential offer object | | `POST` | `…/exchanges/:id/openid/token` | Access token endpoint | | `POST` | `…/exchanges/:id/openid/credential` | Credential request endpoint | | `POST` | `…/exchanges/:id/openid/batch_credential` | Batch credential requests | | `GET` | `…/exchanges/:id/openid/jwks` | JSON Web Key Set | | `POST` | `…/exchanges/:id/openid/nonce` | Nonce endpoint | ### OID4VP Endpoints Legacy (no client profile ID) and modern (with client profile ID) routes are both supported: | Method | Path | Description | |--------|------|-------------| | `GET/POST` | `…/exchanges/:id/openid/client/authorization/request` | Retrieve authorization request (default profile) | | `POST` | `…/exchanges/:id/openid/client/authorization/response` | Submit authorization response (default profile) | | `GET/POST` | `…/exchanges/:id/openid/clients/:clientProfileId/authorization/request` | Retrieve authorization request (named profile) | | `POST` | `…/exchanges/:id/openid/clients/:clientProfileId/authorization/response` | Submit authorization response (named profile) | ### Invite Request Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `…/exchanges/:id/invite-request/response` | Submit invite response | ## Supported Protocols ### VC-API The [VC-API][] protocol is automatically supported for any workflow that has `credentialTemplates`, or for any step that has a `verifiablePresentationRequest` or `verifiablePresentation`. The exchange URL itself serves as the `vcapi` interaction endpoint. ### OID4VCI [OpenID for Verifiable Credential Issuance][OID4VCI-spec] is supported when an exchange is created with an `openId` configuration object. Supported grant types: - Pre-authorized code flow (`urn:ietf:params:oauth:grant-type:pre-authorized_code`) Supported proof types for credential requests: - `jwt` (JWT-based DID proof) - `di_vp` (Data Integrity VP DID proof) Supported credential formats (OID4VCI v1.0): - `ldp_vc` / `application/vc` (JSON-LD Verifiable Credential) - `jwt_vc_json` / `application/jwt` (JWT Verifiable Credential) Supported for backwards compatibility with OID4VCI Draft 13 (deprecated): - `jwt_vc_json-ld` (JSON-LD-based VC-JWT envelope) > **Deprecated:** OID4VCI Draft 13 credential request formats are supported > for backwards compatibility but are deprecated. New implementations should > use OID4VCI v1.0. ### OID4VP [OpenID for Verifiable Presentations][OID4VP-spec] is supported when a step includes an `openId` configuration. The module supports: - OID4VP 1.0+ (with prefixed `client_id` schemes and `request_uri_method=post`) - Signed authorization requests (via `authorizationRequestSigningParameters`) - Multiple response modes: `direct_post`, `direct_post.jwt`, `dc_api`, `dc_api.jwt` - mDL / ISO 18013-7 presentations (Annex B, C, and D handover types) - DCQL queries (`dcql_query`) Supported for backwards compatibility (deprecated): - OID4VP Draft 18 - Presentation Definitions (`presentation_definition`) ### Invite Request A lightweight invite-based protocol. When a step exposes an `inviteRequest` property, the exchange will include an `inviteRequest` URL in its `protocols` response. The client POSTs an invite response (containing a `url`, `purpose`, and optional `referenceId`) to complete this step. Because the static `computedStep` schema does not declare an `inviteRequest` property (it uses `additionalProperties: false`), the property must be injected at runtime via a `stepTemplate` rather than declared directly in the workflow configuration: ```js "steps": { "myStep": { "stepTemplate": { "type": "jsonata", "template": "{ \"inviteRequest\": inviteRequest }" } } } ``` The exchange variable `inviteRequest` must be set to `true` when creating the exchange to activate the invite request protocol for that step. ## Exchange Lifecycle ```mermaid graph TD A[Create Exchange]:::accent0 --> B[Exchange Pending]:::accent1 B --> C{Protocol}:::accent2 C -->|VC-API| D[POST to exchange URL] C -->|OID4VCI| E[Pre-auth code flow] C -->|OID4VP| F[Fetch authz request] C -->|Invite| G[POST invite response] D & E & F & G --> H{Step complete?}:::accent3 H -->|Next step exists| B H -->|No more steps| I[Exchange Complete]:::accent4 B -->|TTL expires| J[Exchange Expired]:::accent5 ``` Exchanges are stored in MongoDB. By default they expire after **15 minutes** (`ttl: 900`). The maximum TTL is **48 hours** (`ttl: 172800`). Expired exchanges are retained for an additional grace period of 3 days before being eligible for removal. Exchange `variables` hold the mutable state that flows between steps, including per-step results accessible at `variables.results[stepName]`. ## Multi-Step Workflows A workflow can chain multiple steps using the `nextStep` field. This enables patterns such as: 1. **Verify then Issue** — Require the holder to present an existing credential before receiving a new one. 2. **OID4VP then OID4VCI** — Verify via OID4VP and then issue via OID4VCI in the same exchange session. 3. **Invite then Issue** — Collect wallet/app metadata via invite request before issuing credentials. Step results are stored in `exchange.variables.results[stepName]` and are accessible in subsequent JSONata step templates or credential templates. ## Security - Workflow management routes require **zcap** or **OAuth2** authorization. - Exchange execution routes (VC-API `POST`) are unauthenticated by design, relying on capability URLs for authorization; exchanges are single-use, short-lived, and scoped to a specific workflow. Workflows themselves can add authentication by asking for verifiable presentations and verifiable credentials. - CORS is enabled on all endpoints. This is safe because authorization is performed using HTTP signatures and capabilities (not cookies), making CSRF impossible. - Private key material (`privateKeyJwk`) and exchange `secrets` are stripped from all API responses. - VC-API exchange creation and exchange-use payloads are limited to **10 MB** (configured via `config.express.bodyParser.routes`). OID4VP authorization-response form posts are also limited to **10 MB** (via an inline `urlencoded` body parser). Other OID4VCI endpoints (token, credential, etc.) rely on the framework's default body-size limits. A feature exists to enable larger exchange state, but it has not been enabled at this time. ## Backwards Compatibility This module previously exposed a `vc-exchanger` service at `/exchangers`. That service type and route prefix are still registered as aliases and will continue to function, but new deployments should use `vc-workflow` and `/workflows`. ## License [New BSD License](LICENSE.md) © 2022-2026 Digital Bazaar, Inc. [Bedrock]: https://github.com/digitalbazaar/bedrock [JSONata]: https://jsonata.org [VC-API]: https://w3c-ccg.github.io/vc-api/ [OID4VCI-spec]: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html [OID4VP-spec]: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html [`@bedrock/mongodb`]: https://github.com/digitalbazaar/bedrock-mongodb [`@bedrock/service-core`]: https://github.com/digitalbazaar/bedrock-service-core