17 KiB
NOSTR_LOGIN_LITE - Login Logic Analysis
This document explains the complete login and authentication flow for the NOSTR_LOGIN_LITE library, including how state is maintained upon page refresh.
System Architecture Overview
The library uses a modular authentication architecture with these key components:
- FloatingTab - UI component for login trigger and status display
- Modal - UI component for authentication method selection
- NostrLite - Main library coordinator and facade manager
- WindowNostr - NIP-07 compliant facade for non-extension methods
- AuthManager - Persistent state management with encryption
- Extension Bridge - Browser extension detection and management
Authentication Flow Diagrams
Initial Page Load Flow
┌─────────────────────┐
│ Page Loads │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ NOSTR_LOGIN_LITE │
│ .init() called │
└─────────┬───────────┘
│
▼
┌─────────────────────┐ YES ┌─────────────────────┐
│ Real extension │──────────▶│ Extension-First │
│ detected? │ │ Mode: Don't install │
└─────────┬───────────┘ │ facade │
│ NO └─────────────────────┘
▼
┌─────────────────────┐
│ Install WindowNostr │
│ facade for local/ │
│ NIP-46/readonly │
└─────────┬───────────┘
│
▼
┌─────────────────────┐ YES ┌─────────────────────┐
│ Persistence │──────────▶│ _attemptAuthRestore │
│ enabled? │ │ called │
└─────────┬───────────┘ └─────────┬───────────┘
│ NO │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Initialization │ │ Check storage for │
│ complete │ │ saved auth state │
└─────────────────────┘ └─────────┬───────────┘
│
▼
┌─────────────────────┐ YES
│ Valid auth state │────────┐
│ found? │ │
└─────────┬───────────┘ │
│ NO │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Show login UI │ │ Restore auth & │
│ (FloatingTab,etc) │ │ dispatch events │
└─────────────────────┘ └─────────────────────┘
User-Initiated Login Flow
┌─────────────────────┐ ┌─────────────────────┐
│ User clicks │ │ User clicks │
│ FloatingTab │ │ Login Button │
└─────────┬───────────┘ └─────────┬───────────┘
│ │
▼ ▼
┌─────────────────────┐ │
│ Extension │ │
│ available? │ │
└─────────┬───────────┘ │
│ YES │
▼ │
┌─────────────────────┐ │
│ Auto-try extension │ │
│ authentication │ │
└─────────┬───────────┘ │
│ SUCCESS │
▼ │
┌─────────────────────┐ │
│ Authentication │ │
│ complete │◀──────────────────┘
└─────────────────────┘ │ FAIL OR ALWAYS
▼
┌─────────────────────┐
│ Open Modal with │
│ method selection: │
│ • Extension │
│ • Local Key │
│ • NIP-46 Connect │
│ • Read-only │
│ • OTP/DM │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ User selects method │
│ and completes auth │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ Authentication │
│ complete │
└─────────────────────┘
Authentication Storage & Persistence Flow
┌─────────────────────┐
│ Authentication │
│ successful │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ nlMethodSelected │
│ event dispatched │
└─────────┬───────────┘
│
▼
┌─────────────────────┐ Extension? ┌─────────────────────┐
│ AuthManager. │─────────────────▶│ Store verification │
│ saveAuthState() │ │ data only (no │
└─────────┬───────────┘ │ secrets) │
│ Local Key? └─────────────────────┘
▼
┌─────────────────────┐
│ Encrypt secret key │
│ with session │
│ password + AES-GCM │
└─────────┬───────────┘
│ NIP-46?
▼
┌─────────────────────┐
│ Store connection │
│ parameters (no │
│ secrets) │
└─────────┬───────────┘
│ Read-only?
▼
┌─────────────────────┐
│ Store method only │
│ (no secrets) │
└─────────┬───────────┘
│
▼
┌─────────────────────┐ isolateSession? ┌─────────────────────┐
│ Choose storage: │─────────YES─────────▶│ sessionStorage │
│ localStorage vs │ │ (per-window) │
│ sessionStorage │◀────────NO───────────┤ │
└─────────┬───────────┘ └─────────────────────┘
│
▼
┌─────────────────────┐
│ localStorage │
│ (cross-window) │
└─────────────────────┘
Key Decision Points and Logic
1. Extension Detection Logic (Line 994-1046)
Function: NostrLite._isRealExtension(obj)
// Conservative extension detection
if (!obj || typeof obj !== 'object') return false;
if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') return false;
// Exclude our own classes
const constructorName = obj.constructor?.name;
if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') return false;
if (obj === window.NostrTools) return false;
// Look for extension indicators
const extensionIndicators = [
'_isEnabled', 'enabled', 'kind', '_eventEmitter', '_scope',
'_requests', '_pubkey', 'name', 'version', 'description'
];
const hasIndicators = extensionIndicators.some(prop => obj.hasOwnProperty(prop));
const hasExtensionConstructor = constructorName &&
constructorName !== 'Object' &&
constructorName !== 'Function';
return hasIndicators || hasExtensionConstructor;
2. Facade Installation Decision (Line 942-972)
Function: NostrLite._setupWindowNostrFacade()
Extension detected? ──YES──▶ DON'T install facade
Store reference for persistence
│
NO
▼
Install WindowNostr facade ──▶ Handle local/NIP-46/readonly methods
3. FloatingTab Click Behavior (Line 351-369)
Current UX Inconsistency Issue:
async _handleClick() {
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
this._showUserMenu(); // Show user options
} else {
// INCONSISTENCY: Auto-tries extension instead of opening modal
if (window.nostr && this._isRealExtension(window.nostr)) {
await this._tryExtensionLogin(window.nostr); // Automatic extension attempt
} else {
if (this.modal) {
this.modal.open({ startScreen: 'login' }); // Fallback to modal
}
}
}
}
Comparison with Login Button behavior:
- Login Button: Always opens modal for user choice
- FloatingTab: Auto-tries extension first, only shows modal if denied
4. Authentication Restoration on Page Refresh
Two-Path System:
Path 1: Extension Mode (Line 1115-1173)
async _attemptExtensionRestore() {
const authManager = new AuthManager({ isolateSession: this.options?.isolateSession });
const storedAuth = await authManager.restoreAuthState();
if (!storedAuth || storedAuth.method !== 'extension') return null;
// Verify extension still works with same pubkey
if (!window.nostr || !this._isRealExtension(window.nostr)) return null;
const currentPubkey = await window.nostr.getPublicKey();
if (currentPubkey !== storedAuth.pubkey) return null;
// Dispatch nlAuthRestored event for UI updates
window.dispatchEvent(new CustomEvent('nlAuthRestored', { detail: extensionAuth }));
}
Path 2: Non-Extension Mode (Line 1080-1098)
// Uses facade's restoreAuthState method
if (this.facadeInstalled && window.nostr?.restoreAuthState) {
const restoredAuth = await window.nostr.restoreAuthState();
if (restoredAuth) {
// Handle NIP-46 reconnection if needed
if (restoredAuth.requiresReconnection) {
this._showReconnectionPrompt(restoredAuth);
}
}
}
5. Storage Strategy (Line 1408-1414)
Storage Type Selection:
if (options.isolateSession) {
this.storage = sessionStorage; // Per-window isolation
} else {
this.storage = localStorage; // Cross-window persistence
}
6. Event-Driven State Synchronization
Key Events:
nlMethodSelected- Dispatched when user completes authenticationnlAuthRestored- Dispatched when authentication is restored from storagenlLogout- Dispatched when user logs outnlReconnectionRequired- Dispatched when NIP-46 needs reconnection
Event Listeners:
- FloatingTab listens to all auth events for UI updates (Line 271-295)
- WindowNostr listens to nlMethodSelected/nlLogout for state management (Line 823-869)
State Persistence Security Model
By Authentication Method:
Extension:
- ✅ Store: pubkey, verification metadata
- ❌ Never store: extension object, secrets
- 🔒 Security: Minimal data, 1-hour expiry
Local Key:
- ✅ Store: encrypted secret key, pubkey
- 🔒 Security: AES-GCM encryption with session-specific password
- 🔑 Session password stored in sessionStorage (cleared on tab close)
NIP-46:
- ✅ Store: connection parameters, pubkey
- ❌ Never store: session secrets
- 🔄 Requires: User reconnection on restore
Read-only:
- ✅ Store: method type, pubkey
- ❌ No secrets to store
Current Issues Identified
UX Inconsistency (THE MAIN ISSUE)
Problem: FloatingTab and Login Button have different click behaviors
- FloatingTab: Auto-tries extension → Falls back to modal if denied
- Login Button: Always opens modal for user choice
Impact:
- Confusing user experience
- Inconsistent interaction patterns
- Users don't get consistent choice of authentication method
Root Cause: Line 358-367 in FloatingTab._handleClick() method
Proposed Solutions:
Option 1: Make FloatingTab Consistent (Recommended)
async _handleClick() {
if (this.isAuthenticated && this.options.behavior.showUserInfo) {
this._showUserMenu();
} else {
// Always open modal - consistent with login button
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
}
}
Option 2: Add Configuration Option
floatingTab: {
behavior: {
autoTryExtension: false, // Default to consistent behavior
// ... other options
}
}
⚠️ IMPLEMENTATION STATUS: READY FOR CODE CHANGES
User Decision: FloatingTab should behave exactly like login buttons - always open modal for authentication method selection.
Required Changes:
- File:
lite/build.js - Method:
FloatingTab._handleClick()(lines 351-369) - Action: Remove extension auto-detection, always open modal
Current Code to Replace (lines 358-367):
// Check if extension is available for direct login
if (window.nostr && this._isRealExtension(window.nostr)) {
console.log('FloatingTab: Extension available, attempting direct extension login');
await this._tryExtensionLogin(window.nostr);
} else {
// Open login modal
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
}
Replacement Code:
// Always open login modal (consistent with login buttons)
if (this.modal) {
this.modal.open({ startScreen: 'login' });
}
Critical Safety Notes:
- ✅ DO NOT change
_checkExistingAuth()method (lines 299-349) - this handles automatic restoration on page refresh - ✅ ONLY change the click handler to remove manual extension detection
- ✅ Authentication restoration will continue to work properly via the separate restoration system
- ✅ Extension detection logic remains intact for other purposes (storage, verification, etc.)
After Implementation:
- Rebuild the library with
node lite/build.js - Test that both floating tab and login buttons behave identically
- Verify that automatic login restoration on page refresh still works properly
Important Notes
- Extension-First Architecture: The system never interferes with real browser extensions
- Dual Storage Support: Supports both per-window (sessionStorage) and cross-window (localStorage) persistence
- Security-First: Sensitive data is always encrypted or not stored
- Event-Driven: All components communicate via custom events
- Automatic Restoration: Authentication state is automatically restored on page refresh when possible
The login logic is complex due to supporting multiple authentication methods, security requirements, and different storage strategies, but it provides a flexible and secure authentication system for Nostr applications.