Single Source of Truth Architecture - Complete authentication state management with storage-based getAuthState() as sole authoritative source
This commit is contained in:
413
login_logic.md
Normal file
413
login_logic.md
Normal file
@@ -0,0 +1,413 @@
|
||||
# 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:
|
||||
|
||||
1. **FloatingTab** - UI component for login trigger and status display
|
||||
2. **Modal** - UI component for authentication method selection
|
||||
3. **NostrLite** - Main library coordinator and facade manager
|
||||
4. **WindowNostr** - NIP-07 compliant facade for non-extension methods
|
||||
5. **AuthManager** - Persistent state management with encryption
|
||||
6. **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)`
|
||||
|
||||
```javascript
|
||||
// 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:**
|
||||
|
||||
```javascript
|
||||
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)
|
||||
```javascript
|
||||
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)
|
||||
```javascript
|
||||
// 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:**
|
||||
```javascript
|
||||
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 authentication
|
||||
- `nlAuthRestored` - Dispatched when authentication is restored from storage
|
||||
- `nlLogout` - Dispatched when user logs out
|
||||
- `nlReconnectionRequired` - 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)
|
||||
```javascript
|
||||
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
|
||||
```javascript
|
||||
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:**
|
||||
1. **File:** `lite/build.js`
|
||||
2. **Method:** `FloatingTab._handleClick()` (lines 351-369)
|
||||
3. **Action:** Remove extension auto-detection, always open modal
|
||||
|
||||
**Current Code to Replace (lines 358-367):**
|
||||
```javascript
|
||||
// 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:**
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
1. **Extension-First Architecture:** The system never interferes with real browser extensions
|
||||
2. **Dual Storage Support:** Supports both per-window (sessionStorage) and cross-window (localStorage) persistence
|
||||
3. **Security-First:** Sensitive data is always encrypted or not stored
|
||||
4. **Event-Driven:** All components communicate via custom events
|
||||
5. **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.
|
||||
Reference in New Issue
Block a user