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
JavaScript
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;
}
}