Skip to content

Commit 69d1849

Browse files
authored
Merge pull request #137 from ty-everett/codex/async-auth-session-managers
Add async auth session managers
2 parents 8d3a4f3 + c078013 commit 69d1849

15 files changed

Lines changed: 223 additions & 76 deletions

File tree

docs/architecture/identity.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ Server response:
5151

5252
Both parties emerge from the handshake having verified each other's identity keys. Subsequent requests in the same session use a session token derived from the initial handshake.
5353

54-
`@bsv/auth-express-middleware` installs BRC-31 as Express middleware. Any route wrapped by it requires a valid BRC-31 handshake from the client.
54+
`@bsv/auth-express-middleware` installs BRC-103/BRC-104 as Express middleware. Any route wrapped by it requires a valid BRC-103 handshake from the client over the BRC-104 HTTP binding.
5555

56-
The machine-readable spec is at `specs/auth/brc31-handshake.yaml` (AsyncAPI 3.0).
56+
The machine-readable spec is at `specs/auth/brc103-mutual-auth.yaml` (AsyncAPI 3.0).
5757

5858
## Identity Keys
5959

docs/packages/middleware/auth-express-middleware.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ app.listen(3000)
5757
- **AuthRequest** — Extended Request with `.auth` object containing `identityKey`
5858
- **BRC-103 mutual authentication** — Nonce-based challenge-response with signatures
5959
- **BRC-104 HTTP transport** — Custom headers (`x-bsv-auth-*`) for non-general messages
60-
- **Session management** — Tracks nonces per identity; SessionManager interface extensible
60+
- **Session management** — Tracks nonces per identity; AsyncSessionManager interface extensible to synchronous or asynchronous stores
6161
- **Certificate exchange** — Optional verifiable certificates during handshake
6262
- **Response wrapping** — Middleware intercepts response methods to sign responses before sending
6363
- **Optional authentication**`allowUnauthenticated` flag for mixed public/private endpoints
@@ -104,6 +104,19 @@ app.use(express.json())
104104
app.use(authMiddleware)
105105
```
106106

107+
### Shared sessions for scaled servers
108+
109+
The default `SessionManager` stores BRC-103 nonce/session state in one process. For load-balanced Express servers, pass a `AsyncSessionManager` backed by shared storage so every instance can resolve the same handshake state instead of relying on sticky routing.
110+
111+
```typescript
112+
import { AsyncSessionManager } from '@bsv/sdk'
113+
114+
const authMiddleware = createAuthMiddleware({
115+
wallet,
116+
sessionManager: sharedSessionManager satisfies AsyncSessionManager
117+
})
118+
```
119+
107120
### Per-route protection
108121

109122
```typescript
@@ -118,7 +131,7 @@ app.post('/secure-endpoint', createAuthMiddleware({ wallet }), (req, res) => {
118131
- **BRC-104 HTTP transport** — Uses custom headers and `/.well-known/auth` endpoint
119132
- **General vs non-general messages** — Non-general for handshake, general for authenticated app requests
120133
- **Nonce binding** — Each request/response pair bound to server-generated nonce; prevents replay attacks
121-
- **Session management** — Tracks nonces per identity; SessionManager interface extensible
134+
- **Session management** — Tracks nonces per identity; AsyncSessionManager interface extensible to synchronous or asynchronous stores
122135
- **Certificate exchange** — Optional verifiable certificates during handshake
123136
- **Response wrapping** — Middleware intercepts response methods to sign responses before sending
124137

docs/reference/brc-index.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,13 @@ Implementations: `@bsv/paymail`, `@bsv/message-box-client`, `@bsv/402-pay`
7878

7979
Conformance vectors: `conformance/vectors/wallet/brc29/`
8080

81-
### BRC-31: HTTP Mutual Authentication Handshake
81+
### BRC-103 / BRC-104: Mutual Authentication Handshake + HTTP Transport
8282

83-
Specifies the `x-bsv-auth-*` HTTP headers that implement mutual-auth challenge-response over standard HTTP. Built on the BRC-103 Peer framework.
83+
BRC-103 specifies the peer-to-peer mutual authentication protocol (AuthMessage envelope, nonces, certificates, sessions). BRC-104 binds it to HTTP via the `/.well-known/auth` endpoint and `x-bsv-auth-*` headers. Replaces the legacy BRC-31 identifier.
8484

85-
Implementations: `@bsv/auth-express-middleware`, `@bsv/authsocket`
85+
Implementations: `@bsv/auth-express-middleware`, `@bsv/authsocket`, `@bsv/sdk` (`Peer`, `Transport`)
8686

87-
Spec: `specs/auth/brc31-handshake.yaml`
87+
Spec: `specs/auth/brc103-mutual-auth.yaml`
8888

8989
### BRC-42: Key Derivation Scheme (BKDS)
9090

docs/specs/brc-31-auth.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,4 @@ BRC-31-related portable coverage currently lives in `conformance/vectors/messagi
186186

187187
## Spec artifact
188188

189-
[brc31-handshake.yaml](https://github.com/bsv-blockchain/ts-stack/blob/main/specs/auth/brc31-handshake.yaml)
189+
[brc103-mutual-auth.yaml](https://github.com/bsv-blockchain/ts-stack/blob/main/specs/auth/brc103-mutual-auth.yaml)

packages/middleware/auth-express-middleware/API.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
99
```ts
1010
export interface AuthMiddlewareOptions {
1111
wallet: WalletInterface;
12-
sessionManager?: SessionManager;
12+
sessionManager?: SessionManager | AsyncSessionManager;
1313
allowUnauthenticated?: boolean;
1414
certificatesToRequest?: RequestedCertificateSet;
1515
onCertificatesReceived?: (senderPublicKey: string, certs: VerifiableCertificate[], req: Request, res: Response, next: NextFunction) => void;

packages/middleware/auth-express-middleware/README.md

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ By layering **BRC-103** on top of Express, you can:
5353
Supports BRC-103’s concept of revealing only certain fields in a certificate, helping to preserve privacy for you and your users while verifying necessary information.
5454

5555
- **Extendable**
56-
Provide a custom `SessionManager` or plug in advanced logic for verifying user attributes.
56+
Provide a custom `AsyncSessionManager`, including asynchronous shared stores, or plug in advanced logic for verifying user attributes.
5757

5858
---
5959

@@ -123,7 +123,7 @@ Use the factory function:
123123
createAuthMiddleware({
124124
wallet: myWallet,
125125
allowUnauthenticated?: boolean,
126-
sessionManager?: SessionManager,
126+
sessionManager?: AsyncSessionManager,
127127
certificatesToRequest?: RequestedCertificateSet,
128128
onCertificatesReceived?: (senderPublicKey, certs, req, res, next) => void
129129
})
@@ -133,10 +133,49 @@ createAuthMiddleware({
133133

134134
- **`wallet`** *(required)*: A wallet instance that implements signing and key management, typically from `@bsv/sdk` or your own custom build.
135135
- **`allowUnauthenticated`** *(default: `false`)*: If `true`, requests without valid BRC-103 authentication will **not** be rejected. Instead, `req.auth.identityKey` is set to `"unknown"`.
136-
- **`sessionManager`** *(optional)*: Customize session management (nonce tracking, etc.). By default, an internal `SessionManager` is used.
136+
- **`sessionManager`** *(optional)*: Customize session management (nonce tracking, etc.). By default, an internal `SessionManager` is used. For horizontally scaled servers, pass a `AsyncSessionManager` backed by shared storage so every instance can resolve the same nonce/session state.
137137
- **`certificatesToRequest`** *(optional)*: A specification of which certificates (by type, fields, issuer) to request automatically from the peer.
138138
- **`onCertificatesReceived`** *(optional)*: Callback invoked when the peer responds with **Verifiable Certificates**.
139139

140+
### Horizontal Scaling
141+
142+
The default `SessionManager` stores handshake state in memory. That is appropriate for one process, but a load-balanced deployment can route the initial nonce exchange, certificate request, and general message to different server instances. In that topology, provide a `AsyncSessionManager` backed by shared storage so nonce lookups are available to every instance:
143+
144+
```ts
145+
import { PeerSession, AsyncSessionManager } from '@bsv/sdk'
146+
147+
async function getSharedSession(identifier: string): Promise<PeerSession | undefined> {
148+
const session = await store.get(`auth:nonce:${identifier}`)
149+
if (session != null) return session
150+
151+
const nonce = await store.get(`auth:identity:${identifier}`)
152+
return nonce == null ? undefined : await store.get(`auth:nonce:${nonce}`)
153+
}
154+
155+
const sessionManager: AsyncSessionManager = {
156+
async addSession(session: PeerSession) {
157+
await store.set(`auth:nonce:${session.sessionNonce}`, session)
158+
await store.set(`auth:identity:${session.peerIdentityKey}`, session.sessionNonce)
159+
},
160+
async updateSession(session: PeerSession) {
161+
await store.set(`auth:nonce:${session.sessionNonce}`, session)
162+
await store.set(`auth:identity:${session.peerIdentityKey}`, session.sessionNonce)
163+
},
164+
async getSession(identifier: string) {
165+
return await getSharedSession(identifier)
166+
},
167+
async removeSession(session: PeerSession) {
168+
await store.delete(`auth:nonce:${session.sessionNonce}`)
169+
await store.delete(`auth:identity:${session.peerIdentityKey}`)
170+
},
171+
async hasSession(identifier: string) {
172+
return await getSharedSession(identifier) != null
173+
}
174+
}
175+
176+
app.use(createAuthMiddleware({ wallet: myWallet, sessionManager }))
177+
```
178+
140179
### Injecting the Middleware into Express
141180

142181
Simply call:
@@ -200,7 +239,7 @@ If `allowUnauthenticated` is **false**, any request without a valid handshake or
200239
Returns an Express middleware function. **Options**:
201240

202241
- **`wallet`**: (required) A BRC-100 object implementing your signing and verification logic.
203-
- **`sessionManager`**: (optional) Manage nonces & state across requests.
242+
- **`sessionManager`**: (optional) Manage nonces & state across requests. Supports synchronous or asynchronous `AsyncSessionManager` implementations.
204243
- **`allowUnauthenticated`**: (optional) If true, non-authenticated requests are allowed but marked as `identityKey: 'unknown'`.
205244
- **`certificatesToRequest`**: (optional) Automatic certificate request data structure.
206245
- **`onCertificatesReceived`**: (optional) A callback triggered when certs arrive from the client.
@@ -290,4 +329,4 @@ app.listen(3000, () => console.log('Server up!'))
290329

291330
---
292331

293-
**Happy hacking!** If you have questions, suggestions, or want to contribute improvements, feel free to open an issue or PR in our repository.
332+
**Happy hacking!** If you have questions, suggestions, or want to contribute improvements, feel free to open an issue or PR in our repository.

packages/middleware/auth-express-middleware/src/index.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
RequestedCertificateSet,
1010
Transport,
1111
SessionManager,
12+
AsyncSessionManager,
1213
WalletInterface,
1314
PubKeyHex
1415
} from '@bsv/sdk'
@@ -37,7 +38,10 @@ export interface AuthRequest extends Request {
3738
// Developers may optionally provide a handler for incoming certificates.
3839
export interface AuthMiddlewareOptions {
3940
wallet: WalletInterface
40-
sessionManager?: SessionManager // Optional if dev wants custom SessionManager
41+
// Optional session store. Default is in-process synchronous `SessionManager`.
42+
// Pass an `AsyncSessionManager` (Redis/SQL-backed, etc.) to share state
43+
// across load-balanced instances; Peer awaits internally so both work.
44+
sessionManager?: SessionManager | AsyncSessionManager
4145
allowUnauthenticated?: boolean
4246
certificatesToRequest?: RequestedCertificateSet
4347
onCertificatesReceived?: (
@@ -378,7 +382,7 @@ export class ExpressTransport implements Transport {
378382
* @param {NextFunction} next - The Express `next` middleware function.
379383
* @param {Function} [onCertificatesReceived] - Optional callback invoked when certificates are received.
380384
*/
381-
public handleIncomingRequest (
385+
public async handleIncomingRequest (
382386
req: AuthRequest,
383387
res: Response,
384388
next: NextFunction,
@@ -389,7 +393,7 @@ export class ExpressTransport implements Transport {
389393
res: Response,
390394
next: NextFunction
391395
) => void
392-
): void {
396+
): Promise<void> {
393397
this.log('debug', 'Handling incoming request', {
394398
path: req.path,
395399
headers: req.headers,
@@ -402,7 +406,7 @@ export class ExpressTransport implements Transport {
402406
throw new Error('You must set a Peer before you can handle incoming requests!')
403407
}
404408
if (req.path === '/.well-known/auth') {
405-
this.handleWellKnownAuth(req, res, next, onCertificatesReceived)
409+
await this.handleWellKnownAuth(req, res, next, onCertificatesReceived)
406410
} else if (req.headers['x-bsv-auth-request-id']) {
407411
this.handleGeneralMessage(req, res, next)
408412
} else {
@@ -417,7 +421,7 @@ export class ExpressTransport implements Transport {
417421
/**
418422
* Handles a request to /.well-known/auth (non-general / handshake messages).
419423
*/
420-
private handleWellKnownAuth (
424+
private async handleWellKnownAuth (
421425
req: AuthRequest,
422426
res: Response,
423427
next: NextFunction,
@@ -428,7 +432,7 @@ export class ExpressTransport implements Transport {
428432
res: Response,
429433
next: NextFunction
430434
) => void
431-
): void {
435+
): Promise<void> {
432436
const message = req.body as AuthMessage
433437
this.log('debug', 'Received non-general message at /.well-known/auth', { message })
434438

@@ -443,7 +447,7 @@ export class ExpressTransport implements Transport {
443447
this.openNonGeneralHandles[requestId] = [{ res, next }]
444448
}
445449

446-
if (!this.peer!.sessionManager.hasSession(message.identityKey)) {
450+
if (!await this.peer!.sessionManager.hasSession(message.identityKey)) {
447451
this.registerCertificateListener(req, res, next, requestId, message, onCertificatesReceived)
448452
}
449453

@@ -650,7 +654,7 @@ export class ExpressTransport implements Transport {
650654
}
651655

652656
this.hijackResponse(res, next, wrapper, buildAndSendResponse)
653-
this.scheduleNextOrCertificateWait(next, senderPublicKey, wrapper, buildAndSendResponse)
657+
void this.scheduleNextOrCertificateWait(next, senderPublicKey, wrapper, buildAndSendResponse).catch(next)
654658
}
655659

656660
/**
@@ -728,13 +732,13 @@ export class ExpressTransport implements Transport {
728732
/**
729733
* Either calls next() immediately or stores it pending certificate arrival.
730734
*/
731-
private scheduleNextOrCertificateWait (
735+
private async scheduleNextOrCertificateWait (
732736
next: NextFunction,
733737
senderPublicKey: string,
734738
wrapper: ResponseWriterWrapper,
735739
buildAndSendResponse: () => Promise<void>
736-
): void {
737-
const hasSession = this.peer?.sessionManager.hasSession(senderPublicKey) ?? false
740+
): Promise<void> {
741+
const hasSession = await (this.peer?.sessionManager.hasSession(senderPublicKey) ?? false)
738742
const needsCertificates = this.peer?.certificatesToRequest?.certifiers?.length
739743
this.log('debug', 'Checking if we need to wait for certificates', {
740744
senderPublicKey,
@@ -991,6 +995,6 @@ export function createAuthMiddleware (options: AuthMiddlewareOptions): (req: Aut
991995
method: req.method
992996
})
993997
}
994-
transport.handleIncomingRequest(req, res, next, onCertificatesReceived)
998+
void transport.handleIncomingRequest(req, res, next, onCertificatesReceived).catch(next)
995999
}
9961000
}

0 commit comments

Comments
 (0)