x401

Protect your endpoints from agentic traffic with Digital Credentials.

x401 protects your endpoints from agentic traffic through Digital Credentials. When a user requests a protected resource, the endpoint responds with an x401 PROOF-REQUEST header: a Digital Credentials Presentation request.

The user agent delegates the request to the user's Wallet. The user authenticates to their Wallet and approves the credential presentation. The user agent retries the protected resource with a PROOF-RESPONSE header: a Digital Credentials Presentation response.

After cryptographic verification of the presented Verifiable Credential, the user gets access to the protected resource.

x401 is built on top of Digital Credentials. Proof's Cloud Wallet supports x401 through OpenID for Verifiable Presentations (OID4VP).

Proof Cloud Wallet is also available as a Web Component for your website!

User agentYour endpointProof Cloud WalletGET /protected1401 + PROOF-REQUEST2Presentation request3User authenticates and approves4vp_token5GET /protected + PROOF-RESPONSE6Verify vp_token7200 + protected resource8

Getting started

Ask your agent to protect your endpoints behind x401 using Proof's Cloud Wallet:

Protect my API endpoints behind the x401 protocol, using Proof's Cloud Wallet, following the instructions at https://api.proof.com/.well-known/agent-skills/setup-x401-verifier/SKILL.md

Or follow the step-by-step guide.

1. Install dependencies

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

2. Generate a key pair

The key pair signs your OID4VP requests and your enrollment. It uses P-256.

openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem

3. Host an OAuth Client ID Metadata Document

Create an OAuth Client ID Metadata Document (CIMD) and host it at an endpoint you own, publicly accessible over HTTPS. The CLIENT_ID is the exact URL of the document.

import express from "express";
import { readFileSync } from "node:fs";
import { createPrivateKey, createPublicKey } from "node:crypto";
import { agentsTrustListUrl, createClientIdMetadataDocument } from "@proof.com/proof-vc-server";

// The exact URL at which the Client ID Metadata Document is hosted.
const CLIENT_ID = "https://example.com/.well-known/proof-client.json";
const privateKey = createPrivateKey(readFileSync("private-key.pem"));
const publicJwk = createPublicKey(privateKey).export({ format: "jwk" });
// "sandbox" for testing, "production" for production.
const environment = "sandbox";

const app = express();

const document = await createClientIdMetadataDocument({
  environment,
  clientId: CLIENT_ID,
  // Your business name, shown to users when they approve
  // a credential presentation for your endpoints.
  clientName: "Acme Corporation",
  // The agents allowed to use x401 with Proof's Cloud Wallet at your endpoints:
  // your own agents (by URL) or Proof's trusted list with `agentsTrustListUrl`.
  redirectUris: [agentsTrustListUrl(environment)],
  jwks: [publicJwk],
});

app.get("/.well-known/proof-client.json", (req, res) => {
  res.json(document);
});
📘

Agents trust list

agentsTrustListUrl defines which trusted agents use x401 and Proof's Cloud Wallet at your endpoints. Proof publishes its list at https://api.proof.com/.well-known/agents-trust-list. See the OID4VP over the Digital Credentials API expected_origins parameter for details.

4. Register your OAuth Client ID Metadata Document

Enroll on Proof as an x401 Verifier by registering your CIMD, then follow the instructions received by email to complete your enrollment.

npx @proof.com/proof-vc-server enroll https://example.com/.well-known/proof-client.json --email [email protected] --key private-key.pem --environment sandbox
📘

Email address

The email address should be on the host of your CIMD, with www. counting as the apex domain: [email protected] for https://example.com/... and https://www.example.com/....

5. Protect your endpoint

Create an x401Middleware that inspects the request's PROOF-RESPONSE header and cryptographically verifies the presented vp_token containing the Verifiable Credential.

import { readFileSync } from "node:fs";
import { createPrivateKey, randomUUID } from "node:crypto";
import { verifier as x401, HEADER, DC_API_PROTOCOL } from "@proof.com/x401-node";
import { agentsTrustListUrl, createClient, createVerifier } from "@proof.com/proof-vc-server";

const CLIENT_ID = "https://example.com/.well-known/proof-client.json";
const privateKey = createPrivateKey(readFileSync("private-key.pem"));
const environment = "sandbox";

const proofClient = createClient({
  environment,
  clientId: CLIENT_ID,
  // Signs your OID4VP requests.
  useSecuredAuthorizationRequest: true,
  privateKeyFactory: () => privateKey,
});
const proofVerifier = createVerifier({ environment });

async function x401Middleware(req, res, next) {
  const response = req.get(HEADER.PROOF_RESPONSE);

  if (response === undefined) {
    // A signed OID4VP request over the Digital Credentials API.
    const request = await proofClient.signedDcApiRequest({
      // See https://dev.proof.com/docs/verify-a-credential#scopes-and-claims
      scope: "urn:proof:params:scope:verifiable-credentials:basic",
      // Prevents replay attacks.
      nonce: randomUUID(),
      // A subset of the redirectUris of your CIMD.
      expectedOrigins: [agentsTrustListUrl(environment)],
    });
    // The PROOF-REQUEST header, following the x401 protocol.
    const payload = x401.buildPayload({
      credentialRequirements: {
        digital: {
          requests: [{ protocol: DC_API_PROTOCOL.SIGNED, data: { request } }],
        },
      },
    });
    return res
      .status(401)
      .set(HEADER.PROOF_REQUEST, x401.encodePayload(payload))
      .send("x401 PROOF-RESPONSE required");
  }

  try {
    const artifact = x401.decodeResultArtifact(response);
    // Verifies the vp_token, issued by Proof's Certificate Authority.
    // See https://dev.proof.com/docs/proof-certificate-authority
    const presentation = await proofVerifier.verifyVPToken({
      encodedVPToken: artifact.credential_result.data.vp_token,
      aud: CLIENT_ID,
    });
    req.credential = presentation.proof_id_default[0];
  } catch {
    return res.status(401).send("x401 PROOF-RESPONSE invalid");
  }
  next();
}

app.get("/protected", x401Middleware, (req, res) => {
  // Reachable by user agents presenting a valid Verifiable Credential.
  res.json({ message: "Access granted", claims: req.credential.toJSON() });
});
🚧

Prevent replay attacks

Compare req.credential.getNonce() with the nonce of your OID4VP request.

📘

Claims and scopes

Learn about the claims supported by Proof's Verifiable Credentials and how to request them with scopes.

6. Test your endpoint

The endpoint returns HTTP 401 with a proof-request header. Inspect the Digital Credentials Presentation request, then the OID4VP request it carries:

curl -so /dev/null -w '%header{proof-request}' https://example.com/protected \
  | jq -R 'gsub("-";"+") | gsub("_";"/") | @base64d | fromjson'
curl -so /dev/null -w '%header{proof-request}' https://example.com/protected \
  | jq -R 'gsub("-";"+") | gsub("_";"/") | @base64d | fromjson
    | .credential_requirements.digital.requests[0].data.request
    | split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson'

Then ask your agent to fetch the protected resource with Proof's Cloud Wallet:

Please fetch https://example.com/protected and use my Proof Cloud Wallet following instructions from https://api.proof.com/.well-known/agent-skills/access-x401-resource/SKILL.md

With a valid PROOF-RESPONSE, the endpoint returns 200 and the verified claims.

Go to production

Set environment to "production" and enroll:

npx @proof.com/proof-vc-server enroll https://example.com/.well-known/proof-client.json --email [email protected] --key private-key.pem

Resources


Did this page help you?