UNPKG

sessionize-auth

Version:

A flexible session management library for React, Next.js, Angular, and React Native

241 lines (210 loc) 6.75 kB
import { SSOConfig, SSOProvider, SSOUser, SSOAuthResult, SSOAuthState, SSOError } from "./types"; import { BaseSSOProvider, SSOErrorClass } from "./providers/base"; import { GoogleSSOProvider } from "./providers/google"; import { MicrosoftSSOProvider } from "./providers/microsoft"; import { GitHubSSOProvider } from "./providers/github"; /** * SSO Manager for handling multiple OAuth providers */ export class SSOManager { private config: SSOConfig; private providers: Map<string, BaseSSOProvider> = new Map(); private stateStorage: Map<string, SSOAuthState> = new Map(); constructor(config: SSOConfig) { this.config = config; this.initializeProviders(); } /** * Initialize all configured providers */ private initializeProviders(): void { this.config.providers.forEach(providerConfig => { let provider: BaseSSOProvider; switch (providerConfig.id) { case 'google': provider = new GoogleSSOProvider(providerConfig.id, { clientId: providerConfig.clientId, redirectUri: providerConfig.redirectUri, scopes: providerConfig.scopes }); break; case 'microsoft': provider = new MicrosoftSSOProvider(providerConfig.id, { clientId: providerConfig.clientId, redirectUri: providerConfig.redirectUri, scopes: providerConfig.scopes }); break; case 'github': provider = new GitHubSSOProvider(providerConfig.id, { clientId: providerConfig.clientId, redirectUri: providerConfig.redirectUri, scopes: providerConfig.scopes }); break; default: throw new SSOErrorClass({ code: 'UNSUPPORTED_PROVIDER', message: `Unsupported SSO provider: ${providerConfig.id}` }); } this.providers.set(providerConfig.id, provider); }); } /** * Get all available providers */ getProviders(): SSOProvider[] { return Array.from(this.providers.values()).map(provider => provider.getProvider()); } /** * Get a specific provider */ getProvider(providerId: string): SSOProvider | null { const provider = this.providers.get(providerId); return provider ? provider.getProvider() : null; } /** * Generate authorization URL for a provider */ generateAuthUrl(providerId: string, returnTo?: string): string { const provider = this.providers.get(providerId); if (!provider) { throw new SSOErrorClass({ code: 'PROVIDER_NOT_FOUND', message: `Provider not found: ${providerId}` }); } const state = this.generateState(); const authState: SSOAuthState = { provider: providerId, state: state, timestamp: Date.now(), returnTo: returnTo }; this.stateStorage.set(state, authState); return provider.generateAuthUrl(state, returnTo); } /** * Handle OAuth callback */ async handleCallback( providerId: string, code: string, state: string ): Promise<SSOAuthResult> { try { // Verify state const authState = this.stateStorage.get(state); if (!authState) { throw new SSOErrorClass({ code: 'INVALID_STATE', message: 'Invalid or expired state parameter' }); } if (authState.provider !== providerId) { throw new SSOErrorClass({ code: 'PROVIDER_MISMATCH', message: 'Provider mismatch in state' }); } // Check state expiration (5 minutes) if (Date.now() - authState.timestamp > 5 * 60 * 1000) { this.stateStorage.delete(state); throw new SSOErrorClass({ code: 'STATE_EXPIRED', message: 'State parameter has expired' }); } const provider = this.providers.get(providerId); if (!provider) { throw new SSOErrorClass({ code: 'PROVIDER_NOT_FOUND', message: `Provider not found: ${providerId}` }); } // Exchange code for token const tokenData = await provider.exchangeCodeForToken(code, state); // Get user info const user = await provider.getUserInfo(tokenData.accessToken); // Add token information user.accessToken = tokenData.accessToken; user.refreshToken = tokenData.refreshToken; user.expiresAt = tokenData.expiresIn ? Date.now() + (tokenData.expiresIn * 1000) : undefined; // Clean up state this.stateStorage.delete(state); return { success: true, user: user, provider: providerId }; } catch (error) { return { success: false, error: error instanceof SSOErrorClass ? error.message : 'Unknown error occurred', provider: providerId }; } } /** * Refresh access token */ async refreshToken(providerId: string, refreshToken: string): Promise<{ accessToken: string; expiresIn?: number }> { const provider = this.providers.get(providerId); if (!provider) { throw new SSOErrorClass({ code: 'PROVIDER_NOT_FOUND', message: `Provider not found: ${providerId}` }); } return provider.refreshToken(refreshToken); } /** * Get user info with current access token */ async getUserInfo(providerId: string, accessToken: string): Promise<SSOUser> { const provider = this.providers.get(providerId); if (!provider) { throw new SSOErrorClass({ code: 'PROVIDER_NOT_FOUND', message: `Provider not found: ${providerId}` }); } return provider.getUserInfo(accessToken); } /** * Generate a random state parameter */ private generateState(): string { const array = new Uint8Array(32); crypto.getRandomValues(array); return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); } /** * Clean up expired states */ cleanupExpiredStates(): void { const now = Date.now(); const expiredStates: string[] = []; this.stateStorage.forEach((state, key) => { if (now - state.timestamp > 5 * 60 * 1000) { // 5 minutes expiredStates.push(key); } }); expiredStates.forEach(state => this.stateStorage.delete(state)); } /** * Update configuration */ updateConfig(newConfig: Partial<SSOConfig>): void { this.config = { ...this.config, ...newConfig }; this.initializeProviders(); } }