# Browser Authentication Guide
The browser authentication module provides lightweight client-side authentication flows for web applications. Two protocols are supported:
- **NIP-07** (`NostrBrowserAuth`) — authenticates via browser extensions like nos2x, Alby, or NostrKey
- **NIP-46** (`Nip46AuthHandler`) — authenticates via remote signers (bunkers) over relays
## Features
- NIP-07 compliant authentication via `window.nostr`
- NIP-46 remote signer authentication via kind 24133 events
- Transport-agnostic NIP-46 — you provide relay I/O
- Challenge-response based security
- Session management
- TypeScript support
- Configurable event kinds and timeouts
## Installation
```bash
npm install @humanjavaenterprises/nostr-auth-middleware
```
## Basic Usage
```javascript
import { NostrBrowserAuth } from '@humanjavaenterprises/nostr-auth-middleware';
const auth = new NostrBrowserAuth();
// Authenticate user
async function login() {
try {
const result = await auth.authenticate();
console.log('Authenticated as:', result.pubkey);
return result;
} catch (error) {
console.error('Authentication failed:', error);
throw error;
}
}
```
## Configuration
The `NostrBrowserAuth` constructor accepts an optional configuration object:
```javascript
const auth = new NostrBrowserAuth({
customKind: 22242, // Custom event kind for authentication (default: 22242)
clientName: 'my-app' // Client name for challenge generation (default: 'nostr-auth')
});
```
## Authentication Flow
1. **Public Key Request**
- Calls `window.nostr.getPublicKey()`
- Triggers the extension's permission popup for read access
- Returns the user's public key
2. **Challenge Creation**
- Generates a unique challenge string
- Includes timestamp and client name
- Prevents replay attacks
3. **Challenge Signing**
- Creates a Nostr event with the challenge
- Requests the user to sign it via their extension
- Triggers the extension's permission popup for write access
4. **Session Management**
- Returns authentication result with pubkey and signed event
- Provides session validation method
## Session Management
```javascript
// Store session after authentication
const session = {
pubkey: result.pubkey,
timestamp: result.timestamp
};
localStorage.setItem('nostrSession', JSON.stringify(session));
// Validate session later
const isValid = await auth.validateSession(session);
```
## Error Handling
The authentication process may throw errors in these cases:
- Nostr extension not found
- User denied permissions
- Network errors
- Invalid signatures
Example error handling:
```javascript
try {
await auth.authenticate();
} catch (error) {
if (error.message.includes('extension not found')) {
// Prompt user to install a Nostr extension
} else if (error.message.includes('denied')) {
// Handle permission denial
} else {
// Handle other errors
}
}
```
## Security Considerations
1. **Challenge Uniqueness**
- Each authentication attempt uses a unique challenge
- Includes timestamps to prevent replay attacks
- Uses client-specific prefixes
2. **Permission Scoping**
- Read permission: Only requests public key access
- Write permission: Only requests event signing for authentication
- No additional permissions requested
3. **Session Validation**
- Validates current public key matches session
- Checks for extension availability
- Safe error handling
## TypeScript Support
The module includes full TypeScript definitions:
```typescript
interface NostrBrowserConfig {
customKind?: number;
clientName?: string;
}
interface AuthResult {
pubkey: string;
timestamp: number;
challenge: string;
signedEvent: NostrEvent;
}
```
## Examples
### Complete Authentication Flow
```javascript
import { NostrBrowserAuth } from '@humanjavaenterprises/nostr-auth-middleware';
class AuthService {
constructor() {
this.auth = new NostrBrowserAuth({
clientName: 'my-app'
});
this.currentUser = null;
}
async login() {
try {
const result = await this.auth.authenticate();
this.currentUser = {
pubkey: result.pubkey,
timestamp: result.timestamp
};
this.saveSession();
return true;
} catch (error) {
console.error('Login failed:', error);
throw error;
}
}
async validateAndRestoreSession() {
const savedSession = localStorage.getItem('nostrSession');
if (savedSession) {
try {
const session = JSON.parse(savedSession);
const isValid = await this.auth.validateSession(session);
if (isValid) {
this.currentUser = session;
return true;
}
} catch (error) {
console.error('Session restoration failed:', error);
}
}
return false;
}
saveSession() {
if (this.currentUser) {
localStorage.setItem('nostrSession', JSON.stringify(this.currentUser));
}
}
logout() {
this.currentUser = null;
localStorage.removeItem('nostrSession');
}
}
```
### Vue.js Integration Example
```javascript
// auth.js
import { NostrBrowserAuth } from '@humanjavaenterprises/nostr-auth-middleware';
export const auth = new NostrBrowserAuth({
clientName: 'vue-app'
});
// App.vue
Welcome, {{ pubkey }}
```
---
## NIP-46 Remote Signer Authentication
For apps that authenticate users via NIP-46 bunkers (remote signers) instead of browser extensions. This is useful when:
- The user's keys are managed by a separate app (e.g., NostrKey browser plugin acting as a bunker)
- You need to support mobile or cross-device authentication
- The signing app runs in a different context than the web app
### Installation
```bash
npm install nostr-auth-middleware
```
### Basic Usage
```typescript
import { Nip46AuthHandler } from 'nostr-auth-middleware/browser';
const auth = new Nip46AuthHandler({
bunkerUri: 'bunker://?relay=wss://relay.example.com&secret=mysecret',
serverUrl: 'https://auth.example.com',
});
```
### Transport Interface
NIP-46 requires relay communication. You provide the transport — the handler doesn't own any WebSocket connections. This keeps it compatible with any relay library.
```typescript
interface Nip46Transport {
/** Publish a signed kind 24133 event to relays */
sendEvent(event: SignedNostrEvent): Promise;
/** Subscribe to events matching a filter. Returns a cleanup function. */
subscribe(
filter: { kinds: number[]; '#p': string[]; since?: number },
onEvent: (event: SignedNostrEvent) => void
): () => void;
}
```
Example with a hypothetical relay library:
```typescript
auth.setTransport({
sendEvent: async (event) => {
await pool.publish(['wss://relay.example.com'], event);
},
subscribe: (filter, onEvent) => {
const sub = pool.subscribe(['wss://relay.example.com'], [filter]);
sub.on('event', onEvent);
return () => sub.close();
},
});
```
### Configuration
```typescript
const auth = new Nip46AuthHandler({
// Option 1: bunker:// URI (recommended)
bunkerUri: 'bunker://?relay=wss://relay.example.com&secret=...',
// Option 2: direct config
remotePubkey: '',
relays: ['wss://relay.example.com'],
secret: 'connection-secret',
// Common options
serverUrl: 'https://auth.example.com', // Required for authenticate()
timeout: 30000, // Remote signer response timeout (ms)
customKind: 22242, // Event kind for challenge events
permissions: 'sign_event,get_public_key', // Requested permissions
});
```
### Authentication Flow
1. **Connect** — Creates an ephemeral keypair, sends a `connect` request to the bunker, waits for `ack`
2. **Get Public Key** — Asks the bunker for the user's identity pubkey
3. **Fetch Challenge** — Requests a challenge from your auth server
4. **Sign Challenge** — Asks the bunker to sign the challenge event
5. **Verify** — Submits the signed event to your auth server for JWT
```typescript
// Full flow
auth.setTransport(myTransport);
await auth.connect();
const result = await auth.authenticate();
// result: { pubkey, signedEvent, sessionInfo, timestamp }
```
### Session Management
```typescript
// Check if the remote signer is still reachable
const isAlive = await auth.validateSession();
// Get session info
const info = auth.getSessionInfo();
// { clientPubkey: '...', remotePubkey: '...' } or null
// Clean up
auth.destroy();
```
### Error Handling
```typescript
try {
await auth.connect(transport);
const result = await auth.authenticate();
} catch (error) {
if (error.message.includes('timed out')) {
// Remote signer didn't respond within timeout
} else if (error.message.includes('Connect failed')) {
// Bunker rejected the connection (bad secret, etc.)
} else if (error.message.includes('Transport is required')) {
// Forgot to set transport before connecting
} else if (error.message.includes('Not connected')) {
// Called authenticate() before connect()
}
}
```
### Choosing Between NIP-07 and NIP-46
| | NIP-07 (`NostrBrowserAuth`) | NIP-46 (`Nip46AuthHandler`) |
|---|---|---|
| **How it works** | Calls `window.nostr` directly | Sends encrypted messages via relays |
| **User experience** | Extension popup for each action | Approve in remote signer app |
| **Key location** | In the browser extension | In the bunker (can be anywhere) |
| **Cross-device** | No — same browser only | Yes — any device with relay access |
| **Transport** | None needed | You provide relay I/O |
| **Best for** | Simple web apps | Apps needing remote/mobile signing |
### React Example with NIP-46
```typescript
import { Nip46AuthHandler } from 'nostr-auth-middleware/browser';
import { useState } from 'react';
function BunkerLogin({ bunkerUri, serverUrl, transport }) {
const [pubkey, setPubkey] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
async function login() {
setLoading(true);
setError(null);
try {
const auth = new Nip46AuthHandler({ bunkerUri, serverUrl });
auth.setTransport(transport);
await auth.connect();
const result = await auth.authenticate();
setPubkey(result.pubkey);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
if (pubkey) return