Authentication of clients to relays
NIP-42 enables relays to authenticate clients using signed AUTH events (kind 22242), supporting paid relays, private access, and identity verification.
NIP-42: Authentication of clients to relays
Status: Draft Authors: fiatjaf, Kukks, arthurfranca Category: Relay Features
Overview
NIP-42 defines how relays can authenticate clients using a challenge-response mechanism, verifying user identity before granting access.
Core Concept:
- Relay sends AUTH challenge to client
- Client signs challenge with private key
- Relay verifies signature and grants access
- Enables paid relays, private access, rate limiting
Authentication Flow:
- Client connects to relay
- Relay sends AUTH challenge
- Client signs and returns AUTH event (kind 22242)
- Relay verifies and authorizes client
Use Cases:
- ✅ Paid relays: Verify subscription status
- ✅ Private relays: Restrict access to members
- ✅ Rate limiting: Different limits per user
- ✅ Access control: Whitelist/blacklist management
- ✅ Analytics: Track authenticated usage
- ✅ Compliance: Identity verification when required
Status: Draft (implemented by major relay software)
Why Authentication Matters
Without NIP-42
Problems before authentication:
- No way to verify client identity
- Can’t implement paid relay subscriptions
- Difficult to enforce access controls
- Rate limiting applies to all clients equally
- No protection against anonymous abuse
With NIP-42
Benefits:
- Identity verification: Know who’s connecting
- Paid relays: Verify subscriptions before access
- Access control: Private/member-only relays
- Fair rate limiting: Per-user quotas
- Abuse prevention: Ban specific users, not IPs
- Analytics: Understand authenticated usage
Use Cases
- Paid Relay Services: Verify payment before granting access
- Private Communities: Members-only relay access
- Rate Limiting: Higher limits for verified users
- Spam Prevention: Authenticate before posting
- Compliance: Verify identity for regulated content
- Premium Features: Extra capabilities for authenticated users
How It Works
Challenge-Response Flow
Client Relay
| |
|--- WebSocket Connect -------->|
| |
|<--- AUTH Challenge (JSON) ----|
| ["AUTH", "<challenge>"] |
| |
|--- AUTH Event (signed) ------>|
| ["AUTH", {...event}] |
| |
|<--- OK (acceptance) -----------|
| ["OK", "...", true, ""] |
| |
|<--- Relay now accepts reqs -->|
AUTH Challenge Format
Relay sends challenge to client:
["AUTH", "<challenge-string>"]
Challenge string:
- Random, unique per connection
- Used once (prevents replay attacks)
- No specific format required (relay decides)
Example:
["AUTH", "d5f8a9c2-3b1e-4f2d-9a7c-8e3f1d4b2a6c"]
AUTH Event Structure
Client responds with kind 22242 event:
{
"id": "event-id",
"pubkey": "client-pubkey",
"created_at": 1673347337,
"kind": 22242,
"tags": [
["relay", "wss://relay.example.com"],
["challenge", "d5f8a9c2-3b1e-4f2d-9a7c-8e3f1d4b2a6c"]
],
"content": "",
"sig": "signature..."
}
Required Tags:
relay- URL of the relay being authenticated tochallenge- Challenge string sent by relay
Client Implementation
Basic Authentication
import { getPublicKey, signEvent } from 'nostr-tools';
class AuthenticatedClient {
constructor(relayUrl, privateKey) {
this.relayUrl = relayUrl;
this.privateKey = privateKey;
this.pubkey = getPublicKey(privateKey);
this.ws = null;
this.authenticated = false;
}
async connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.relayUrl);
this.ws.onopen = () => {
console.log("Connected to relay, waiting for AUTH challenge...");
};
this.ws.onmessage = async (event) => {
const message = JSON.parse(event.data);
if (message[0] === "AUTH") {
await this.handleAuthChallenge(message[1]);
} else if (message[0] === "OK" && !this.authenticated) {
this.authenticated = true;
console.log("Authentication successful!");
resolve();
}
};
this.ws.onerror = (error) => {
reject(error);
};
});
}
async handleAuthChallenge(challenge) {
console.log("Received AUTH challenge:", challenge);
// Create AUTH event
const authEvent = {
kind: 22242,
created_at: Math.floor(Date.now() / 1000),
tags: [
["relay", this.relayUrl],
["challenge", challenge]
],
content: "",
pubkey: this.pubkey
};
// Sign event
const signedAuth = signEvent(authEvent, this.privateKey);
// Send AUTH response
this.ws.send(JSON.stringify(["AUTH", signedAuth]));
console.log("Sent AUTH response");
}
async publish(event) {
if (!this.authenticated) {
throw new Error("Not authenticated");
}
this.ws.send(JSON.stringify(["EVENT", event]));
}
async subscribe(filters) {
if (!this.authenticated) {
throw new Error("Not authenticated");
}
const subId = Math.random().toString(36).substring(7);
this.ws.send(JSON.stringify(["REQ", subId, ...filters]));
return subId;
}
}
// Usage
const client = new AuthenticatedClient(
"wss://paid-relay.example.com",
myPrivateKey
);
await client.connect();
console.log("Ready to publish and subscribe!");
await client.publish(myEvent);
Automatic Retry on Auth Failure
class RobustAuthClient extends AuthenticatedClient {
constructor(relayUrl, privateKey, maxRetries = 3) {
super(relayUrl, privateKey);
this.maxRetries = maxRetries;
this.retryCount = 0;
}
async connect() {
try {
await super.connect();
this.retryCount = 0; // Reset on success
} catch (error) {
if (this.retryCount < this.maxRetries) {
this.retryCount++;
console.log(`Auth failed, retrying (${this.retryCount}/${this.maxRetries})...`);
await new Promise(resolve => setTimeout(resolve, 1000 * this.retryCount));
return this.connect();
}
throw new Error(`Authentication failed after ${this.maxRetries} attempts`);
}
}
}
Relay Implementation
Challenge Generation
import { randomBytes } from 'crypto';
function generateChallenge() {
return randomBytes(32).toString('hex');
}
// Store active challenges (with expiration)
const activeChallenge = new Map();
function createChallenge(wsConnectionId) {
const challenge = generateChallenge();
activeChallenges.set(wsConnectionId, {
challenge,
createdAt: Date.now(),
expiresAt: Date.now() + (5 * 60 * 1000) // 5 minutes
});
return challenge;
}
Challenge Verification
import { verifySignature } from 'nostr-tools';
async function verifyAuthEvent(authEvent, wsConnectionId, relayUrl) {
// 1. Verify event structure
if (authEvent.kind !== 22242) {
return { valid: false, error: "Invalid event kind" };
}
// 2. Extract tags
const relayTag = authEvent.tags.find(t => t[0] === "relay");
const challengeTag = authEvent.tags.find(t => t[0] === "challenge");
if (!relayTag || !challengeTag) {
return { valid: false, error: "Missing required tags" };
}
// 3. Verify relay URL matches
if (relayTag[1] !== relayUrl) {
return { valid: false, error: "Relay URL mismatch" };
}
// 4. Verify challenge
const challengeData = activeChallenges.get(wsConnectionId);
if (!challengeData) {
return { valid: false, error: "No challenge found for connection" };
}
if (challengeData.challenge !== challengeTag[1]) {
return { valid: false, error: "Challenge mismatch" };
}
// 5. Check expiration
if (Date.now() > challengeData.expiresAt) {
activeChallenges.delete(wsConnectionId);
return { valid: false, error: "Challenge expired" };
}
// 6. Verify timestamp freshness
const eventAge = Date.now() / 1000 - authEvent.created_at;
if (eventAge > 300) { // 5 minutes
return { valid: false, error: "Event too old" };
}
// 7. Verify signature
if (!verifySignature(authEvent)) {
return { valid: false, error: "Invalid signature" };
}
// 8. Cleanup used challenge
activeChallenges.delete(wsConnectionId);
return { valid: true, pubkey: authEvent.pubkey };
}
Access Control
class RelayWithAuth {
constructor() {
this.authenticatedClients = new Map();
this.allowedPubkeys = new Set(); // Whitelist
}
async handleConnection(ws, connectionId) {
// Send AUTH challenge
const challenge = createChallenge(connectionId);
ws.send(JSON.stringify(["AUTH", challenge]));
// Wait for AUTH response
ws.on('message', async (data) => {
const message = JSON.parse(data);
if (message[0] === "AUTH") {
await this.handleAuthResponse(ws, message[1], connectionId);
} else {
// Check if authenticated before processing other messages
if (!this.isAuthenticated(connectionId)) {
ws.send(JSON.stringify([
"NOTICE",
"Authentication required. Please send AUTH event."
]));
return;
}
// Process authenticated request
this.handleAuthenticatedMessage(ws, message, connectionId);
}
});
}
async handleAuthResponse(ws, authEvent, connectionId) {
const result = await verifyAuthEvent(authEvent, connectionId, this.relayUrl);
if (result.valid) {
// Check if user is allowed
if (this.allowedPubkeys.size > 0 && !this.allowedPubkeys.has(result.pubkey)) {
ws.send(JSON.stringify([
"OK",
authEvent.id,
false,
"restricted: pubkey not authorized"
]));
ws.close();
return;
}
// Mark as authenticated
this.authenticatedClients.set(connectionId, {
pubkey: result.pubkey,
authenticatedAt: Date.now()
});
ws.send(JSON.stringify([
"OK",
authEvent.id,
true,
""
]));
console.log(`Client authenticated: ${result.pubkey}`);
} else {
ws.send(JSON.stringify([
"OK",
authEvent.id,
false,
`auth-required: ${result.error}`
]));
}
}
isAuthenticated(connectionId) {
return this.authenticatedClients.has(connectionId);
}
getAuthenticatedPubkey(connectionId) {
return this.authenticatedClients.get(connectionId)?.pubkey;
}
}
Advanced Use Cases
Paid Relay Verification
class PaidRelay extends RelayWithAuth {
constructor(subscriptionChecker) {
super();
this.subscriptionChecker = subscriptionChecker;
}
async handleAuthResponse(ws, authEvent, connectionId) {
const result = await verifyAuthEvent(authEvent, connectionId, this.relayUrl);
if (!result.valid) {
ws.send(JSON.stringify([
"OK",
authEvent.id,
false,
`auth-required: ${result.error}`
]));
return;
}
// Check subscription status
const hasSubscription = await this.subscriptionChecker.verify(result.pubkey);
if (!hasSubscription) {
ws.send(JSON.stringify([
"OK",
authEvent.id,
false,
"payment-required: Valid subscription required. Visit https://relay.example.com/subscribe"
]));
ws.close();
return;
}
// Grant access
this.authenticatedClients.set(connectionId, {
pubkey: result.pubkey,
authenticatedAt: Date.now(),
subscriptionTier: await this.subscriptionChecker.getTier(result.pubkey)
});
ws.send(JSON.stringify([
"OK",
authEvent.id,
true,
""
]));
}
}
Per-User Rate Limiting
class RateLimitedRelay extends RelayWithAuth {
constructor() {
super();
this.userLimits = new Map();
}
checkRateLimit(pubkey) {
const now = Date.now();
const userLimit = this.userLimits.get(pubkey) || {
requests: 0,
windowStart: now
};
// Reset window if 1 minute has passed
if (now - userLimit.windowStart > 60000) {
userLimit.requests = 0;
userLimit.windowStart = now;
}
// Check limit (e.g., 100 requests per minute)
if (userLimit.requests >= 100) {
return false;
}
userLimit.requests++;
this.userLimits.set(pubkey, userLimit);
return true;
}
handleAuthenticatedMessage(ws, message, connectionId) {
const pubkey = this.getAuthenticatedPubkey(connectionId);
if (!this.checkRateLimit(pubkey)) {
ws.send(JSON.stringify([
"NOTICE",
"rate-limited: Too many requests. Please slow down."
]));
return;
}
// Process message
super.handleAuthenticatedMessage(ws, message, connectionId);
}
}
Security Considerations
Challenge Security
// ✅ Good: Cryptographically secure random
const challenge = randomBytes(32).toString('hex');
// ❌ Bad: Predictable challenge
const challenge = Date.now().toString();
Timestamp Validation
function isTimestampFresh(timestamp, maxAge = 300) {
const now = Math.floor(Date.now() / 1000);
const age = now - timestamp;
// Check if timestamp is recent
if (age > maxAge) {
return false;
}
// Check if timestamp is not in future (clock skew tolerance)
if (age < -60) {
return false;
}
return true;
}
Replay Attack Prevention
class ReplayProtection {
constructor(ttl = 5 * 60 * 1000) {
this.usedChallenges = new Map();
this.ttl = ttl;
}
markUsed(challenge) {
this.usedChallenges.set(challenge, Date.now());
this.cleanup();
}
isUsed(challenge) {
return this.usedChallenges.has(challenge);
}
cleanup() {
const now = Date.now();
for (const [challenge, timestamp] of this.usedChallenges) {
if (now - timestamp > this.ttl) {
this.usedChallenges.delete(challenge);
}
}
}
}
Client Support
Relay Software with NIP-42
- nostream - Full NIP-42 implementation
- strfry - Plugin support for authentication
- nostr-rs-relay - Auth module available
- rnostr - Built-in authentication
Client Library Support
| Library | NIP-42 Support | Auto-Auth | Retry Logic |
|---|---|---|---|
| nostr-tools | ⚠️ Manual | ❌ | ❌ |
| NDK | ✅ | ✅ | ✅ |
| nostr-sdk (Rust) | ✅ | ✅ | ✅ |
| pynostr | ⚠️ Manual | ❌ | ❌ |
Check library documentation for implementation details.
Common Questions
Is authentication mandatory?
No. Relays can choose whether to require authentication. Public relays often don’t require it.
Can I connect anonymously?
Depends on the relay. Some relays require authentication, others allow anonymous connections with limited features.
What if I don’t respond to AUTH challenge?
Relay may close connection or allow limited read-only access. Behavior is relay-specific.
Can relays see my private key?
No! You only send signed events. Your private key never leaves your client.
How often do I need to authenticate?
Once per connection. If you disconnect and reconnect, you’ll receive a new challenge.
Can multiple clients use the same identity?
Yes. Each client authenticates independently with the relay.
Related NIPs
- NIP-01 - Basic protocol (event structure, signatures)
- NIP-11 - Relay information (advertising auth requirements)
- NIP-65 - Relay lists (choosing authenticated relays)
Technical Specification
For the complete technical specification, see NIP-42 on GitHub.
Summary
NIP-42 enables relay authentication on Nostr:
✅ Challenge-response mechanism ✅ Kind 22242 AUTH events ✅ Signed authentication with private key ✅ Use cases: Paid relays, access control, rate limiting ✅ Replay protection via unique challenges
AUTH flow:
1. Relay sends: ["AUTH", "<challenge>"]
2. Client responds: ["AUTH", {kind: 22242, tags: [["relay", "..."], ["challenge", "..."]], ...}]
3. Relay verifies signature and grants access
AUTH event structure:
{
"kind": 22242,
"tags": [
["relay", "wss://relay.example.com"],
["challenge", "d5f8a9c2..."]
],
"content": ""
}
Status: Draft - implemented by major relay software.
Best practice: Use secure random challenges, validate timestamps, prevent replay attacks.
Next Steps:
- Learn about relay info in NIP-11
- Explore relay lists in NIP-65
- Understand event kinds in NIP-01
- Browse all NIPs in our reference
Last updated: January 2024 Official specification: GitHub
Client Support
This NIP is supported by the following clients: