@re-auth/http-adapters
Version:
HTTP adapters for ReAuth authentication framework
407 lines • 16.8 kB
JavaScript
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