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
Authorizationheader. - 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
- Build event with
kind=27235, tags u, method, and optionally payload. - Compute event id:
sha256(JSON.encode([0, pubkey, created_at, kind, tags, content])). - Sign
idwith Schnorr over secp256k1 (per NIP-01). Put signature insig. - Base64-encode the whole JSON event and send
Authorization: Nostr ....
Server validation checklist
- Kind: must be
27235. - Time window: reject stale / future events (e.g. ±60s).
- URL: u equals the absolute request URL (including query).
- Method: method matches the HTTP verb actually used.
- Payload (if present or required): hash matches the request body.
- Signature: verify Schnorr signature of
idagainstpubkey. - 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_atwindow; 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
401on 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