Files
nostr_login_lite/login_logic.md

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:

  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)

// 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 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:

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:

  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):

// 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

  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.