This document describes the authentication flow in the Nostr Auth Middleware. For a comprehensive understanding of how this fits into the larger system architecture, please refer to our Architecture Guide.
The authentication flow is implemented as a standalone security service, following our core architectural principles:
┌─────────────────┐
│ Client App │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Nostr Auth │◄── You are here
│ Service │
└────────┬────────┘
│
▼
┌─────────────────┐
│ App Platform │
└─────────────────┘
This isolation ensures:
- Clear security boundaries
- Auditable authentication code
- Protected application logic
- Scalable architecture
sequenceDiagram
participant C as Client App
participant A as Auth Service
participant P as App Platform
Note over C,P: Authentication Flow
C->>A: 1. Request Challenge
A->>A: 2. Generate Challenge
A->>C: 3. Return Challenge
C->>C: 4. Sign Challenge
C->>A: 5. Submit Signed Challenge
A->>A: 6. Verify Signature
A->>A: 7. Generate JWT
A->>C: 8. Return JWT
C->>P: 9. Use JWT with App Platform
Note over C,P: The App Platform remains independent
// Using nostr-tools in your frontend
import { getPublicKey, signEvent } from 'nostr-tools';
// Check if user has a Nostr extension (like nos2x or Alby)
const hasNostr = window.nostr !== undefined;
async function requestChallenge(pubkey) {
const response = await fetch('http://your-api/auth/nostr/challenge', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ pubkey }),
});
return await response.json();
}
async function signChallenge(challengeEvent) {
// The event will be signed by the user's Nostr extension
const signedEvent = await window.nostr.signEvent({
kind: 22242,
created_at: Math.floor(Date.now() / 1000),
tags: [
['challenge', challengeEvent.id],
],
content: `nostr:auth:${challengeEvent.id}`,
});
return signedEvent;
}
async function verifySignature(challengeId, signedEvent) {
const response = await fetch('http://your-api/auth/nostr/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
challengeId,
signedEvent,
}),
});
return await response.json();
}
async function loginWithNostr() {
try {
// Get user's public key
const pubkey = await window.nostr.getPublicKey();
// Request challenge
const { event: challengeEvent, challengeId } = await requestChallenge(pubkey);
// Sign challenge
const signedEvent = await signChallenge(challengeEvent);
// Verify signature and get JWT token
const { token, profile } = await verifySignature(challengeId, signedEvent);
// Store token for future requests
localStorage.setItem('authToken', token);
// Use token in subsequent API calls
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
};
return { token, profile };
} catch (error) {
console.error('Login failed:', error);
throw error;
}
}
-
Challenge Generation
- Validates incoming pubkey
- Creates a unique challenge event
- Signs it with server's private key
- Stores challenge temporarily
-
Signature Verification
- Validates challenge existence and expiry
- Verifies event signature
- Checks event content and tags
-
Token Generation
- Creates JWT token with user's pubkey
- Configurable expiration time
- Optional: Stores user profile in Supabase
-
Private Key Management
- Development: Uses environment variables
- Production: Stored securely in Supabase
- Never expose private keys in client-side code
-
Challenge Expiry
- Challenges expire after 5 minutes
- Each challenge can only be used once
- Prevents replay attacks
-
JWT Token Security
- Short expiration time (configurable)
- Contains minimal user data
- Should be transmitted over HTTPS only
-
User Profile Storage
-- Example Supabase table structure create table public.profiles ( id uuid primary key default uuid_generate_v4(), pubkey text unique not null, name text, about text, picture text, enrolled_at timestamp with time zone default now() );
-
Session Management
- JWT tokens can be validated against Supabase
- User profiles are automatically created/updated
- Supports multiple login methods per user
-
Common Errors
- No Nostr extension found
- Challenge expired
- Invalid signature
- Network issues
-
Error Responses
{ success: false, message: 'Detailed error message', code: 'ERROR_CODE' }
-
Frontend
- Always check for Nostr extension availability
- Handle network errors gracefully
- Implement token refresh mechanism
- Store tokens securely
-
Backend
- Use proper CORS configuration
- Implement rate limiting
- Monitor failed authentication attempts
- Regular security audits
The middleware includes test scripts to verify:
- Challenge generation
- Signature verification
- Token generation
- Profile management
Run tests using:
npm run test:live