7.7 KiB
Authentication Architecture
Last Updated: 2025-03-25
Status: Active
Related To: Nostr Integration, State Management
Purpose
This document describes the authentication architecture of the POWR app, focusing on Nostr-based authentication, key management, and the state machine implementation needed for the MVP.
Overview
Authentication in POWR is built on the Nostr protocol, which uses public key cryptography. The system:
- Manages user keypairs for Nostr authentication
- Maintains clear login/logout state
- Securely stores private keys on device
- Implements a proper state machine for auth transitions
- Supports offline capabilities with cached authentication
Component Architecture
High-Level Components
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ UI Layer │ │ Service Layer │ │ Storage Layer │
│ │ │ │ │ │
│ Login Prompt │ │ Auth Manager │ │ Secure Storage │
│ Auth State UI │◄───►│ State Machine │◄───►│ NDK Signer │
│ Profile Display │ │ Relay Manager │ │ Session Storage │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Auth State Machine
The core of the authentication architecture is a state machine with these states:
- Unauthenticated: No valid authentication exists
- Authenticating: Authentication process in progress
- Authenticated: User is fully authenticated
- Deauthenticating: Logout process in progress
This state machine handles all transitions between these states, ensuring consistent authentication behavior.
Implementation
The authentication system uses Zustand for state management:
// Auth store implementation example
interface AuthState {
// State
state: 'unauthenticated' | 'authenticating' | 'authenticated' | 'deauthenticating';
user: { pubkey: string; metadata?: any } | null;
error: Error | null;
// Actions
login: (privateKeyOrNpub: string) => Promise<void>;
createAccount: () => Promise<void>;
logout: () => Promise<void>;
}
const useAuthStore = create<AuthState>((set, get) => ({
state: 'unauthenticated',
user: null,
error: null,
login: async (privateKeyOrNpub) => {
try {
set({ state: 'authenticating', error: null });
// Implementation details for key import
// NDK setup with signer
// Profile fetching
set({ state: 'authenticated', user: { pubkey: '...' } });
} catch (error) {
set({ state: 'unauthenticated', error });
}
},
createAccount: async () => {
try {
set({ state: 'authenticating', error: null });
// Generate new keypair
// Save to secure storage
// NDK setup with signer
set({ state: 'authenticated', user: { pubkey: '...' } });
} catch (error) {
set({ state: 'unauthenticated', error });
}
},
logout: async () => {
try {
set({ state: 'deauthenticating' });
// Clean up connections
// Clear sensitive data
set({ state: 'unauthenticated', user: null });
} catch (error) {
// Even on error, force logout
set({ state: 'unauthenticated', user: null, error });
}
}
}));
Key Storage and Management
Private keys are stored using:
- iOS: Keychain Services
- Android: Android Keystore System
- Both: Expo SecureStore wrapper
The keys are never exposed directly to application code; instead, a signer interface is used that can perform cryptographic operations without exposing the private key.
MVP Implementation Focus
For the MVP release, the authentication system focuses on:
-
Robust State Management
- Clear state transitions
- Error handling for each state
- Proper event tracking
-
Basic Auth Flows
- Login with npub or nsec (with security warnings)
- Account creation
- Reliable logout
-
Key Security
- Secure storage of private keys
- Zero exposure of private keys in app memory
- Proper cleanup on logout
Integration Points
The authentication system integrates with:
-
NDK Instance
- Provides signer for NDK
- Manages relay connections
- Triggers cleanup on logout
-
Profile Management
- Fetches user profile on login
- Updates profile metadata
- Handles associated data loading
-
UI Components
- Login/create account prompt
- Authentication state indicators
- Profile display components
Known Issues and Solutions
Current Issues
-
Inconsistent Auth State
- Problem: Multiple components updating auth state cause race conditions
- Solution: Centralized state machine with explicit transitions
-
Incomplete Logout
- Problem: Resources not properly cleaned up on logout
- Solution: Comprehensive cleanup in deauthenticating state
-
Subscription Cleanup
- Problem: Subscriptions not tied to auth lifecycle
- Solution: Link subscription management to auth state changes
State Transitions
Handling auth state transitions properly:
┌───────────────┐ ┌───────────────┐
│ │ │ │
│Unauthenticated│ │ Authenticated │
│ │ │ │
└───────┬───────┘ └───────┬───────┘
│ │
│ login/ │ logout
│ createAccount │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ │ success │ │
│Authenticating │─────────────────────────────►│Deauthenticating│
│ │ │ │
└───────────────┘ └───────────────┘
│ │
│ error │ always
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ │ │ │
│Unauthenticated│◄─────────────────────────────┤Unauthenticated│
│ (with error)│ │ │
└───────────────┘ └───────────────┘
Related Documentation
- NDK Comprehensive Guide - NDK implementation
- Subscription Analysis - Subscription management
- Profile Features - Profile integration
- MVP and Targeted Rebuild - Overall MVP strategy