UNPKG

saget-auth-middleware

Version:

SSO Middleware dengan dukungan localStorage untuk validasi authentifikasi domain malinau.go.id dan semua subdomain pada aplikasi Next.js 14 & 15

242 lines (212 loc) 7.86 kB
import { logger } from '../../logger.js'; /** * SSO Configuration class with enhanced validation and error handling */ export class SSOConfig { constructor(options = {}) { // Allow configuration override for testing this.options = options; // Initialize configuration with validation this.initializeConfig(); } /** * Initialize configuration with proper validation * @private */ initializeConfig() { try { // Validate environment variables first this.validateEnvironmentVariables(); // Initialize all environment variables with defaults const envVars = this.getEnvironmentVariables(); // Log environment variables in development only (without sensitive data) if (process.env.NODE_ENV === 'development') { this.logConfigurationSafely(envVars); } // Assign all environment variables to instance Object.assign(this, envVars); logger.info('SSO Configuration initialized successfully'); } catch (error) { logger.error('Failed to initialize SSO configuration:', error.message); throw new Error(`SSO Configuration Error: ${error.message}`); } } /** * Get environment variables with proper defaults and validation * @private * @returns {object} Environment variables object */ getEnvironmentVariables() { const envVars = { SSO_LOGIN_URL: this.getRequiredEnvVar('SSO_LOGIN_URL') || process.env.SSO_LOGIN_URL, APP_KEY: this.getRequiredEnvVar('SSO_APP_KEY') || process.env.SSO_APP_KEY, JWT_SECRET: this.getJWTSecret('SSO_JWT_SECRET') || process.env.SSO_JWT_SECRET, JWT_REFRESH_SECRET: this.getJWTSecret('SSO_JWT_REFRESH_SECRET') || process.env.SSO_JWT_REFRESH_SECRET, COOKIE_ACCESS_TOKEN_NAME: this.getEnvVar('COOKIE_ACCESS_TOKEN_NAME', 'sso_access_token'), COOKIE_REFRESH_TOKEN_NAME: this.getEnvVar('COOKIE_REFRESH_TOKEN_NAME', 'sso_refresh_token'), SSO_API_URL: this.getRequiredEnvVar('SSO_API_URL') || process.env.SSO_API_URL, COOKIE_DOMAIN: this.getEnvVar('COOKIE_DOMAIN', '') || process.env.COOKIE_DOMAIN, NEXT_REDIRECT_URL: this.getEnvVar('NEXT_REDIRECT_URL', '') || process.env.NEXT_REDIRECT_URL, NEXT_PUBLIC_COOKIE_ACCESS_TOKEN_NAME: this.getEnvVar('NEXT_PUBLIC_COOKIE_ACCESS_TOKEN_NAME', 'sso_access_token') || process.env.NEXT_PUBLIC_COOKIE_ACCESS_TOKEN_NAME, NEXT_PUBLIC_COOKIE_REFRESH_TOKEN_NAME: this.getEnvVar('NEXT_PUBLIC_COOKIE_REFRESH_TOKEN_NAME', 'sso_refresh_token') || process.env.NEXT_PUBLIC_COOKIE_REFRESH_TOKEN_NAME, NEXT_PUBLIC_COOKIE_DOMAIN: this.getEnvVar('NEXT_PUBLIC_COOKIE_DOMAIN', '') || process.env.NEXT_PUBLIC_COOKIE_DOMAIN, NEXT_PUBLIC_REDIRECT_URL: this.getEnvVar('NEXT_PUBLIC_REDIRECT_URL', '') || process.env.NEXT_PUBLIC_REDIRECT_URL, // Ensure header names don't contain invalid characters LOCAL_STORAGE_ACCESS_TOKEN_KEY: this.sanitizeHeaderName( this.getEnvVar('LOCAL_STORAGE_ACCESS_TOKEN_KEY', 'x-access-token') ), LOCAL_STORAGE_REFRESH_TOKEN_KEY: this.sanitizeHeaderName( this.getEnvVar('LOCAL_STORAGE_REFRESH_TOKEN_KEY', 'x-refresh-token') ) }; return envVars; } /** * Get required environment variable with validation * @private * @param {string} varName - Environment variable name * @returns {string} Environment variable value * @throws {Error} If variable is missing or empty */ getRequiredEnvVar(varName) { const value = this.options[varName] || process.env[varName]; if (!value || value.trim() === '') { throw new Error(`Required environment variable '${varName}' is missing or empty`); } return value.trim(); } /** * Get optional environment variable with default value * @private * @param {string} varName - Environment variable name * @param {string} defaultValue - Default value if not set * @returns {string} Environment variable value or default */ getEnvVar(varName, defaultValue = '') { const value = this.options[varName] || process.env[varName]; return value ? value.trim() : defaultValue; } /** * Get JWT secret as encoded bytes * @private * @param {string} varName - Environment variable name * @returns {Uint8Array|null} Encoded JWT secret */ getJWTSecret(varName) { const secret = this.getRequiredEnvVar(varName); try { return new TextEncoder().encode(secret); } catch (error) { throw new Error(`Failed to encode JWT secret '${varName}': ${error.message}`); } } /** * Sanitize header names to remove invalid characters * @private * @param {string} headerName - Header name to sanitize * @returns {string} Sanitized header name */ sanitizeHeaderName(headerName) { return headerName.split(':')[0].toLowerCase().trim(); } /** * Log configuration safely (without sensitive data) * @private * @param {object} config - Configuration object */ logConfigurationSafely(config) { const safeConfig = { ...config }; // Remove sensitive data from logs delete safeConfig.JWT_SECRET; delete safeConfig.JWT_REFRESH_SECRET; logger.info('SSO Configuration loaded:', safeConfig); } /** * Validate environment variables * @private */ validateEnvironmentVariables() { const requiredVars = [ 'SSO_LOGIN_URL', 'SSO_APP_KEY', 'SSO_JWT_SECRET', 'SSO_JWT_REFRESH_SECRET', 'SSO_API_URL' ]; const missingVars = requiredVars.filter(varName => { const value = this.options[varName] || process.env[varName]; return !value || value.trim() === ''; }); if (missingVars.length > 0) { throw new Error(`Missing required environment variables: ${missingVars.join(', ')}`); } // Validate URL formats this.validateURLs(); } /** * Validate URL formats * @private */ validateURLs() { const urlVars = [ { name: 'SSO_LOGIN_URL', value: this.options.SSO_LOGIN_URL || process.env.SSO_LOGIN_URL }, { name: 'SSO_API_URL', value: this.options.SSO_API_URL || process.env.SSO_API_URL } ]; urlVars.forEach(({ name, value }) => { if (value && !this.isValidURL(value)) { throw new Error(`Invalid URL format for '${name}': ${value}`); } }); } /** * Check if a string is a valid URL * @private * @param {string} urlString - URL string to validate * @returns {boolean} True if valid URL */ isValidURL(urlString) { try { new URL(urlString); return true; } catch { return false; } } /** * Validate cookie domain to prevent issues * @param {string} domain - Domain to validate * @returns {string|undefined} Validated domain or undefined */ validateCookieDomain(domain) { if (!domain || domain.trim() === '') { logger.info('No domain provided, using default'); return undefined; } // Remove protocol if present domain = domain.replace(/^https?:\/\//, '').trim(); // Handle localhost specifically - don't add dot prefix if (domain === 'localhost' || domain.startsWith('localhost:')) { return undefined; // For localhost, don't set domain at all } // For production domains, ensure it starts with a dot for subdomain support if (!domain.startsWith('.')) { domain = '.' + domain; } logger.info('Validated domain:', domain); return domain; } /** * Get configuration as plain object (for testing/debugging) * @returns {object} Configuration object */ toObject() { const config = {}; const excludeKeys = ['options']; // Exclude internal properties Object.keys(this).forEach(key => { if (!excludeKeys.includes(key)) { config[key] = this[key]; } }); return config; } }