Authenticate protected websocket connections in-band - #2280
Conversation
|
@cdk-bot review |
The browser WebSocket API cannot set the Blind-auth header, so a browser wallet could not authenticate a protected NUT-17 /v1/ws connection. NUT-22 solves this with an in-band JSON-RPC authenticate command sent after the connection is established. Mint: add the authenticate command, gate subscribe/unsubscribe with error 31001 until a connection is authenticated, verify and spend the BAT via the existing verify_auth path, and close never-authenticated connections after a timeout. The upgrade handler no longer rejects a header-less upgrade to a protected endpoint; it defers to the in-band command. Wallet: for blind auth on the websocket, send the authenticate command right after connecting instead of a Blind-auth header, so one code path serves native and browser targets. Only sent when the mint protects /v1/ws.
e6d65cb to
eb548f1
Compare
cdk-bot
left a comment
There was a problem hiding this comment.
Verified findings approved for disclosure:
- New wallet loses WS subscriptions against older mints that protect /v1/ws (Blind-auth header removed, no fallback) (low) - A new wallet connecting to a pre-PR mint that has /v1/ws manually configured as blind-auth-protected will fail the WebSocket handshake (no Blind-auth header sent), permanently degrade to 2-second HTTP polling for the process lifetime, and lose push notifications. The mint's default configuration did not protect /v1/ws before this PR, so only manually-configured mints are affected. Functionality is preserved via polling; only efficiency and latency are impacted.
| }; | ||
| let endpoint = cdk_common::ProtectedEndpoint::new(Method::Get, RoutePath::Ws); | ||
| let auth_wallet = client.http_client.get_auth_wallet().await; | ||
|
|
There was a problem hiding this comment.
What changed
stream_client no longer sends the Blind-auth header when opening the NUT-17 websocket. The old code fetched a BAT at connect time and set either a Clear-auth or Blind-auth header; the new code only ever sets Clear-auth and relies exclusively on the in-band NUT-22 authenticate command for blind auth (crates/cdk/src/wallet/subscription.rs, the headers/needs_blind_auth block around lines 583–603).
Problem
Against a mint that protects GET /v1/ws with blind auth but does not implement this PR's in-band authenticate flow — i.e. any pre-PR cdk mint (whose ws_handler called verify_auth unconditionally at upgrade and rejected header-less upgrades with a 4xx) or any other implementation that only supports header-based blind auth — the new wallet's header-less upgrade is rejected during the handshake.
The failure path is silent and permanent for the wallet process:
tokio_tungstenite::connect_asyncfails on the rejected handshake →WsError::Connection(..)(crates/cdk-http-client/src/ws/native.rs).map_ws_errormapsWsError::Connection(_) => PubsubError::NotSupported(crates/cdk/src/wallet/subscription.rs).- The consumer treats
NotSupportedas "streaming not supported" and setsstream_supported = false(crates/cdk-common/src/pub_sub/remote_consumer.rs), so it never retries the websocket again — all subscriptions silently degrade to the 2-second HTTP polling fallback for the lifetime of the process.
Before this PR, the same wallet would have sent the Blind-auth header and used push notifications successfully. Note the reverse direction is fine: the mint side of this PR keeps the header path (ws_handler still verifies a provided header token at upgrade), so old wallets keep working against new mints.
Suggested fix
Consider a compatibility fallback, e.g. when blind auth is required and the websocket handshake fails (or the server responds to authenticate with an unknown-method/parse error), retry the connection once with the legacy Blind-auth header before giving up on streaming. If the protocol break is intentional, it is at least worth an explicit mention in the changelog/release notes so mint operators know older mints' protected /v1/ws is incompatible with new wallets.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #2280 +/- ##
========================================
Coverage 74.19% 74.20%
========================================
Files 363 364 +1
Lines 85241 85468 +227
========================================
+ Hits 63248 63422 +174
- Misses 21993 22046 +53 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Description
This depends on cashubtc/nuts#413
The browser WebSocket API cannot set the Blind-auth header, so a browser wallet could not authenticate a protected NUT-17 /v1/ws connection. NUT-22 solves this with an in-band JSON-RPC authenticate command sent after the connection is established.
Mint: add the authenticate command, gate subscribe/unsubscribe with error 31001 until a connection is authenticated, verify and spend the BAT via the existing verify_auth path, and close never-authenticated connections after a timeout. The upgrade handler no longer rejects a header-less upgrade to a protected endpoint; it defers to the in-band command.
Wallet: for blind auth on the websocket, send the authenticate command right after connecting instead of a Blind-auth header, so one code path serves native and browser targets. Only sent when the mint protects /v1/ws.
Notes to the reviewers
Suggested CHANGELOG Updates
CHANGED
ADDED
REMOVED
FIXED
Checklist
just quick-checkbefore committingcrates/cdk-ffi)