@solidus-network/x401
v0.1.0
Published
Verifier side of x401, the HTTP Proof Requirement Protocol. Protocol only: credential validation is injected, so one verifier serves both the gated route and the token endpoint. Mirrored publicly as @solidus-network/x401.
Maintainers
Readme
@solidus-network/x401
Verifier side of x401, the HTTP Proof Requirement Protocol.
x402 answers "what does this agent owe". x401 answers "what does this agent have to prove". A server
answers a request with a PROOF-REQUEST header describing the credential it needs, the agent
satisfies it through a credential manager, and retries the same route with PROOF-RESPONSE.
Running against a live deployment: https://auth.solidus.network/v1/x401/demo. Open it in a browser and the requirement is rendered. Point an agent at it and the same requirement is in the header.
This package is protocol only. It never decides who to trust. Credential validation is injected, so the same verifier that guards your routes is the one your token endpoint uses, and the two cannot drift apart.
Status
x401 is an editor's draft at v0.2.0 and can change. This is an independent implementation and is not affiliated with its authors.
Implemented, against the specification's Verifier conformance list:
| item | what |
|---|---|
| 1, 12 | PROOF-REQUEST emission, and HTTP status kept independent of proof state |
| 2 | openid4vp-v1-signed and openid4vp-v1-unsigned |
| 3 | OAuth token endpoint advertised in the oauth member |
| 5 | Result Artifact validation, delegated to your OpenID4VP verifier |
| 6, 7 | Result Artifacts in PROOF-RESPONSE, inline and by reference |
| 8, 9 | Verification Tokens, and caller binding when Authorization is also present |
| 10 | PROOF-RESULT with an x401 Error Object |
| Consumer Client Compatibility | the body-embedded <data> mirror for header-blind clients |
| Agent side | satisfyProofRequest, the Digital Credentials API carrier |
Not implemented:
- Item 4,
trusted_authorities. See "A note on issuer trust" below. - The
dc_apianddc_api.jwtresponse modes, and JWE response encryption. The agent-side carrier below is complete and the composed request is a validDigitalCredentialRequestOptions, but a Verifier that wants the credential manager to return the result through the browser rather than post it back still needs those response modes. Until then the request this package composes carries whatever response mode your OpenID4VP verifier issues. openid4vp-v1-multisigned. x401 v0.2.0'sprotocolmember does not permit it, although the W3C Digital Credentials API requires user agents to support it. Raised as proof/x401#41.
Install
npm install @solidus-network/x401fastify is an optional peer dependency, needed only for the route gate and the token endpoint.
Gate a route
import Fastify from 'fastify'
import { x401Gate, verifyCredentialResult } from '@solidus-network/x401'
const app = Fastify()
app.get(
'/protected',
{
preHandler: x401Gate({
enabled: true,
tokenEndpoint: 'https://example.com/x401/token',
challengeStatus: 401,
buildRequest: async () => createOpenId4vpRequest(),
verifier: {
resultUriPrefix: 'https://example.com/.well-known/x401/results/',
fetchCredentialResult: async (uri) => fetchWithSsrfGuard(uri),
},
verifyProof: (result) =>
verifyCredentialResult(result, {
submitResponse: (input) => yourOpenId4vpVerifier.submitResponse(input),
}),
}),
},
async () => ({ secret: 'only after proof' }),
)buildRequest must mint a fresh nonce per challenge. Reusing one silently removes replay
detection, and nothing else in the protocol will notice.
Satisfy a requirement (agent side)
import { satisfyProofRequest } from '@solidus-network/x401'
const challenge = await fetch(url)
const proofRequest = challenge.headers.get('proof-request')
const proofResponse = await satisfyProofRequest(proofRequest, {
get: (credentialRequirements) => navigator.credentials.get(credentialRequirements),
agentId: 'did:web:agent.example',
})
const retry = await fetch(url, { headers: { 'PROOF-RESPONSE': proofResponse } })credential_requirements is passed to the credential manager verbatim. The specification
requires it, and the result binds to the request the Verifier issued, so an agent that "helpfully"
adjusts the request breaks the binding it depends on.
Two things worth knowing before you deploy this
The by-reference Result Artifact is a server-side request forgery surface. credential_result_uri
is supplied by whoever retries the route. This package checks the URI against resultUriPrefix
before any network call, so a URI you did not issue never becomes a request. The
fetchCredentialResult you supply should still apply address-level protection, because that check
only proves the URI looks like yours.
A failed verification is not an absent one. verifyProof is required rather than optional,
because a gate that forgets to verify is worse than no gate: it looks like one. If it throws, the
route does not run and the response carries an x401 error object.
A note on issuer trust
The specification says accepted issuers belong in the request's DCQL trusted_authorities. If your
issuers are identified by DIDs, you cannot do this today. All three DCQL authority types assume X.509
or a federation: aki is an RFC 5280 AuthorityKeyIdentifier, etsi_tl requires an X.509 certificate
in the credential's trust chain, and openid_federation requires a federation Entity Identifier.
This package therefore omits the member rather than synthesising a value. Omitting it is conformant, because expressing it is a SHOULD. Asserting an authority key that does not exist would not be, and a wallet doing real matching would fail or match wrongly.
Tracked upstream at openid/OpenID4VP#494.
Test vectors
The tests use the specification's own examples as fixtures rather than this implementation's output: Appendix A's minimal payload, both Result Artifact forms, the x401 Token Object, and the Error Object. A green test therefore means the code accepts what the specification publishes.
npm install
npm run build
npm testLicense
Apache-2.0
