UNPKG

@re-auth/http-adapters

Version:

HTTP adapters for ReAuth authentication framework

407 lines 16.8 kB
import { Router } from 'express'; import { createHttpAdapter, createRouteOverride, createCustomRoute, createAutoIntrospectionConfig, introspectReAuthEngine, createContextRule, OAuth2ContextRules, findContextRules, } from '../../utils/http-adapter-factory'; /** * Express framework adapter implementation */ class ExpressFrameworkAdapter { constructor(engine) { this.contextRules = []; this.adapterConfig = {}; this.router = Router(); this.engine = engine; } /** * Set the ReAuth engine instance (for shared adapter pattern) */ setEngine(engine) { this.engine = engine; } /** * Set context rules (for shared adapter pattern) */ setContextRules(rules) { this.contextRules = rules; } /** * Set adapter config (for shared adapter pattern) */ setAdapterConfig(config) { this.adapterConfig = config; } /** * Get expected inputs for a plugin step */ getExpectedInputs(pluginName, stepName) { return this.engine?.getStepInputs?.(pluginName, stepName) || []; } setupMiddleware(context) { // Store context rules and adapter config at instance level this.setContextRules(context.config.contextRules); this.setAdapterConfig({ cookieName: context.config.cookieName || 'auth_token', cookieOptions: context.config.cookieOptions || {}, }); // Parse cookies this.router.use((req, res, next) => { if (req.cookies) return next(); // Simple cookie parsing if no cookie-parser is used const cookieHeader = req.headers.cookie || ''; req.cookies = cookieHeader .split(';') .reduce((cookies, cookie) => { const [name, value] = cookie.trim().split('='); if (name && value) { cookies[name] = value; } return cookies; }, {}); next(); }); // Global middleware if (context.config.globalMiddleware) { context.config.globalMiddleware.forEach((middleware) => { this.router.use(middleware); }); } // Auth middleware (much lighter without heavy context attachments) this.router.use(async (req, res, next) => { const token = this.extractToken(req); if (token) { try { const session = await context.engine.checkSession(token); if (session.valid && session.entity) { req.user = session.entity; req.token = session.token; req.isAuthenticated = () => true; } } catch (error) { console.warn('Invalid token:', error); } } if (!req.isAuthenticated) { req.isAuthenticated = () => false; } next(); }); } createRoute(method, path, handler, middleware = []) { const routeMethod = method.toLowerCase(); if (typeof this.router[routeMethod] === 'function') { // No need to wrap handler for context attachment - everything is at adapter level now this.router[routeMethod](path, ...middleware, handler); } } async extractInputs(request, pluginName, stepName) { // Get expected inputs from the shared engine instance instead of request const expectedInputs = this.getExpectedInputs(pluginName, stepName); const inputs = {}; // Extract from body, query, and params expectedInputs.forEach((inputName) => { if (request.body && request.body[inputName] !== undefined) { inputs[inputName] = request.body[inputName]; } else if (request.query[inputName] !== undefined) { inputs[inputName] = request.query[inputName]; } else if (request.params && request.params[inputName] !== undefined) { inputs[inputName] = request.params[inputName]; } }); return inputs; } /** * Add configurable context inputs from cookies and headers based on context rules * This mirrors the implementation from the factory but is specific to Express */ addConfigurableContextInputs(request, inputs, pluginName, stepName, contextRules) { // Use stored context rules instead of parameter (for compatibility) const rulesToUse = contextRules.length > 0 ? contextRules : this.contextRules; // Find applicable context extraction rules const applicableRules = findContextRules(pluginName, stepName, rulesToUse); applicableRules.forEach((rule) => { // Extract cookies if (rule.extractCookies && request.cookies) { rule.extractCookies.forEach((cookieName) => { if (request.cookies[cookieName]) { let value = request.cookies[cookieName]; // Apply transform if provided if (rule.transformInput) { value = rule.transformInput(cookieName, value, request); } inputs[cookieName] = value; } }); } // Extract headers if (rule.extractHeaders && request.headers) { const headerConfig = rule.extractHeaders; if (Array.isArray(headerConfig)) { // Simple array format: ['header-name'] headerConfig.forEach((headerName) => { const headerValue = request.headers[headerName.toLowerCase()]; if (headerValue) { let value = headerValue; // Apply transform if provided if (rule.transformInput) { value = rule.transformInput(headerName, value, request); } inputs[headerName.replace(/-/g, '_')] = value; // Convert header-name to header_name } }); } else { // Object format: { 'header-name': 'inputName' } Object.entries(headerConfig).forEach(([headerName, inputName]) => { const headerValue = request.headers[headerName.toLowerCase()]; if (headerValue) { let value = headerValue; // Apply transform if provided if (rule.transformInput) { value = rule.transformInput(inputName, value, request); } inputs[inputName] = value; } }); } } }); } handleStepResponse(request, response, result, httpConfig) { const { token, redirect, success, status, cookies, ...data } = result; // Handle token (set cookie) using stored adapter config if (token) { response.cookie(this.adapterConfig.cookieName, token, this.adapterConfig.cookieOptions); } // Handle additional cookies from result.cookies if (cookies) { Object.entries(cookies).forEach(([name, value]) => { response.cookie(name, value); }); } // Handle redirect if (redirect) { return response.redirect(redirect); } // Determine status code const statusCode = this.getStatusCode(result, httpConfig); // Send response response.status(statusCode).json({ success, ...data, }); } /** * Handle configurable context outputs (set cookies and headers) based on context rules * This mirrors the implementation from the factory but is specific to Express */ handleConfigurableContextOutputs(request, response, result, pluginName, stepName, contextRules) { // Use stored context rules instead of parameter (for compatibility) const rulesToUse = contextRules.length > 0 ? contextRules : this.contextRules; // Find applicable context extraction rules const applicableRules = findContextRules(pluginName, stepName, rulesToUse); applicableRules.forEach((rule) => { // Set cookies from result if (rule.setCookies && response.cookie) { rule.setCookies.forEach((cookieName) => { if (result[cookieName] !== undefined) { let value = result[cookieName]; // Apply transform if provided if (rule.transformOutput) { value = rule.transformOutput(cookieName, value, result, request); } // Handle complex cookie options if (typeof value === 'object' && value !== null && 'value' in value) { // Value is a cookie options object const { value: cookieValue, ...cookieOptions } = value; response.cookie(cookieName, cookieValue, cookieOptions); } else { // Simple value response.cookie(cookieName, value); } } }); } // Set headers from result if (rule.setHeaders && response.setHeader) { const headerConfig = rule.setHeaders; if (Array.isArray(headerConfig)) { // Simple array format: ['header-name'] headerConfig.forEach((headerName) => { const inputName = headerName.replace(/-/g, '_'); // Convert header-name to header_name if (result[inputName] !== undefined) { let value = result[inputName]; // Apply transform if provided if (rule.transformOutput) { value = rule.transformOutput(headerName, value, result, request); } response.setHeader(headerName, value); } }); } else { // Object format: { 'header-name': 'resultKey' } Object.entries(headerConfig).forEach(([headerName, resultKey]) => { if (result[resultKey] !== undefined) { let value = result[resultKey]; // Apply transform if provided if (rule.transformOutput) { value = rule.transformOutput(headerName, value, result, request); } response.setHeader(headerName, value); } }); } } }); } getStatusCode(result, httpConfig) { // First, check if the plugin step defines a specific status code for the result status if (result.status && httpConfig[result.status]) { return httpConfig[result.status]; } // Fallback to generic success/error codes from httpConfig if (result.success && httpConfig.success) { return httpConfig.success; } if (!result.success && httpConfig.error) { return httpConfig.error; } // Fallback to standard HTTP status codes based on result.status if (result.status === 'redirect') return 302; if (result.status === 'unauthorized') return 401; if (result.status === 'forbidden') return 403; if (result.status === 'not_found') return 404; if (result.status === 'conflict') return 409; if (result.status === 'error') return 400; // Default fallback return result.success ? 200 : 400; } extractToken(request) { // From cookie if (request.cookies && request.cookies.auth_token) { return request.cookies.auth_token; } // From Authorization header const authHeader = request.headers.authorization; if (authHeader?.startsWith('Bearer ')) { return authHeader.substring(7); } return null; } requireAuth() { return (req, res, next) => { if (!req.isAuthenticated || !req.isAuthenticated()) { return res.status(401).json({ error: 'Authentication required' }); } next(); }; } errorResponse(response, error) { console.error('HTTP Adapter Error:', error); response.status(500).json({ success: false, message: error.message || 'Internal server error', error: process.env.NODE_ENV === 'development' ? error.stack : undefined, }); } getAdapter() { return this.router; } } /** * Create a shared Express framework adapter instance */ const sharedExpressFrameworkAdapter = new ExpressFrameworkAdapter(); /** * Create Express adapter using the factory pattern with shared instance */ export const createExpressAdapterV2 = (engine, config, frameworkAdapter) => { return createHttpAdapter(frameworkAdapter)(engine, config); }; /** * Express adapter class with additional features */ export class ExpressAdapterV2 { constructor(engine, config = {}, frameworkAdapter) { this.engine = engine; this.config = config; this.frameworkAdapter = frameworkAdapter || new ExpressFrameworkAdapter(engine); // Set the engine on the shared adapter before creating the HTTP adapter this.frameworkAdapter.setEngine(engine); // Create the router using the factory this.router = createExpressAdapterV2(engine, config, this.frameworkAdapter); // No need for middleware to attach config - it's stored at adapter level now } /** * Get the Express router */ getRouter() { return this.router; } /** * Add a custom route */ addRoute(method, path, handler, options = {}) { const middleware = options.middleware || []; if (options.requireAuth) { middleware.push(this.frameworkAdapter.requireAuth()); } const routeMethod = method.toLowerCase(); if (typeof this.router[routeMethod] === 'function') { this.router[routeMethod](path, ...middleware, handler); } } /** * Protection middleware for routes */ protect(options = {}) { return async (req, res, next) => { // Check authentication if (!req.isAuthenticated || !req.isAuthenticated()) { return res.status(401).json({ error: 'Authentication required' }); } // Check roles if (options.roles && options.roles.length > 0) { const userRole = req.user?.role; if (!userRole || !options.roles.includes(userRole)) { return res.status(403).json({ error: 'Insufficient permissions' }); } } // Custom authorization if (options.authorize) { try { const isAuthorized = await options.authorize(req.user, req, res, next); if (!isAuthorized) { return res.status(403).json({ error: 'Access denied' }); } } catch (error) { console.error('Authorization error:', error); return res.status(500).json({ error: 'Authorization check failed' }); } } next(); }; } } /** * Convenience function to create Express adapter with options */ export function createExpressAdapter(engine, config = {}) { return new ExpressAdapterV2(engine, config); } // Export utility functions export { createRouteOverride, createCustomRoute, createAutoIntrospectionConfig, introspectReAuthEngine, createContextRule, OAuth2ContextRules, }; //# sourceMappingURL=express-adapter-v2.js.map