@bsv/auth-express-middleware
v2.2.8
Published
BSV Blockchain mutual-authentication express middleware
Readme
@bsv/auth-express-middleware
Express middleware for BRC-103 peer-to-peer mutual authentication over the BRC-104 HTTP transport. It handles the public handshake endpoint, verifies authenticated application requests, signs responses, and optionally exchanges verifiable certificates.
The current release preserves BRC-100 byte fields across supported JSON and
byte-array forms, snapshots handshake messages before asynchronous work, and
rejects parsed bodies that cannot be represented without losing semantics.
Version 2.2.6 also preserves bodyless authenticated requests on Express 4, whose
JSON parser can supply an empty-object placeholder where Express 5 supplies
undefined. HTTP message framing distinguishes that placeholder from a real
JSON {} body, which remains signed data. Existing clients need no changes. Version 2.2.7 also
preserves Express's one-argument res.set({ ...headers }) overload, including
payment challenge headers. The wrapper forwards the original argument count;
Express retains its own header validation and coercion, and signed responses
retain their existing BRC-104 representation.
Version 2.2.8 requires SDK 2.8.5 and removes middleware header-size and
header-count ceilings. createAuthMiddleware configures its Peer with
maxGeneralPayloadBytes: null, avoiding an indirect SDK general-message
ceiling for received headers while retaining metadata and signature validation.
Received headers are excluded from maxRequestBytes, which still applies to
handshake/plain-data and encoded request bodies. The middleware preserves all
selected header bytes for BRC-104 signing and verification, rather than rejecting
a received payment because of an additional header budget. HTTP server, CDN,
proxy and WAF configuration own transport header limits. Configure those layers
for the largest supported payment proof and validate the complete route.
Malformed or duplicate signed headers, unsafe header values, authentication failures and invalid signatures still fail validation. Body budgets, timeouts and replay protection remain separate. Clients consuming larger signed responses need matching capacity; SDK 2.8.5 raises its header limits by 4x. No BRC100 call, wire or wallet-data migration is required. Source publication is a separate protected release step.
Requirements
- Node.js 22 or newer
- Express 4.18 or newer, including Express 5
- A BRC-100
WalletInterface
The package ships native ESM and CommonJS entry points with declarations for both module systems.
Install
npm install @bsv/auth-express-middleware @bsv/sdk expressExpress is a peer dependency, so the middleware uses the application's single
Express runtime and type graph. This prevents duplicate Express installations
from making AuthRequest or the returned middleware incompatible with the
application's route types.
Basic use
Parse the request body before authentication so the signed payload contains the same value your route receives:
import express from 'express'
import { PrivateKey, ProtoWallet } from '@bsv/sdk'
import { createAuthMiddleware, type AuthRequest } from '@bsv/auth-express-middleware'
const wallet = new ProtoWallet(PrivateKey.fromRandom())
const app = express()
app.use(express.json())
app.use(createAuthMiddleware({ wallet }))
app.get('/private', (req: AuthRequest, res) => {
res.json({ identityKey: req.auth?.identityKey })
})Authentication is required by default. Requests without BRC-103/104
authentication receive 401. With allowUnauthenticated: true, they continue
with req.auth.identityKey === 'unknown'.
The exact /.well-known/auth path is always reachable through this middleware
because it establishes the session used by protected routes. Similar prefixes
such as /.well-known/auth/extra are not treated as handshake traffic.
Options
const auth = createAuthMiddleware({
wallet,
allowUnauthenticated: false,
sessionManager,
certificatesToRequest,
onCertificatesReceived,
certificateApprovalStore,
logger,
logLevel: 'error',
transportLimits: {
requestTimeoutMs: 30_000,
maxPendingRequests: 1_000,
maxRequestBytes: 8 * 1024 * 1024,
maxResponseBytes: 8 * 1024 * 1024
}
})walletis required and must implement the BRC-100 wallet interface.allowUnauthenticateddefaults tofalse.sessionManageraccepts the SDK'sSessionManageror anAsyncSessionManager.certificatesToRequestasks a peer for allowed certificate types and fields. The legacy v0.1 shape does not assert that every listed type or field was supplied; inspect the validated certificates inonCertificatesReceivedbefore approving.onCertificatesReceivedmay be synchronous or asynchronous. It receives(senderPublicKey, certificates, req, res, approve). When this callback is configured, the protected request remains blocked unless the callback explicitly callsapprove(). Returning normally without approval is a denial and eventually produces the configured authentication timeout. Callingapprovemore than once has no effect. The callback must validate every application-specific certificate policy, including required type and field completeness, before approving.certificateApprovalStorerecords that approval against the exact BRC-103 session nonce and identity. The defaultInMemoryCertificateApprovalStoreis bounded to 10,000 approvals and is appropriate only when the handshake and protected request remain in one process. A replicated service usingonCertificatesReceivedmust inject a shared store and retain approvals for at least the corresponding session lifetime. Store failures and every verdict other than exact booleantruefail closed.loggerandlogLevelenable structured lifecycle logs. Authentication headers, certificate bodies, signatures, response bodies, and wallet objects are not logged.transportLimits.requestTimeoutMsbounds handshake, verification, certificate, and response-signing state. It defaults to 30 seconds.transportLimits.maxPendingRequestsbounds per-process pending protocol state. It defaults to 1,000 and fails closed with503at capacity.transportLimits.maxRequestBytesbounds handshake plain-data and encoded request-body work before peer processing, excluding received HTTP headers. It defaults to 8 MiB. Set it to-1only when the embedding service enforces an equivalent request budget.transportLimits.maxResponseBytesbounds application responses buffered for BRC-104 signing, including files passed tores.sendFile. It defaults to 8 MiB and fails closed with a signed413response. Set it to-1only when the embedding service enforces an equivalent response budget.
Invalid option types fail during startup.
Horizontally scaled services
The default SessionManager is process-local. Use a shared
AsyncSessionManager when a load balancer can route the handshake and the
authenticated request to different instances:
import type { AsyncSessionManager } from '@bsv/sdk'
const sessionManager: AsyncSessionManager = {
async addSession(session) {
await sessions.put(session.sessionNonce, session)
},
async updateSession(session) {
await sessions.put(session.sessionNonce, session)
},
async getSession(identifier) {
return await sessions.get(identifier)
},
async removeSession(session) {
await sessions.delete(session.sessionNonce)
},
async hasSession(identifier) {
return (await sessions.get(identifier)) !== undefined
}
}
app.use(createAuthMiddleware({ wallet, sessionManager }))The backing store must preserve the SDK's session semantics and should use
appropriate atomicity, expiry, availability, and encryption controls. If
onCertificatesReceived is configured, the same replicas must also share
certificateApprovalStore; approval is keyed by exact session nonce and
identity, not merely by identity. Sticky routing is not a substitute for
shared state when instances can be replaced.
Certificates
app.use(
createAuthMiddleware({
wallet,
certificatesToRequest: {
certifiers: ['<compressed-certifier-public-key>'],
types: {
'<base64-certificate-type>': ['firstName']
}
},
async onCertificatesReceived(senderPublicKey, certificates, req, res, approve) {
await authorizeDisclosedFields(senderPublicKey, certificates)
approve()
}
})
)The application remains responsible for authorization policy, certificate revocation checks, and safe storage of disclosed data. A missing required certificate fails with a stable public error. Internal wallet, signing, and certificate-handler errors are logged only through the optional logger and are not returned to callers.
Authenticated res.sendFile() responses preserve Express's root,
dotfiles, start, end, and headers security-relevant options while the
file is buffered for signing. Relative paths require root; traversal and
symlink escapes outside that root fail closed. Other send-file cache and range
negotiation options remain the application's responsibility.
The response wrapper also buffers write(), writeHead(), flushHeaders(),
and data passed to end() so those standard Node/Express paths cannot escape
the signed response. Direct status and header state is captured before signing.
Streaming remains bounded buffering: this protocol must know the complete body
before it can sign it.
Public services, CORS, and CSP
This package does not impose CORS, CSP, or an origin allowlist. That is intentional: auth endpoints may serve browser apps, WUI, mobile clients, and other callers across many domains. Configure those policies at the application or edge layer:
- Keep public-service access available by default when that is the service contract.
- Offer an operator-configured origin allowlist as an opt-in restriction.
- Never combine
Access-Control-Allow-Origin: *with credentialed CORS. - Expose the required
x-bsv-auth-*response headers to browser clients. - Handle
OPTIONSbefore authentication when browser preflight is supported. - Treat CSP as a browser-document policy; API responses generally need CORS and transport controls instead.
Do not hard-code a deployment-specific domain list in this middleware.
Error behavior
Public errors are deliberately stable and do not include internal exception messages:
| Status | Code | Meaning |
| ------ | ---------------------------------- | ----------------------------------------------- |
| 400 | ERR_AUTH_MALFORMED | Invalid handshake or auth headers |
| 400 | ERR_CERTIFICATES_REQUIRED | Required certificates were not supplied |
| 401 | UNAUTHORIZED / ERR_AUTH_FAILED | Authentication was absent or failed |
| 408 | ERR_AUTH_TIMEOUT | A bounded protocol step timed out |
| 500 | ERR_INTERNAL_SERVER_ERROR | Internal auth processing failed |
| 500 | ERR_RESPONSE_SIGNING_FAILED | The authenticated response could not be signed |
| 503 | ERR_AUTH_CAPACITY | Pending-auth state reached its configured limit |
Security notes
- Use HTTPS. Mutual authentication provides integrity and identity, not confidentiality for all HTTP metadata and content.
- Install the middleware once per request path; response methods are temporarily wrapped while an authenticated response is signed.
- Do not trust
req.auth.identityKey === 'unknown'as authorization. - Use shared session state for multi-instance deployments.
- Keep timeouts, response sizes, and capacity limits finite and monitor
408/413/503rates. - Validate authorization separately after identity authentication.
- BRC-104 v0.1 signs the method, pathname, query, selected headers, and body; it
does not sign the scheme/authority (
Host),Cookie, forwarding headers, or arbitrary standard headers. This is deliberate because browser and webpage libraries often cannot safely observe those values when signing. Never select a tenant or grant authority from those omitted values. Pin the expected authority at the edge and compare any security-relevant value with an exact signedx-bsv-*orAuthorizationfield. Use distinct server identity keys when virtual authorities are separate security principals. A valid signature authenticates only this documented subset, not the complete browser or proxy context. - Signed response headers are likewise limited to
x-bsv-*(excluding the auth envelope) andAuthorization. Do not carry authenticated decisions in unsignedLocation, cookie, content-type, or other response metadata. - Parse JSON, URL-encoded, text, and binary bodies with their matching Express parser before auth. URL-encoded parsed objects must contain only exact string fields; nested/array coercions and unsupported nonempty bodies fail closed.
- Keep request body limits and normal Express hardening in place.
- Late authentication failures after a response or connection has already settled are contained to that request and are never written as a second Express response.
API
Runtime exports:
createAuthMiddlewareExpressTransport
Type exports:
AuthMiddlewareOptionsAuthRequestAuthTransportLimitsCertificateApprovalStore
Runtime approval-store export:
InMemoryCertificateApprovalStoreLogLevel
See API.md for generated signatures.
Development
pnpm typecheck
pnpm lint
pnpm format:check
pnpm test:coverage
pnpm pack:checkpack:check builds and validates the exact npm tarball in ESM and CommonJS
consumer probes, plus real authenticated requests on Express 4 and 5 with
legacy and current SDK clients. Tests do not rebuild the package as a side effect.
Specifications
License
Current TS Stack changes are licensed under the Open BSV License Version 6; see
LICENSE.txt. This package also retains pre-uniformization code
under the Open BSV License Version 4. Redistributors must preserve
THIRD_PARTY_NOTICES.md and the applicable text in
LICENSES/.
