Home | Back

NOSTR

Nostr NIP-98 — HTTP Auth

Authenticate HTTP requests with an ephemeral, signed Nostr event instead of passwords or API keys.

At a glance

  • Purpose: Prove request authorship using a signed event, carried in the HTTP Authorization header.
  • Event kind: 27235 (ephemeral).
  • Header: Authorization: Nostr <base64(json_event)>.
  • Required tags: ["u", "<absolute URL>"], ["method", "<HTTP method>"].
  • Optional tag (body requests): ["payload", "<sha256(body)>"].
  • Content: SHOULD be empty.

Client flow

  1. Build event with kind=27235, tags u, method, and optionally payload.
  2. Compute event id: sha256(JSON.encode([0, pubkey, created_at, kind, tags, content])).
  3. Sign id with Schnorr over secp256k1 (per NIP-01). Put signature in sig.
  4. Base64-encode the whole JSON event and send Authorization: Nostr ....

Server validation checklist

  1. Kind: must be 27235.
  2. Time window: reject stale / future events (e.g. ±60s).
  3. URL: u equals the absolute request URL (including query).
  4. Method: method matches the HTTP verb actually used.
  5. Payload (if present or required): hash matches the request body.
  6. Signature: verify Schnorr signature of id against pubkey.
  7. On failure → respond 401 Unauthorized.

Canonical event structure (NIP-01 rules)

Field Description
id Lowercase hex of sha256 of the canonical array [0, pubkey, created_at, kind, tags, content].
pubkey Lowercase 32-byte hex of author’s X-only pubkey.
created_at Unix time (seconds).
kind 27235 for NIP-98.
tags Includes u, method, optional payload.
content SHOULD be an empty string.
sig 64-byte Schnorr signature (hex) over the id.

Example NIP-98 event (JSON)

{
  "id": "fe964e758903360f28d8424d092da8494ed207cba823110be3a57dfe4b578734",
  "pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
  "content": "",
  "kind": 27235,
  "created_at": 1682327852,
  "tags": [
    ["u", "https://api.example.com/upload"],
    ["method", "POST"],
    ["payload", "3a7bd3e2360a..."]
  ],
  "sig": "5ed9d8ec958bc854...05c22184"
}

HTTP header + cURL

Authorization: Nostr <base64(json_event)>
curl -X POST https://api.example.com/upload \
  -H "Authorization: Nostr <base64(json_event)>" \
  -H "Content-Type: application/json" \
  --data-binary @body.json

Note: Base64 increases size (~33% overhead) but ensures ASCII-safe transport in headers.

Security considerations

  • Replay protection: enforce tight created_at window; reject re-use.
  • Exact target binding: compare u to the full absolute URL (scheme, host, path, query).
  • Method binding: the method tag must equal the HTTP verb.
  • Payload binding: verify body hash; required by some endpoints (e.g. relay admin APIs).
  • Clock skew: allow small skew but fail hard outside tolerance.
  • Logging: avoid logging raw tokens in production.

Implementer notes

  • Use your platform’s Schnorr (secp256k1, BIP-340) primitives (e.g. Rust secp256k1, Node @noble/curves/secp256k1).
  • Serialise exactly per NIP-01 (no extra whitespace; UTF-8).
  • Base64 the JSON event, not the hash or signature.
  • Return 401 on any check failure; do not leak which check failed.

Minimal server-side verification (pseudocode)

// input: req (HTTP request)
token = req.headers["authorization"];                   // "Nostr <base64>"
assert token.startsWith("Nostr ");
eventJson = base64Decode(token.slice(6).trim());
event = JSON.parse(eventJson);

// 1) Kind + timestamp
assert(event.kind === 27235);
assert(Math.abs(now() - event.created_at) <= 60);

// 2) URL + method binding
assert(tag(event, "u") === req.absoluteUrl);           // exact match incl. query
assert(tag(event, "method") === req.method);

// 3) Payload binding (if present/required)
if (hasTag(event, "payload")) {
  assert(tag(event, "payload") === sha256Hex(req.bodyRaw));
}

// 4) Signature (Schnorr over secp256k1)
id = sha256(jsonEncode([0, event.pubkey, event.created_at, event.kind, event.tags, event.content]));
assert(verifySchnorr(event.sig, id, event.pubkey));

// OK → proceed as authenticated user pubkey

References

  • NIP-98: HTTP Auth — Spec (mirrors available)
  • NIP-01: Events & signatures — Spec
  • NIP-86: Relay Management API (uses NIP-98; requires payload) — Spec
  • Example server using NIP-98 auth — Nosdav

©2025 tomdwyer.co.uk