How Buzz Handles NIP-42 Authentication Over WebSockets
Buzz implements NIP-42 authentication as a challenge-response handshake over WebSockets, where the relay issues a random 32-byte challenge and the client responds with a signed kind 22242 AUTH event containing that challenge and a host-bound relay tag.
The Buzz Nostr relay uses a layered authentication architecture to enforce NIP-42 requirements before allowing restricted operations. This WebSocket-based flow ensures that clients cryptographically prove ownership of their public key while binding the authentication to a specific relay host.
NIP-42 Challenge-Response Flow Overview
When a WebSocket connection opens, Buzz immediately initiates the NIP-42 handshake. The flow consists of three distinct phases: challenge issuance, client response, and server verification.
The relay generates a random 32-byte hex challenge in crates/buzz-auth/src/nip42.rs using generate_challenge(), storing it in the connection state. The client must then construct a kind 22242 AUTH event containing this challenge in a ["challenge", "..."] tag and a ["relay", "wss://..."] tag matching the host URL. Finally, the relay verifies the signature and challenge match before upgrading the connection's AuthState to authenticated.
Client-Side Authentication Implementation
Initiating the WebSocket Connection
The WsConnection struct in crates/buzz-ws-client/src/connection.rs manages the TCP/WebSocket lifecycle. Upon connection establishment, the client automatically receives the AUTH challenge from the relay.
use buzz_ws_client::WsConnection;
// Open the socket and receive the NIP-42 challenge
let mut conn = WsConnection::new("wss://relay.example.com").await?;
let challenge = conn.receive_auth_challenge().await?;
The receive_auth_challenge() method parses the relay's initial challenge message and returns the hex-encoded random string required for the next step.
Generating and Signing the AUTH Event
The client constructs a kind 22242 event using the EventBuilder from the buzz-keys crate. This event must include the challenge string and the relay host for binding verification.
use buzz_keys::EventBuilder;
// Build the NIP-42 AUTH event (kind 22242)
let auth_event = EventBuilder::new(22242)
.content("")
.add_tag(["relay", conn.url().host_str().unwrap()])
.add_tag(["challenge", challenge])
.sign(&keys)?;
The buzz-ws-client::publish_event helper simplifies this by wrapping the signing and transmission logic. After signing, the client sends the event via conn.send_message(buzz_ws_client::Message::Auth(auth_event)).await?.
Server-Side Verification and State Management
Challenge Verification in the Relay
The handle_auth function in crates/buzz-relay/src/handlers/auth.rs processes incoming AUTH events. It extracts the challenge and relay tags, then calls verify_nip42_event() from crates/buzz-auth/src/nip42.rs to perform cryptographic validation.
use buzz_auth::nip42::{verify_nip42_event, AuthContext};
fn handle_auth(event: Event, expected_relay: &str) -> Result<AuthContext, AuthError> {
// Verifies signature, challenge match, and host binding
verify_nip42_event(event, expected_relay)
}
Verification ensures the event is correctly signed, contains the expected challenge previously sent to that connection, and that the relay tag matches the host URL (preventing replay attacks across different relays).
AuthState Tracking and Timeouts
The connection maintains an AuthState enum defined in crates/buzz-relay/src/connection.rs that transitions from Unauthenticated → WaitingForChallenge → Authenticated. If the client fails to complete the handshake within the configurable timeout (default 5 seconds), the connection closes automatically.
After successful verification, handlers across the relay—such as relay_admin, moderation_commands, and ingest—gate their logic on conn.authenticated_pubkey(), applying additional auth policies like API-token checks or ban lists based on the verified public key.
Practical Integration Examples
Automatic Authentication with WsClient
For most applications, the high-level WsClient API handles the entire NIP-42 handshake transparently.
use buzz_ws_client::WsClient;
use buzz_keys::Keypair;
// Load private key and connect with automatic NIP-42 auth
let keys = Keypair::from_secret_key_env().unwrap();
let client = WsClient::connect_and_authenticate("wss://relay.example.com", &keys)
.await
.expect("NIP-42 handshake failed");
// Client is ready for normal Nostr operations
client.publish_event(event).await?;
Manual Handshake for Custom Agents
Custom clients or debugging tools can manually perform each step of the handshake for finer control over the authentication process.
// 1. Open socket
let mut conn = buzz_ws_client::WsConnection::new("wss://relay.example.com").await?;
// 2. Receive challenge
let challenge = conn.receive_auth_challenge().await?;
// 3. Build AUTH event
let auth_event = buzz_keys::EventBuilder::new(22242)
.content("")
.add_tag(["relay", "relay.example.com"])
.add_tag(["challenge", challenge])
.sign(&keys)?;
// 4. Send AUTH response
conn.send_message(buzz_ws_client::Message::Auth(auth_event)).await?;
// 5. Proceed with REQ/EVENT/COUNT
Summary
- Challenge Generation: Buzz generates a random 32-byte hex challenge in
buzz-auth/src/nip42.rsupon WebSocket connection. - Client Response: Clients create kind 22242 events containing the challenge and host-bound relay tag using
buzz-ws-clienthelpers. - Server Verification: The relay verifies signatures, challenge matches, and host binding in
buzz-relay/src/handlers/auth.rs. - State Management:
AuthStatetracks authentication progress with a default 5-second timeout for handshake completion. - Policy Enforcement: Post-authentication, the relay applies permission checks based on the verified public key extracted from the NIP-42 event.
Frequently Asked Questions
What is the purpose of the relay tag in NIP-42 AUTH events?
The relay tag binds the authentication event to a specific host URL, preventing replay attacks where an attacker intercepts a valid AUTH event and reuse it on a different relay. verify_nip42_event in crates/buzz-auth/src/nip42.rs explicitly checks that the tag value matches the expected relay host.
How does Buzz handle authentication timeouts?
Buzz enforces a configurable timeout (defaulting to 5 seconds) on the AuthState transition from WaitingForChallenge to Authenticated. If the client fails to send a valid kind 22242 event within this window, crates/buzz-relay/src/connection.rs closes the WebSocket connection automatically.
Can I disable NIP-42 authentication on a Buzz relay?
While the code supports optional authentication policies for different handlers, the NIP-42 challenge is always issued upon connection in the current implementation. Administrative handlers and moderation commands specifically require authenticated_pubkey() to return a valid key, making authentication mandatory for privileged operations.
What happens if the AUTH event signature is invalid?
The handle_auth function in crates/buzz-relay/src/handlers/auth.rs rejects events that fail verify_nip42_event, returning an error that prevents the connection state from transitioning to Authenticated. The connection remains in an unauthenticated state and may be disconnected if restricted operations are attempted.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →