# How Buzz Implements NIP-98 Authentication for HTTP Endpoints

> Learn how Buzz implements NIP-98 authentication for HTTP endpoints by embedding signed Nostr events in the Authorization header for secure server verification.

- Repository: [Block Open Source/buzz](https://github.com/block/buzz)
- Tags: how-to-guide
- Published: 2026-08-28

---

**Buzz implements NIP-98 authentication by embedding a signed, time-bound Nostr event in the `Authorization` header, which the server verifies against expected URLs, request payloads, and replay guards before processing.**

Block's Buzz relay secures its HTTP API using the NIP-98 standard, extending Nostr's cryptographic identity model to REST endpoints. This implementation embeds ephemeral authentication events directly into HTTP headers, providing the same security guarantees as NIP-42 WebSocket authentication while maintaining stateless request semantics. Every authenticated request flows through four tightly-coupled verification layers implemented across the `buzz-relay` and `buzz-auth` crates.

## Authorization Header Construction

The client-side implementation centers on `build_nip98_auth_header` in [`buzz-relay/src/relay.rs`](https://github.com/block/buzz/blob/main/buzz-relay/src/relay.rs) (lines 118-130). This function accepts the HTTP method, full request URL, optional request body bytes, and the tenant's signing keys to generate a fresh NIP-98 event.

The resulting event uses the current Unix timestamp for `created_at` and includes tags binding it to the specific HTTP verb and payload. The helper serializes this event into the standard `Authorization: Nostr <event-json>` header format. For advanced use cases, `build_nip98_auth_header_for_keys` provides a lower-level interface that accepts raw `Keys` instead of the full relay state.

```rust
use buzz_relay::relay::{build_nip98_auth_header_for_keys, Method};
use nostr::Keys;

let keys = Keys::new(...);
let auth_header = build_nip98_auth_header_for_keys(
    &keys, 
    &Method::POST, 
    "https://relay.example.com/events", 
    b"{\"kind\":1}"
).expect("failed to sign NIP-98");

```

## URL Binding and Request Context

To prevent credential theft across communities, Buzz derives the expected verification URL using `nip98_expected_url` in [`buzz-relay/src/api/bridge.rs`](https://github.com/block/buzz/blob/main/buzz-relay/src/api/bridge.rs) (lines 191-210). This function constructs the canonical URL by combining the relay's public base URL, the community (tenant) identifier, and the specific request path.

This binding ensures that a NIP-98 event signed for one community cannot be replayed against another, even if an attacker obtains the raw authorization header. The expected URL becomes part of the signed event's tag structure, cryptographically tying the authentication to the exact host and path combination.

## Replay Protection Mechanism

Before accepting any request, Buzz checks the event ID against the `Nip98ReplayGuard` implemented in [`buzz-auth/src/nip98_replay.rs`](https://github.com/block/buzz/blob/main/buzz-auth/src/nip98_replay.rs). The guard stores a deterministic replay key using the format `buzz:{community}:nip98:{event_id_hex}` in a per-community cache.

The `check_nip98_replay` function rejects any event ID that has been seen within the retention window, effectively mitigating replay attacks. The guard is injected into the relay state via `state.nip98_replay`, making it available to all HTTP handlers without additional configuration.

## Server-Side Event Verification

The final validation happens in [`buzz-auth/src/nip98.rs`](https://github.com/block/buzz/blob/main/buzz-auth/src/nip98.rs) through `verify_nip98_event`. This function performs three critical checks:

1. **Cryptographic integrity**: Verifies the event signature against the public key embedded in the event.
2. **Context binding**: Confirms the event's URL tag matches the `nip98_expected_url` derivation and that the HTTP method tag matches the incoming request.
3. **Temporal freshness**: Validates that the event's `created_at` timestamp falls within a configurable freshness window to prevent stale authentication.

If the request includes a body, the verification also checks the SHA-256 hash of the payload against the event's payload tag.

## Implementation Examples

### Manual POST Request with Body

When you need full control over the signing process, use the low-level helper to authenticate POST requests with JSON payloads:

```rust
use buzz_auth::nip98::{HttpMethod, HttpData};
use buzz_relay::relay::build_nip98_auth_header_for_keys;
use nostr::Keys;

let keys = Keys::from_env("BUZZ_PRIVATE_KEY")?;
let body = b"{\"kind\":1,\"content\":\"Hello\"}";
let auth = build_nip98_auth_header_for_keys(
    &keys,
    &Method::POST,
    "https://relay.example.com/events",
    body
)?;

let resp = reqwest::Client::new()
    .post("https://relay.example.com/events")
    .header("Authorization", auth)
    .body(body)
    .send()
    .await?;

```

### High-Level GET Requests

For read operations, the high-level helper in the Tauri bridge handles key management automatically:

```rust
use buzz_relay::relay::build_nip98_auth_header;

let url = "https://relay.example.com/query";
let auth = build_nip98_auth_header(&Method::GET, url, &[], &state)?;

let resp = reqwest::Client::new()
    .get(url)
    .header("Authorization", auth)
    .send()
    .await?;

```

### CLI Automation

The Buzz CLI automatically injects NIP-98 headers when contacting relays, loading keys from the `BUZZ_PRIVATE_KEY` environment variable:

```bash
buzz events create --kind 1 --content "Authenticated via NIP-98"

```

## Summary

- **Header Generation**: `build_nip98_auth_header` in [`buzz-relay/src/relay.rs`](https://github.com/block/buzz/blob/main/buzz-relay/src/relay.rs) creates `Authorization: Nostr <event>` headers with fresh timestamps.
- **URL Isolation**: `nip98_expected_url` binds events to specific community URLs, preventing cross-tenant credential replay.
- **Replay Defense**: The `Nip98ReplayGuard` in [`buzz-auth/src/nip98_replay.rs`](https://github.com/block/buzz/blob/main/buzz-auth/src/nip98_replay.rs) stores event IDs under `buzz:{community}:nip98:{event_id_hex}` keys to block duplicate submissions.
- **Verification**: `verify_nip98_event` in [`buzz-auth/src/nip98.rs`](https://github.com/block/buzz/blob/main/buzz-auth/src/nip98.rs) validates signatures, URL tags, HTTP methods, and timestamp freshness.

## Frequently Asked Questions

### How does NIP-98 authentication differ from NIP-42 in Buzz?

While NIP-42 handles WebSocket AUTH messages for persistent connections, Buzz uses NIP-98 for stateless HTTP requests. NIP-98 embeds the authentication event directly in the `Authorization` header of each request, whereas NIP-42 establishes a session through a WebSocket handshake. Both use the same cryptographic primitives, but NIP-98 includes the HTTP method and request body hash in the signed event.

### What prevents an attacker from replaying a captured NIP-98 header?

Buzz implements the `Nip98ReplayGuard` which maintains a cache of recently seen event IDs per community. When `check_nip98_replay` processes a request, it checks for the existence of `buzz:{community}:nip98:{event_id_hex}` in the cache and rejects duplicates. Combined with the strict `created_at` freshness window enforced by `verify_nip98_event`, this renders captured headers useless after their first use or after expiration.

### Why does Buzz include the community identifier in the expected URL?

The `nip98_expected_url` function includes the community ID to scope authentication events to specific tenants. This prevents a user from extracting a valid `Authorization` header sent to Community A and using it to access resources in Community B, even if both communities reside on the same Buzz relay instance. The community identifier becomes part of the signed URL tag, creating a cryptographic boundary between tenants.

### What happens if the `created_at` timestamp is slightly off?

`verify_nip98_event` rejects events where the `created_at` timestamp falls outside a configurable freshness window (typically a few minutes). This prevents attackers from pre-signing events for later use. Clients must generate a fresh NIP-98 event for each request with a current timestamp, which the `build_nip98_auth_header` helper handles automatically.