Skip to content

Authenticate protected websocket connections in-band - #2280

Draft
crodas wants to merge 1 commit into
cashubtc:mainfrom
crodas:feat/nut22-websocket-auth
Draft

Authenticate protected websocket connections in-band#2280
crodas wants to merge 1 commit into
cashubtc:mainfrom
crodas:feat/nut22-websocket-auth

Conversation

@crodas

@crodas crodas commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

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

  • I followed the code style guidelines
  • I ran just quick-check before committing
  • If the Wallet API was modified (added/removed/changed), I have reflected those changes in the FFI bindings (crates/cdk-ffi)

@crodas
crodas requested review from asmogo and thesimplekid August 1, 2026 15:35
@crodas crodas self-assigned this Aug 1, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in CDK Aug 1, 2026
@crodas

crodas commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

@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.
@crodas
crodas force-pushed the feat/nut22-websocket-auth branch from e6d65cb to eb548f1 Compare August 1, 2026 15:37

@cdk-bot cdk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. tokio_tungstenite::connect_async fails on the rejected handshake → WsError::Connection(..) (crates/cdk-http-client/src/ws/native.rs).
  2. map_ws_error maps WsError::Connection(_) => PubsubError::NotSupported (crates/cdk/src/wallet/subscription.rs).
  3. The consumer treats NotSupported as "streaming not supported" and sets stream_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

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 56.15385% with 114 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.20%. Comparing base (77256eb) to head (eb548f1).

Files with missing lines Patch % Lines
crates/cdk/src/wallet/subscription.rs 7.44% 87 Missing ⚠️
crates/cdk-axum/src/ws/mod.rs 87.50% 13 Missing ⚠️
crates/cdk-axum/src/ws/authenticate.rs 57.69% 11 Missing ⚠️
crates/cashu/src/nuts/nut17/ws.rs 97.05% 1 Missing ⚠️
crates/cdk-axum/src/router_handlers.rs 0.00% 1 Missing ⚠️
crates/cdk-integration-tests/src/init_auth_mint.rs 0.00% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants