413 lines
17 KiB
Markdown
413 lines
17 KiB
Markdown
# 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. |