Skip to content

Repository files navigation

Proof Digital Credentials

drawing

A digital passport. Verified once, usable everywhere.

Read our documentation or try it!

Table of Contents

Packages

Package Runtime Usage Runtime deps
@proof.com/proof-vc-common browser or Node Request a Verifiable Presentation 0 ✅
@proof.com/proof-vc-server Node proof-vc-common plus Presentation Verification, Pushed Authorization Requests, Secured Authorization Requests (JAR), Digital Credentials API, Transaction Templates sd-jwt, owf, jose

Installation

Browser / Node:

npm install @proof.com/proof-vc-common

Node:

npm install @proof.com/proof-vc-server

Proof implements the OpenID for Verifiable Presentations 1.0 specification. Setup an OAuth Application in your Proof account to get your client_id.

Getting Started

Client Side

You can request a Verifiable Presentation in the browser by using createClient and authorizationUrl to craft an Authorization Request URL. Either fragment or direct_post Response Mode are supported (defaults to fragment).

  • if using fragment, at the callback URI, use parseAuthorizationResponse to extract the vp_token from it and send it to your verification endpoint
  • if using direct_post, the vp_token is sent directly to your verification endpoint
import {
  createClient,
  parseAuthorizationResponse,
} from "@proof.com/proof-vc-common";

const proof = createClient({
  environment: "sandbox",
  clientId: "verifier-demo",
  callbackUri: "https://example.com/callback",
});

button.onclick = () => {
  window.location.href = proof.authorizationUrl({
    nonce: "3e8e4918-e9fb-453a-a538-81152be15c1b",
    scope: "urn:proof:params:scope:verifiable-credentials:basic",
  });
};

// at https://example.com/callback
const response = parseAuthorizationResponse(); // reads window.location.hash
if (response?.type === "success") {
  fetch("/verify_vp_token", {
    method: "POST",
    body: new URLSearchParams({ vp_token: response.vpToken }),
  });
} else if (response?.type === "error") {
  console.error(response.error, response.errorDescription);
}

parseAuthorizationResponse returns { type: "success", vpToken, state? }, { type: "error", error, errorDescription?, errorUri?, state? } when the authorization server answered with an OAuth 2.0 error response, or null when neither is present.

You can also use buildAuthorizationUrl to provide all the Authorization Request parameters at once:

import { buildAuthorizationUrl } from "@proof.com/proof-vc-common";

window.location.href = buildAuthorizationUrl({
  environment: "sandbox",
  clientId: "verifier-demo",
  callbackUri: "https://example.com/callback",
  nonce: "3e8e4918-e9fb-453a-a538-81152be15c1b",
  scope: "urn:proof:params:scope:verifiable-credentials:basic",
});

Server Side

You can request a Verifiable Presentation from our backend by using createClient and authorizationUrl to craft an Authorization Request URL. Either fragment or direct_post Response Mode are supported (defaults to fragment).

With @proof.com/proof-vc-server you can also use Pushed Authorization Requests, Secured Authorization Requests and Transaction Templates.

import { createClient } from "@proof.com/proof-vc-server";

const proof = createClient({
  environment: "sandbox",
  clientId: "verifier-demo",
  callbackUri: "https://example.com/verify_vp_token",
  responseMode: "direct_post",
});

const redirect = await proof.authorizationUrl({
  nonce: "3e8e4918-e9fb-453a-a538-81152be15c1b",
  scope: "urn:proof:params:scope:verifiable-credentials:basic",
  state: "6A2B4CD830",
});
res.redirect(redirect);

state and loginHint are optional and forwarded as the state and login_hint request parameters.

Response Modes

Proof supports fragment and direct_post response modes (default fragment).

fragment

Using fragment the vp_token is returned as a fragment of the callbackUri when the user is 302 redirected from Proof to your website. Use parseAuthorizationResponse() in the browser to read it.

GET https://example.com/verify_vp_token#vp_token=eyJwcm9vZl9pZF9...

direct_post

Using direct_post the vp_token and state (optional) are returned in the application/x-www-form-urlencoded body of a POST request to the callbackUri from Proof to your server. Pass the vp_token value as received to verifyVPToken. See the OID4VP specification for more details.

POST https://example.com/verify_vp_token
Content-Type: application/x-www-form-urlencoded

vp_token=eyJwcm9vZl9pZF9...&state=6A2B4CD830

Pushed Authorization Requests

Proof supports Pushed Authorization Requests (PAR). You may want to use this feature when using Transaction Templates to avoid hitting URL size limits. PAR requires a clientSecret and is therefore available only from @proof.com/proof-vc-server.

import { createClient } from "@proof.com/proof-vc-server";

const proof = createClient({
  environment: "sandbox",
  clientId: "caxdw5a7d",
  clientSecret: process.env.PROOF_CLIENT_SECRET,
  callbackUri: "https://example.com/verify_vp_token",
  responseMode: "direct_post",
  usePushedAuthorizationRequest: true,
});

const redirect = await proof.authorizationUrl({
  nonce: "3e8e4918-e9fb-453a-a538-81152be15c1b",
  scope: "urn:proof:params:scope:verifiable-credentials:basic",
});

The PAR request times out after timeout milliseconds (client config, default 10 000). Pass an AbortSignal as the second argument, proof.authorizationUrl(params, { signal }), to cancel it earlier.

Secured Authorization Requests

Proof supports JWT-Secured Authorization Requests (JAR): the Authorization Request parameters are sent as a single signed JWT ("request object") instead of plain query parameters. JAR is available only from @proof.com/proof-vc-server and requires:

  • useSecuredAuthorizationRequest: true
  • a privateKeyFactory returning your ES256 (P-256) private key as a JWK, CryptoKey or KeyObject

By value

authorizationUrl signs the request object and embeds it in the request parameter. It can be combined with Pushed Authorization Requests.

import { createClient } from "@proof.com/proof-vc-server";

const proof = createClient({
  environment: "sandbox",
  clientId: "caxdw5a7d",
  callbackUri: "https://example.com/verify_vp_token",
  responseMode: "direct_post",
  useSecuredAuthorizationRequest: true,
  privateKeyFactory: () => myPrivateKeyJwk,
});

const redirect = await proof.authorizationUrl({
  nonce: "3e8e4918-e9fb-453a-a538-81152be15c1b",
  scope: "urn:proof:params:scope:verifiable-credentials:basic",
});

By reference

If you'd rather host the request object yourself, use signedAuthorizationRequest to obtain the signed JWT, store it on your backend (serve it with Content-Type: application/oauth-authz-req+jwt), then hand Proof a request_uri pointing at it with jarByReferenceAuthorizationUrl. JAR by reference cannot be combined with Pushed Authorization Requests.

import { createClient } from "@proof.com/proof-vc-server";

const proof = createClient({
  environment: "sandbox",
  clientId: "caxdw5a7d",
  callbackUri: "https://example.com/verify_vp_token",
  responseMode: "direct_post",
  useSecuredAuthorizationRequest: true,
  privateKeyFactory: () => myPrivateKeyJwk,
});

// a signed JWT string; store it and serve it at the request_uri below
const jar = await proof.signedAuthorizationRequest({
  nonce: "3e8e4918-e9fb-453a-a538-81152be15c1b",
  scope: "urn:proof:params:scope:verifiable-credentials:basic",
});
await store.put("jar/42", jar);

const redirect = proof.jarByReferenceAuthorizationUrl({
  requestUri: "https://example.com/jar/42",
});

Digital Credentials API

For the W3C Digital Credentials API (dc_api response mode), signedDcApiRequest returns a signed request object you pass to navigator.credentials.get.

expectedOrigins is required and lists the origins the request may be made from: your own site and any intermediate party it transits through (e.g. an AI-agent VM). callbackUri is not used by this flow.

import { createClient, DCQL_QUERY_BASIC } from "@proof.com/proof-vc-server";

const proof = createClient({
  environment: "sandbox",
  clientId: "caxdw5a7d",
  useSecuredAuthorizationRequest: true,
  privateKeyFactory: () => myPrivateKeyJwk,
});

const request = await proof.signedDcApiRequest({
  nonce: "3e8e4918-e9fb-453a-a538-81152be15c1b",
  dcqlQuery: DCQL_QUERY_BASIC,
  expectedOrigins: ["https://example.com", "https://ai-agent.com"],
});

Client ID Metadata Document

createClientIdMetadataDocument builds the Client ID Metadata Document to serve at your clientId URL.

import { createClientIdMetadataDocument } from "@proof.com/proof-vc-server";

const document = await createClientIdMetadataDocument({
  environment: "sandbox",
  clientId: "https://example.com/x401-client", // the URL serving this document
  clientName: "Example",
  redirectUris: ["https://proof.com/agents-trust-list"],
  jwks: [publicJwk],
});

Verifiable Credential Presentation

Credential Type

Proof issues Verifiable Credentials according to the SD-JWT-VC specification and publishes its OID4VCI Credential Issuer Metadata at https://api.proof.com/.well-known/openid-credential-issuer.

ProofCredentialV1

claim accessor type description
given_name givenName string user's given name as it appears on the verified identity document
family_name familyName string user's family name as it appears on the verified identity document
birth_date birthDate string user's date of birth in ISO 8601 format (YYYY-MM-DD)
age_equal_or_over.18 isOver18 boolean boolean confirming the user is 18 or older
age_equal_or_over.21 isOver21 boolean boolean confirming the user is 21 or older
age_equal_or_over.65 isOver65 boolean boolean confirming the user is 65 or older
is_national.us isNationalUS boolean boolean confirming US nationality (nationality:us scope)

All attributes are selectively disclosable and will return undefined if the claim wasn't disclosed. getClaims() returns the disclosed claims as issued and toJSON() the accessors above. A credential with a vct unknown to this SDK is returned as a DefaultProofCredential with a one-time ProofVCWarning.

Request

Request a Verifiable Credential Presentation with an OAuth 2.0 Authorization Request. See Client Side and Server Side above for the two entry points. Exactly one of scope or dcqlQuery must be given.

Scopes

Proof supports the scope parameter of the OID4VP specification. Each scope maps to a pre-defined DCQL query and returns a specific Credential Type.

Supported scope and their associated Credential Type:

scope Credential Type claims Key Binding JWT
urn:proof:params:scope:verifiable-credentials:basic ProofCredentialV1 given_name, family_name, age_equal_or_over.18 yes
urn:proof:params:scope:verifiable-credentials:nationality:us ProofCredentialV1 age_equal_or_over.18, is_national.us yes

A scope unknown to this SDK is accepted and emits a one-time ProofVCWarning.

Transaction Templates

Transaction Templates allow you to bind specific data to a Verifiable Credential Presentation. Proof uses the Transaction Data parameter of the OID4VP specification. The data is shown to the user during the Presentation flow and the user signs it with a Key Binding JWT (KB-JWT). The KB-JWT is returned as part of the Presentation.

Transaction data is sensitive and is therefore attached only from @proof.com/proof-vc-server. The following Transaction Templates are available:

urn:proof:params:vc:transaction-data:wire-instructions:v1

import { createClient, transactionData } from "@proof.com/proof-vc-server";

const proof = createClient({
  environment: "sandbox",
  clientId: "verifier-demo",
  callbackUri: "https://example.com/verify_vp_token",
});

const data = transactionData.wireInstructions({
  recipient: {
    institution_name: "Crestline Financial",
    individual_name: "Acme Corp LLC",
    routing_number: "055000123",
    account_number: "7293",
  },
  source: {
    institution_name: "Sterling & Union",
    individual_name: "Sterling & Union",
    account_number: "4821",
    routing_number: "091000456",
  },
  amount: 5000,
  currency: "USD",
  memo: "Invoice #2024-089",
});
const redirect = await proof.authorizationUrl({
  nonce: "3e8e4918-e9fb-453a-a538-81152be15c1b",
  scope: "urn:proof:params:scope:verifiable-credentials:basic",
  state: "6A2B4CD830",
  transactionData: data,
});

urn:proof:params:vc:transaction-data:payment-itemized:v1

import { createClient, transactionData } from "@proof.com/proof-vc-server";

const proof = createClient({
  environment: "sandbox",
  clientId: "verifier-demo",
  callbackUri: "https://example.com/verify_vp_token",
});

const data = transactionData.paymentItemized({
  title: "Drive Shaft",
  description: "The Roadhouse (18+), May 6 2026",
  currency: "USD",
  items: [
    { quantity: 2, unit_cost: 40.0, label: "General Admission" },
    { quantity: 2, unit_cost: 11.4, label: "Fees" },
  ],
});
const redirect = await proof.authorizationUrl({
  nonce: "3e8e4918-e9fb-453a-a538-81152be15c1b",
  scope: "urn:proof:params:scope:verifiable-credentials:basic",
  state: "6A2B4CD830",
  transactionData: data,
});

urn:proof:params:vc:transaction-data:payment-mandate:v1

import { createClient, transactionData } from "@proof.com/proof-vc-server";

const proof = createClient({
  environment: "sandbox",
  clientId: "verifier-demo",
  callbackUri: "https://example.com/verify_vp_token",
});

const data = transactionData.paymentMandate({
  payment_instrument: {
    type: "wallet",
    id: "did:example:visa-token-7829",
    description: "Visa ••••7829",
  },
  payee: {
    id: "did:example:summitco",
    name: "Summit Co",
    website: "summitco.com",
  },
  prompt_summary:
    "Find me a 4-season backpacking tent from Summit Co under $500",
  amount: 500,
  currency: "USD",
});
const redirect = await proof.authorizationUrl({
  nonce: "3e8e4918-e9fb-453a-a538-81152be15c1b",
  scope: "urn:proof:params:scope:verifiable-credentials:basic",
  state: "6A2B4CD830",
  transactionData: data,
});

urn:proof:params:vc:transaction-data:session-data

import { createClient, transactionData } from "@proof.com/proof-vc-server";

const proof = createClient({
  environment: "sandbox",
  clientId: "verifier-demo",
  callbackUri: "https://example.com/verify_vp_token",
});

const data = transactionData.sessionData({
  ip_address: "203.0.113.42",
  device_id: "8f3c2a5e-4b1d-4c7e-9a0f-6d2b1e8c7f31",
});
const redirect = await proof.authorizationUrl({
  nonce: "3e8e4918-e9fb-453a-a538-81152be15c1b",
  scope: "urn:proof:params:scope:verifiable-credentials:basic",
  transactionData: data,
});

Verify

Verification runs server-side. Create a verifier for the same environment you used in the request and reuse it: production verifies against the Proof Root CA R1, sandbox against the Development root (see Certificate Authority). A credential issued by another environment is rejected on its iss. Pass your client_id as aud to check the Key Binding JWT audience when one is present.

Decode and verify a Verifiable Presentation's vp_token:

import { createVerifier, ProofCredentialV1 } from "@proof.com/proof-vc-server";

const verifier = createVerifier({ environment: "sandbox" });

const vpToken = "eyJwcm9vZl9pZ..."; // the vp_token value as received
const presentation = await verifier.verifyVPToken({
  encodedVPToken: vpToken,
  aud: "verifier-demo", // your client_id
});
const [verifiableCredential] = presentation.proof_id_default;

if (
  verifiableCredential instanceof ProofCredentialV1 &&
  verifiableCredential.isOver18
) {
  purchaseItem();
}

verifyVPToken returns the verified credentials keyed by DCQL credential id (proof_id_default for the basic scope).

Verify a single SD-JWT-VC:

import { createVerifier, ProofCredentialV1 } from "@proof.com/proof-vc-server";

const verifier = createVerifier({ environment: "sandbox" });

const encodedSDJWT = "eyJraWQiOiI3...";
const verifiableCredential = await verifier.verify({
  encodedSDJWT,
  aud: "verifier-demo",
});

if (
  verifiableCredential instanceof ProofCredentialV1 &&
  verifiableCredential.isOver18
) {
  purchaseItem();
}

Nonce

Validating the nonce is out of scope of verify and verifyVPToken. The nonce signed in the Key Binding JWT is exposed on the returned credential and should be validated by the caller against the nonce sent in the Request. Some credentials are presented without a Key Binding JWT; getNonce() then returns undefined and there is no nonce to compare:

const presentation = await verifier.verifyVPToken({
  encodedVPToken: vpToken,
  aud: "verifier-demo",
});

for (const credential of presentation.proof_id_default) {
  const nonce = credential.getNonce();
  if (nonce !== undefined && nonce !== session.nonce) {
    throw new Error("nonce mismatch");
  }
}

Certificate Authority

Proof's Verifiable Credentials are issued by our Certificate Authority following the CA/B Forum Baseline Requirements for the Issuance and Management of Publicly-Trusted TLS Server Certificates published at https://www.cabforum.org.

The Proof Root CA R1 Certificate is published at http://cert.proof.com/proof-root-ca-r1.crt and is also committed in this repository proof_root_ca_r1.ts.

The sandbox Root CA R1 Development certificate is also committed in this repository proof_root_ca_r1_development.ts and used when environment: "sandbox".

Documentation

Digital Credentials guides https://dev.proof.com/docs/digital-credentials-overview
API Documentation https://dev.proof.com/reference/authorizeverifiablecredentialpresentation

Contributing

Contribution guidelines for this project

Releases

Packages

Used by

Contributors

Languages