UNPKG

@re-auth/http-adapters

Version:

HTTP adapters for ReAuth authentication framework

422 lines 17.5 kB
import { createHttpAdapter, createRouteOverride, createCustomRoute, createAutoIntrospectionConfig, introspectReAuthEngine, findContextRules, createContextRule, OAuth2ContextRules, } from '../../utils/http-adapter-factory'; /** * Fastify framework adapter implementation */ class FastifyFrameworkAdapter { constructor(fastify, engine) { this.contextRules = []; this.adapterConfig = {}; this.routes = []; this.fastify = fastify; // Will be injected later if not provided 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) || []; } setFastifyInstance(fastify) { this.fastify = fastify; // Register accumulated routes this.routes.forEach(({ method, path, handler, middleware }) => { this.createRoute(method, path, handler, middleware); }); this.routes = []; // Clear accumulated routes } 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 || {}, }); if (!this.fastify) { console.warn('Fastify instance not available during setupMiddleware'); return; } // Global middleware if (context.config.globalMiddleware) { context.config.globalMiddleware.forEach((middleware) => { this.fastify.addHook('onRequest', middleware); }); } // Auth middleware (much lighter without heavy context attachments) this.fastify.addHook('onRequest', async (request, reply) => { const token = this.extractToken(request); if (token) { try { const session = await context.engine.checkSession(token); if (session.valid && session.entity) { request.user = session.entity; request.token = session.token; request.isAuthenticated = true; } } catch (error) { console.warn('Invalid token:', error); } } if (!request.isAuthenticated) { request.isAuthenticated = false; } }); } createRoute(method, path, handler, middleware = []) { if (!this.fastify) { // Store routes to register later when Fastify instance is available this.routes.push({ method, path, handler, middleware }); return; } const fastifyMethod = method.toLowerCase(); const routeOptions = {}; if (middleware && middleware.length > 0) { routeOptions.preHandler = middleware; } // No need to wrap handler for context attachment - everything is at adapter level now if (typeof this.fastify[fastifyMethod] === 'function') { this.fastify[fastifyMethod](path, routeOptions, 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 && 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 */ 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, reply, result, httpConfig) { const { token, redirect, success, status, cookies, ...data } = result; // Handle token (set cookie) using stored adapter config if (token) { reply.cookie(this.adapterConfig.cookieName, token, this.adapterConfig.cookieOptions); } // Handle additional cookies from result.cookies if (cookies) { Object.entries(cookies).forEach(([name, value]) => { reply.cookie(name, value); }); } // Handle redirect if (redirect) { reply.redirect(redirect); } // Determine status code const statusCode = this.getStatusCode(result, httpConfig); // Send response reply.status(statusCode).send({ success, ...data, }); } /** * Handle configurable context outputs (set cookies and headers) based on context rules */ handleConfigurableContextOutputs(request, reply, 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 && reply.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; reply.cookie(cookieName, cookieValue, cookieOptions); } else { // Simple value reply.cookie(cookieName, value); } } }); } // Set headers from result if (rule.setHeaders && reply.header) { 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); } reply.header(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); } reply.header(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 async (request, reply) => { if (!request.isAuthenticated) { return reply.status(401).send({ error: 'Authentication required' }); } }; } errorResponse(reply, error) { console.error('HTTP Adapter Error:', error); reply.status(500).send({ success: false, message: error.message || 'Internal server error', error: process.env.NODE_ENV === 'development' ? error.stack : undefined, }); } getAdapter() { return this.fastify; } } /** * Create Fastify adapter using the factory pattern with shared instance * Note: Fastify instance must be provided when calling this function */ export function createFastifyAdapterV2(fastify, frameworkAdapter) { return createHttpAdapter(frameworkAdapter); } /** * Fastify adapter class with additional features */ export class FastifyAdapterV2 { constructor(fastify, engine, config = {}, frameworkAdapter) { this.fastify = fastify; this.engine = engine; this.config = config; this.frameworkAdapter = frameworkAdapter || new FastifyFrameworkAdapter(fastify, engine); // Set the engine on the shared adapter before creating the HTTP adapter this.frameworkAdapter.setEngine(engine); // Create the adapter function using the factory this.adapterFunction = createFastifyAdapterV2(fastify, this.frameworkAdapter); // Apply the adapter to the fastify instance this.adapterFunction(engine, config); // No need for hooks to attach config - it's stored at adapter level now } /** * Get the Fastify instance */ getFastify() { return this.fastify; } /** * Add a custom route */ addRoute(method, path, handler, options = {}) { const middleware = options.middleware || []; if (options.requireAuth) { middleware.push(this.frameworkAdapter.requireAuth()); } const fastifyMethod = method.toLowerCase(); const routeOptions = {}; if (middleware.length > 0) { routeOptions.preHandler = middleware; } if (typeof this.fastify[fastifyMethod] === 'function') { this.fastify[fastifyMethod](path, routeOptions, handler); } } /** * Protection middleware for routes */ protect(options = {}) { return async (request, reply) => { // Check authentication if (!request.isAuthenticated) { return reply.status(401).send({ error: 'Authentication required' }); } // Check roles if (options.roles && options.roles.length > 0) { const userRole = request.user?.role; if (!userRole || !options.roles.includes(userRole)) { return reply.status(403).send({ error: 'Insufficient permissions' }); } } // Custom authorization if (options.authorize) { try { const isAuthorized = await options.authorize(request.user, request, reply); if (!isAuthorized) { return reply.status(403).send({ error: 'Access denied' }); } } catch (error) { console.error('Authorization error:', error); return reply .status(500) .send({ error: 'Authorization check failed' }); } } }; } /** * Register this adapter as a Fastify plugin */ static async createPlugin(engine, config = {}) { return async function fastifyReAuthPlugin(fastify, options) { new FastifyAdapterV2(fastify, engine, { ...config, ...options }); }; } } /** * Convenience function to create Fastify adapter with options */ export function createFastifyAdapter(fastify, engine, config = {}) { return new FastifyAdapterV2(fastify, engine, config); } // Export utility functions export { createRouteOverride, createCustomRoute, createAutoIntrospectionConfig, introspectReAuthEngine, createContextRule, OAuth2ContextRules, FastifyFrameworkAdapter, }; //# sourceMappingURL=fastify-adapter-v2.js.map