-
Notifications
You must be signed in to change notification settings - Fork 0
Description
JavaScript Authentication Library Technical Specification
Design Philosophy: This specification prioritizes security, developer experience, and extensibility. Key considerations include preventing XSS token theft, supporting page refresh recovery, and providing a plugin architecture for custom authentication schemes.
Core Architecture
Application Class
Design Consideration: The Application class serves as the main entry point, exposing async/await patterns preferred by developers while maintaining callback support internally. The configuration approach allows for future expansion without breaking existing implementations.
interface ApplicationConfig {
auth?: AuthConfig;
// Other app configurations (including existing storage config)
}
interface AuthConfig {
provider: AuthProvider;
refreshThreshold?: number; // Minutes before expiry to auto-refresh
autoRefresh?: boolean;
}
class Application {
constructor(config: ApplicationConfig);
// Authentication methods
authenticate(credentials: unknown): Promise<AuthResult>;
refresh(token?: RefreshToken): Promise<AuthResult>;
logout(): Promise<void>;
// Credential access
getCredentials(options?: CredentialOptions): Promise<Credentials | null>;
isAuthenticated(): boolean;
// Event handling
on(event: AuthEvent, callback: Function): void;
off(event: AuthEvent, callback: Function): void;
}Authentication Provider Interface
Plugin Architecture Rationale: This interface was designed to support the planned OIDC plugin while allowing developers to create custom authentication schemes. The common interface ensures consistent behavior across different authentication methods. Storage responsibility is delegated to plugin authors, allowing them to choose appropriate storage mechanisms for their specific authentication schemes.
interface AuthProvider {
name: string;
version: string;
// Core authentication methods
authenticate(credentials: unknown, config: AuthConfig): Promise<AuthResult>;
refresh(refreshToken: RefreshToken, config: AuthConfig): Promise<AuthResult>;
logout(credentials: Credentials, config: AuthConfig): Promise<void>;
// Validation and utilities
validateCredentials(credentials: unknown): boolean;
getTokenExpiry(credentials: Credentials): Date | null;
extractCredentials(authResult: AuthResult): Credentials;
// Storage management (plugin responsibility)
saveState(state: AuthState): Promise<void>;
loadState(): Promise<AuthState | null>;
clearState(): Promise<void>;
}
interface AuthResult {
success: boolean;
credentials?: Credentials;
refreshToken?: RefreshToken;
expiresAt?: Date;
metadata?: Record<string, unknown>;
error?: AuthError;
}
interface Credentials {
type: string; // 'bearer', 'basic', 'custom', etc.
value: string | Record<string, unknown>;
expiresAt?: Date;
metadata?: Record<string, unknown>;
}
interface RefreshToken {
value: string;
expiresAt?: Date;
metadata?: Record<string, unknown>;
}State Management
Security vs. Usability Balance: The state management strategy addresses the core requirement of page refresh persistence while maintaining security. Plugin authors are responsible for implementing appropriate storage mechanisms for their authentication schemes, allowing for scheme-specific security and persistence requirements.
Authentication State Store
User Abstraction: Currently excludes user concepts as requested, focusing on credentials and authentication state. The structure allows for future user support without major refactoring.
interface AuthState {
isAuthenticated: boolean;
credentials: Credentials | null;
refreshToken: RefreshToken | null;
lastAuthTime: Date | null;
error: AuthError | null;
}
// Core library only manages in-memory state
interface AuthStateManager {
// Memory-only storage for active credentials (security requirement)
setCredentials(credentials: Credentials): void;
getCredentials(): Credentials | null;
clearCredentials(): void;
// Delegates persistent storage to auth provider
saveState(): Promise<void>; // Calls provider.saveState()
loadState(): Promise<void>; // Calls provider.loadState()
clearState(): Promise<void>; // Calls provider.clearState()
}Storage Strategy
Plugin-Managed Storage: Storage mechanisms are now the responsibility of individual authentication providers, allowing each plugin to implement storage strategies appropriate for their security requirements and authentication flow. The core library only manages in-memory credentials for security.
- Memory Storage: Active credentials always stored in memory only (core library responsibility)
- Persistent Storage: Refresh tokens and recovery state managed by auth providers
- Storage Flexibility: Plugins can choose localStorage, sessionStorage, IndexedDB, or encrypted storage
- Security Control: Each plugin implements security measures appropriate for their authentication method
Authentication Flow
Page Refresh Recovery: This flow addresses the key requirement that authentication state should persist across page refreshes without compromising security. The refresh token mechanism provides seamless recovery while the memory-only credential storage prevents token theft.
Initial Authentication
// 1. Developer calls authenticate
const result = await app.authenticate({
username: 'user@example.com',
password: 'password'
});
// 2. Library flow:
// - Validates credentials via provider
// - Stores credentials in memory
// - Stores refresh token persistently (if provided)
// - Sets up auto-refresh timer
// - Emits 'authenticated' eventPage Refresh Recovery
Automatic Recovery Strategy: This initialization process ensures users don't need to re-authenticate after page refreshes, addressing the core usability requirement. Recovery mechanisms are implemented by auth providers, allowing for authentication-scheme-specific recovery strategies.
// On application initialization:
// 1. Ask auth provider to load any persistent state
// 2. If state found and valid, attempt refresh
// 3. If refresh succeeds, restore authenticated state
// 4. If refresh fails, clear state and require re-authentication
class Application {
async initialize(): Promise<void> {
const persistentState = await this.authProvider.loadState();
if (persistentState?.refreshToken && !this.isExpired(persistentState.refreshToken)) {
try {
await this.refresh(persistentState.refreshToken);
} catch (error) {
await this.authProvider.clearState();
this.emit('authenticationRequired');
}
}
}
}Token Refresh Mechanism
Proactive Token Management: The refresh mechanism supports explicit refresh calls as requested (
await app.refresh(token)) while also providing background refresh capabilities. The timer-based approach prevents authentication failures from expired tokens.
interface RefreshManager {
scheduleRefresh(credentials: Credentials): void;
cancelScheduledRefresh(): void;
executeRefresh(): Promise<void>;
}
// Auto-refresh implementation
class TokenRefreshManager implements RefreshManager {
private refreshTimer: number | null = null;
scheduleRefresh(credentials: Credentials): void {
if (!credentials.expiresAt) return;
const refreshTime = new Date(credentials.expiresAt.getTime() - (this.config.refreshThreshold * 60000));
const delay = refreshTime.getTime() - Date.now();
if (delay > 0) {
this.refreshTimer = setTimeout(() => this.executeRefresh(), delay);
}
}
}Plugin System
Extensibility Focus: The plugin system addresses the requirement for custom authentication schemes while preparing for the planned OIDC plugin. This design allows the library to support authentication methods not officially provided.
Plugin Registration
interface PluginRegistry {
register(provider: AuthProvider): void;
get(name: string): AuthProvider | null;
list(): string[];
}
// Usage
const oidcProvider = new OIDCAuthProvider({
issuer: 'https://auth.example.com',
clientId: 'your-client-id'
});
const app = new Application({
auth: {
provider: oidcProvider
}
});Example OIDC Plugin Structure
Plugin Implementation Guide: This example demonstrates how the planned official OIDC plugin would integrate with the core library, including its own storage management. This shows how plugins can implement storage strategies appropriate for their specific security requirements.
class OIDCAuthProvider implements AuthProvider {
name = 'oidc';
version = '1.0.0';
constructor(private config: OIDCConfig) {
// Plugin-specific storage configuration
this.storageKey = `${this.name}_auth_state`;
this.storageEngine = config.storage || 'localStorage';
}
async authenticate(credentials: OIDCCredentials): Promise<AuthResult> {
// Implement OIDC authentication flow
const tokenResponse = await this.exchangeCredentials(credentials);
const result = {
success: true,
credentials: {
type: 'bearer',
value: tokenResponse.access_token,
expiresAt: new Date(Date.now() + tokenResponse.expires_in * 1000)
},
refreshToken: tokenResponse.refresh_token ? {
value: tokenResponse.refresh_token,
expiresAt: tokenResponse.refresh_expires_in ?
new Date(Date.now() + tokenResponse.refresh_expires_in * 1000) : undefined
} : undefined
};
// Plugin manages its own persistence
await this.saveState({
isAuthenticated: true,
credentials: result.credentials,
refreshToken: result.refreshToken,
lastAuthTime: new Date(),
error: null
});
return result;
}
async refresh(refreshToken: RefreshToken): Promise<AuthResult> {
// Implement token refresh
}
async logout(credentials: Credentials): Promise<void> {
// Implement logout/revocation and clear storage
await this.clearState();
}
// Plugin-specific storage implementation
async saveState(state: AuthState): Promise<void> {
const stateToSave = {
refreshToken: state.refreshToken,
lastAuthTime: state.lastAuthTime
// Don't persist active credentials for security
};
if (this.config.encryptStorage) {
const encrypted = await this.encrypt(JSON.stringify(stateToSave));
this.getStorageEngine().setItem(this.storageKey, encrypted);
} else {
this.getStorageEngine().setItem(this.storageKey, JSON.stringify(stateToSave));
}
}
async loadState(): Promise<AuthState | null> {
const stored = this.getStorageEngine().getItem(this.storageKey);
if (!stored) return null;
try {
const data = this.config.encryptStorage ?
JSON.parse(await this.decrypt(stored)) :
JSON.parse(stored);
return {
isAuthenticated: false, // Will be set after successful refresh
credentials: null, // Never persisted
refreshToken: data.refreshToken,
lastAuthTime: data.lastAuthTime ? new Date(data.lastAuthTime) : null,
error: null
};
} catch (error) {
await this.clearState();
return null;
}
}
async clearState(): Promise<void> {
this.getStorageEngine().removeItem(this.storageKey);
}
private getStorageEngine() {
return this.storageEngine === 'sessionStorage' ? sessionStorage : localStorage;
}
}Event System
Reactive Architecture: The event system enables the existing React integration to respond to authentication changes and supports future framework integrations. Events provide loose coupling between authentication logic and UI components.
type AuthEvent =
| 'authenticated'
| 'refreshed'
| 'logout'
| 'authenticationRequired'
| 'refreshFailed'
| 'error';
interface EventEmitter {
on(event: AuthEvent, callback: (data?: any) => void): void;
off(event: AuthEvent, callback: (data?: any) => void): void;
emit(event: AuthEvent, data?: any): void;
}
// Usage
app.on('authenticated', (credentials) => {
console.log('User authenticated successfully');
});
app.on('authenticationRequired', () => {
// Redirect to login page
});Error Handling
Async/Await Compatibility: Error handling supports both promise rejection and callback patterns as specified, with standardized error codes for consistent error handling across different authentication providers.
class AuthError extends Error {
constructor(
message: string,
public code: string,
public details?: Record<string, unknown>
) {
super(message);
this.name = 'AuthError';
}
}
// Standard error codes
enum AuthErrorCode {
INVALID_CREDENTIALS = 'INVALID_CREDENTIALS',
TOKEN_EXPIRED = 'TOKEN_EXPIRED',
REFRESH_FAILED = 'REFRESH_FAILED',
NETWORK_ERROR = 'NETWORK_ERROR',
PROVIDER_ERROR = 'PROVIDER_ERROR'
}Usage Examples
Developer Experience: These examples demonstrate the
app.getCredentials()pattern requested for accessing authentication proof in HTTP requests, showing integration with the fetch API as currently expected.
Basic Authentication Setup
import { Application } from '@yourlib/core';
import { BasicAuthProvider } from '@yourlib/auth-basic';
const basicAuth = new BasicAuthProvider({
endpoint: 'https://api.example.com/auth'
});
const app = new Application({
auth: {
provider: basicAuth,
refreshThreshold: 5, // Refresh 5 minutes before expiry
autoRefresh: true
}
});
// Authenticate
await app.authenticate({
username: 'user@example.com',
password: 'password'
});
// Use credentials in requests
const credentials = await app.getCredentials();
if (credentials) {
const response = await fetch('/api/protected', {
headers: {
'Authorization': `${credentials.type} ${credentials.value}`
}
});
}React Integration
Framework Integration Strategy: This example shows how the existing React integration library can consume the authentication events and state, demonstrating the event-driven architecture's benefits for UI frameworks.
// React hook example
function useAuth() {
const app = useContext(ApplicationContext);
const [isAuthenticated, setIsAuthenticated] = useState(app.isAuthenticated());
useEffect(() => {
const handleAuth = () => setIsAuthenticated(true);
const handleLogout = () => setIsAuthenticated(false);
app.on('authenticated', handleAuth);
app.on('logout', handleLogout);
return () => {
app.off('authenticated', handleAuth);
app.off('logout', handleLogout);
};
}, [app]);
return {
isAuthenticated,
login: app.authenticate.bind(app),
logout: app.logout.bind(app),
getCredentials: app.getCredentials.bind(app)
};
}Future Considerations
Multi-Session Architecture: While not required for the initial version, this structure addresses the mentioned possibility of supporting multiple concurrent sessions with user switching capabilities. The design uses the internal map approach suggested during our discussion.
Multi-Session Support Structure
Cross-tab Synchronization Note: Cross-tab synchronization was considered but not included due to potential security risks. The current design prioritizes security over convenience for multi-tab scenarios.
interface UserSession {
id: string;
credentials: Credentials;
refreshToken?: RefreshToken;
metadata?: Record<string, unknown>;
}
interface SessionManager {
addSession(session: UserSession): void;
removeSession(id: string): void;
switchSession(id: string): void;
getCurrentSession(): UserSession | null;
getAllSessions(): UserSession[];
}This specification provides a solid foundation that can be extended as requirements evolve, while maintaining security best practices and developer experience.
Key Design Decisions Summary
Security-First Approach: Credentials stay in memory while only refresh tokens are persisted, minimizing XSS attack surface while maintaining usability across page refreshes.
Plugin Architecture: Clean provider interface allows for custom authentication schemes while maintaining consistent API surface for the planned OIDC plugin and community extensions.
State Recovery: Automatic refresh token validation on initialization ensures seamless user experience after page refreshes without compromising security.
TypeScript-Native: Full type definitions provide excellent developer experience and compile-time safety as requested.
Event-Driven: Reactive architecture allows both your React integration and other frameworks to respond to authentication state changes cleanly.
Extensible Foundation: The session manager structure is ready for multi-user support when needed, and the configuration system can accommodate additional features without breaking changes.