
- Add state machine for authentication state management - Add signing queue for background processing - Add auth service for centralized authentication logic - Add React context provider for components - Keep feature flag disabled for now
11 KiB
Secure Logout Procedures in Authentication Architecture
Last Updated: 2025-04-02
Status: Proposed
Authors: POWR Team
Overview
This document outlines the secure logout procedures implemented in POWR's centralized authentication system. Proper logout handling is critical for security, especially when dealing with cryptographic keys and external signers like Amber.
Security Considerations for Logout
A comprehensive logout procedure must address several security concerns:
- Cancellation of In-flight Operations: Any pending signing operations must be properly terminated.
- Secure Removal of Keys: Private keys and session data must be securely erased from memory and storage.
- External Signer Communication: External signers like Amber must be notified of session termination.
- Subscription Termination: NDK subscriptions must be closed to prevent data leakage.
- Memory Cache Clearing: Any in-memory caches must be purged.
- UI State Reset: The UI must reflect the unauthenticated state immediately.
Implementation Details
Core Logout Service
The AuthService
class provides a comprehensive logout method:
/**
* Performs a secure logout, removing all authentication artifacts
* and ensuring no sensitive data remains
*/
async logout(): Promise<boolean> {
try {
// 1. Cancel any pending signing operations
if (this.signingQueue) {
this.signingQueue.cancelAll('User logged out');
}
// 2. Notify the Amber app of session termination (Android only)
if (Platform.OS === 'android' && this.ndk.signer instanceof NDKAmberSigner) {
try {
await NativeModules.AmberSigner.terminateSession();
} catch (error) {
console.warn('Error terminating Amber session:', error);
// Continue with logout even if Amber notification fails
}
}
// 3. Clear the NDK signer reference
this.ndk.signer = undefined;
// 4. Clear all subscriptions to prevent data leakage after logout
this.clearSubscriptions();
// 5. Clear secure storage and AsyncStorage
const storageOperations = [
SecureStore.deleteItemAsync(PRIVATE_KEY_STORAGE_KEY),
AsyncStorage.multiRemove([
'currentUser',
'login',
'signer',
'auth.last_login',
'auth.permissions',
'auth.session',
'ndkMobileSessionLastEose'
])
];
await Promise.all(storageOperations);
// 6. Update auth state
AuthStateManager.logout();
// 7. Clear memory cache if implemented
this.clearMemoryCache();
return true;
} catch (error) {
console.error('Error during logout:', error);
// Even if an error occurs, still try to reset the state
AuthStateManager.logout();
return false;
}
}
/**
* Clears all active NDK subscriptions to prevent data leakage
*/
private clearSubscriptions(): void {
if (this.ndk.pool) {
// Close all relay connections
this.ndk.pool.close();
}
}
/**
* Clears any in-memory cached data
*/
private clearMemoryCache(): void {
// Implementation will depend on your caching strategy
// Clear any in-memory caches here
}
Signing Queue Cancellation
The SigningQueue
class implements a method to cancel all pending operations:
/**
* Cancels all pending operations in the queue
* @param reason The reason for cancellation
*/
cancelAll(reason: string): void {
const error = new Error(`Signing operations canceled: ${reason}`);
// Reject all queued operations
this.queue.forEach(operation => {
operation.reject(error);
});
// Clear the queue
this.queue = [];
// Reset processing state
this.processing = false;
this.activeCount = 0;
}
AuthStateManager Logout
The AuthStateManager
implements a logout method that ensures the UI is immediately updated:
logout: async () => {
try {
// Cancel any pending operations
const currentState = get();
if (currentState.status === 'signing') {
// Reject any pending operations with cancellation error
currentState.operations.forEach(operation => {
operation.reject(new Error('Authentication session terminated'));
});
}
// Securely clear all sensitive data from storage
await SecureStore.deleteItemAsync(PRIVATE_KEY_STORAGE_KEY);
await AsyncStorage.multiRemove([
'currentUser',
'login',
'signer',
'auth.last_login',
'auth.permissions',
'auth.session',
'ndkMobileSessionLastEose'
]);
// Reset state to unauthenticated
set({
status: 'unauthenticated'
});
// Log the logout event (without PII)
console.info('User logged out successfully');
return true;
} catch (error) {
console.error('Error during logout:', error);
return false;
}
}
Android-Specific Considerations
On Android, the Amber integration requires additional handling during logout:
- Amber Session Termination: The native Amber module should be extended to include a
terminateSession
method:
@ReactMethod
fun terminateSession(promise: Promise) {
executorService.execute {
try {
// Create a "terminate session" intent to notify Amber
val intent = Intent("nostrsigner://terminate")
intent.setPackage("com.greenart7c3.nostrsigner")
// Add metadata to the intent
intent.putExtra("app", "powr")
// Launch the intent
mainHandler.post {
try {
currentActivity?.startActivity(intent)
promise.resolve(true)
} catch (e: Exception) {
promise.reject("E_TERMINATE_ERROR", e.message)
}
}
} catch (e: Exception) {
promise.reject("E_PREPARATION_ERROR", e.message)
}
}
}
- Pending Promises Cleanup: Any pending promises in the native module should be rejected:
@ReactMethod
fun cancelAllPendingOperations(promise: Promise) {
val error = "Operations canceled due to logout"
pendingPromises.forEach { (_, pendingPromise) ->
pendingPromise.reject("E_CANCELED", error)
}
pendingPromises.clear()
promise.resolve(true)
}
iOS Considerations
On iOS, the focus is on secure deletion of private keys:
- Keychain Cleanup: The private key stored in the iOS Keychain must be securely removed:
// iOS-specific cleanup
if (Platform.OS === 'ios') {
await SecureStore.deleteItemAsync(PRIVATE_KEY_STORAGE_KEY);
// Additional iOS-specific cleanup as needed
}
- Memory Zeroing: For additional security, any in-memory copies of private keys should be zeroed:
// Helper function to securely zero sensitive data in memory
function securelyZeroMemory(variableRef: any): void {
if (typeof variableRef === 'string') {
// Overwrite the string with zeros
// Note: JavaScript strings are immutable, so this creates a new string
// The garbage collector will eventually clean up the original
for (let i = 0; i < variableRef.length; i++) {
variableRef = variableRef.substring(0, i) + '0' + variableRef.substring(i + 1);
}
} else if (ArrayBuffer.isView(variableRef)) {
// For typed arrays, we can actually zero the memory
const view = new Uint8Array(variableRef.buffer);
view.fill(0);
}
}
Logout UI Component
A dedicated logout button component ensures consistent logout behavior across the app:
// components/LogoutButton.tsx
import React from 'react';
import { TouchableOpacity, Text, ActivityIndicator, StyleSheet } from 'react-native';
import { useNDKStore } from '@/lib/stores/ndk';
import { useAuthStore } from '@/lib/auth/AuthStateManager';
export function LogoutButton() {
const { logout } = useNDKStore();
const [isLoggingOut, setIsLoggingOut] = React.useState(false);
const handleLogout = async () => {
if (isLoggingOut) return;
setIsLoggingOut(true);
try {
await logout();
} catch (error) {
console.error('Error during logout:', error);
// Show error to user if needed
} finally {
setIsLoggingOut(false);
}
};
return (
<TouchableOpacity
style={styles.button}
onPress={handleLogout}
disabled={isLoggingOut}
>
{isLoggingOut ? (
<ActivityIndicator size="small" color="#FFFFFF" />
) : (
<Text style={styles.text}>Sign Out</Text>
)}
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#F44336',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 4,
alignItems: 'center',
justifyContent: 'center',
},
text: {
color: '#FFFFFF',
fontWeight: 'bold',
},
});
Testing Logout Security
Thorough testing should verify:
- Complete Removal: Verify all authentication data is removed after logout
- State Reset: Confirm the UI properly reflects the unauthenticated state
- Failed Attempts: Test that authentication attempts fail after logout
- Pending Operations: Verify pending operations are properly canceled
- External Signer: Confirm Amber sessions are terminated
Example test cases:
// __tests__/auth/logout.test.ts
describe('Logout Security', () => {
it('should reject pending operations on logout', async () => {
// Set up an authenticated state
// ...
// Start a signing operation but don't let it complete
const signingPromise = authService.sign(mockEvent);
// Logout before the operation completes
authService.logout();
// Verify the operation was rejected
await expect(signingPromise).rejects.toThrow('Authentication session terminated');
});
it('should clear all secure storage on logout', async () => {
// Set up an authenticated state
// ...
// Mock SecureStore to verify deletion
const mockDeleteItem = jest.spyOn(SecureStore, 'deleteItemAsync');
// Perform logout
await authService.logout();
// Verify the private key was deleted
expect(mockDeleteItem).toHaveBeenCalledWith(PRIVATE_KEY_STORAGE_KEY);
});
// Additional tests...
});
Security Best Practices
- Immediate UI Feedback: Always update the UI immediately on logout to prevent confusion.
- Graceful Error Handling: Continue with logout even if individual cleanup steps fail.
- Comprehensive Cleanup: Remove all authentication artifacts, not just the obvious ones.
- Defensive Programming: Assume any step might fail and account for it.
- Session Invalidation: Notify external services of session termination when possible.
- Audit Logging: Log logout events (without PII) for security audit purposes.
By following these practices, the logout procedure ensures maximum security and a seamless user experience.