The BTC Map Admin now uses NIP-98 HTTP Authentication for secure, standardized Nostr-based login.
NIP-98 defines an ephemeral event (kind 27235) used to authorize HTTP requests using Nostr events. It's the standard way for HTTP services built for Nostr to authenticate users.
Users sign a kind 27235 event with:
kind: 27235 (NIP-98 HTTP Auth)content: Empty stringtags:["u", "<absolute-url>"]- The exact URL being accessed["method", "<http-method>"]- The HTTP method (POST)
created_at: Current Unix timestamppubkey: User's Nostr public keysig: Schnorr signature
Server validates the event by checking:
- ✅ Event ID: SHA256 hash of serialized event data matches
idfield - ✅ Signature Format: Valid 128-character hex Schnorr signature
- ✅ Kind: Must be exactly 27235
- ✅ Timestamp: Within 60-second window (prevents replay attacks)
- ✅ URL Match:
utag exactly matches request URL - ✅ Method Match:
methodtag matches HTTP method (POST) - ✅ Empty Content: Content field must be empty per spec
// 1. Build canonical login URL
const loginUrl = `${window.location.origin}/auth/nostr/login`;
// 2. Create NIP-98 event
const event = {
kind: 27235,
created_at: Math.floor(Date.now() / 1000),
tags: [
['u', loginUrl],
['method', 'POST']
],
content: '',
pubkey: await window.nostr.getPublicKey()
};
// 3. Sign with NIP-07 extension
const signedEvent = await window.nostr.signEvent(event);
// 4. Submit to server
await fetch('/auth/nostr/login', {
method: 'POST',
body: JSON.stringify({ event: signedEvent })
});# 1. Receive signed event
event_data = request.json.get('event')
# 2. Verify NIP-98 compliance
is_valid, error_msg = verify_nip98_event(
event_data,
request.url,
'POST',
max_age_seconds=60
)
# 3. Extract pubkey and create/load user
pubkey = event_data['pubkey']
user = User(pubkey)
login_user(user)Uses nostr-sdk (Rust bindings) for cryptographic verification:
Event.from_json()- Automatically performs full verification:- SHA256 event ID computation and validation
- Schnorr signature verification via secp256k1
- Raises exception on invalid ID or signature
verify_nip98_event()- NIP-98 specific validation:- Kind must be 27235
- Timestamp within window
- URL and method tags match
- Content is empty
get_event_pubkey()- Extract hex pubkey from verified event
- ✅ No shared secrets stored on server
- ✅ Cryptographic proof of identity
- ✅ Per-user authorization
- ✅ Browser extension security model
- ✅ Standardized (interoperable with other Nostr apps)
- ✅ Timestamp-based replay protection
- ✅ No server-side nonce storage needed
- ✅ Simpler implementation
- ✅ 60-second time window prevents stale events
- ✅ URL binding prevents auth token reuse
- ✅ Method binding prevents cross-endpoint attacks
- ✅ Empty content prevents content manipulation
Works with any NIP-07 compatible extension:
- ✅ Alby
- ✅ nos2x
- ✅ Flamingo
- ✅ Any extension implementing
window.nostr.signEvent()
Test the flow:
- Open http://localhost:5000/login
- Click "Sign in with Nostr"
- Extension prompts for signature of kind 27235 event
- Server validates event and creates session
- Redirects to profile or select_area
Verify event in extension:
- Check
kind: 27235 - Check
tagscontain correct URL and method - Check
contentis empty - Check timestamp is recent
✅ Full signature verification implemented using nostr-sdk (rust-nostr Python bindings)
The nostr-sdk library provides:
- Native Rust performance for cryptographic operations
- Full secp256k1 Schnorr signature verification
- NIP-01 event ID computation and validation
- Battle-tested implementation used across the Nostr ecosystem
- Support optional
payloadtag for POST body validation - Add Authorization header support (in addition to JSON body)
- Implement event caching to prevent duplicate verification
- Add rate limiting per pubkey