UNPKG

@re-auth/http-adapters

Version:

HTTP protocol adapters for ReAuth - framework-agnostic integration with Express, Fastify, and Hono

990 lines (987 loc) 27.7 kB
// src/types.ts var HttpAdapterError = class extends Error { constructor(message, statusCode = 500, code = "INTERNAL_ERROR", details) { super(message); this.statusCode = statusCode; this.code = code; this.details = details; this.name = "HttpAdapterError"; } }; var ValidationError = class extends HttpAdapterError { constructor(message, details) { super(message, 400, "VALIDATION_ERROR", details); this.name = "ValidationError"; } }; var AuthenticationError = class extends HttpAdapterError { constructor(message, details) { super(message, 401, "AUTHENTICATION_ERROR", details); this.name = "AuthenticationError"; } }; var NotFoundError = class extends HttpAdapterError { constructor(message, details) { super(message, 404, "NOT_FOUND", details); this.name = "NotFoundError"; } }; // src/base-adapter.ts var ReAuthHttpAdapter = class { engine; config; endpoints = /* @__PURE__ */ new Map(); logger; constructor(config, logger) { this.engine = config.engine; this.config = config; this.logger = logger; this.buildEndpoints(); } /** * Get adapter configuration */ getConfig() { return this.config; } /** * Build endpoint map from registered plugins */ buildEndpoints() { const plugins = this.engine.getAllPlugins(); for (const plugin of plugins) { if (!plugin.steps) continue; for (const step of plugin.steps) { const endpoint = { pluginName: plugin.name, stepName: step.name, path: `${this.config.basePath || ""}/${plugin.name}/${step.name}`, method: step.protocol?.http?.method || "POST", requiresAuth: Boolean(step.protocol?.http?.auth), description: step.description, inputSchema: step.validationSchema, outputSchema: step.outputs }; const key = `${plugin.name}:${step.name}`; this.endpoints.set(key, endpoint); } } } /** * Get all available endpoints */ getEndpoints() { return Array.from(this.endpoints.values()); } /** * Get endpoint by plugin and step name */ getEndpoint(pluginName, stepName) { return this.endpoints.get(`${pluginName}:${stepName}`); } /** * Execute authentication step */ async executeAuthStep(req, deviceInfo) { const endpoint = this.getEndpoint(req.plugin.name, req.plugin.step); if (!endpoint) { throw new NotFoundError( `Endpoint not found: ${req.plugin.name}/${req.plugin.step}` ); } if (endpoint.requiresAuth) { await this.requireAuthentication(req, deviceInfo); } else { await this.getCurrentUser(req, deviceInfo); } const input = this.extractStepInput(req, endpoint, deviceInfo); try { const output = await this.engine.executeStep( endpoint.pluginName, endpoint.stepName, input ); const pl = this.engine.getPlugin(endpoint.pluginName); const step = pl?.steps?.find((s) => s.name === endpoint.stepName); const status = step?.protocol?.http?.codes?.[output.status] || 200; return { success: true, data: { ...output, sessionToken: output.token }, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() }, status, redirect: output.redirect, // Include redirect URL for OAuth flow secret: output.secret // Include cookies to set for OAuth flow }; } catch (error) { throw new HttpAdapterError( `Step execution failed: ${error instanceof Error ? error.message : String(error)}`, 500, "STEP_EXECUTION_ERROR", { pluginName: endpoint.pluginName, stepName: endpoint.stepName, error } ); } } /** * Create a new session */ async createSession(req, deviceInfo) { const { subjectType, subjectId, ttlSeconds } = req.body || {}; if (!subjectType || !subjectId) { throw new ValidationError("subjectType and subjectId are required"); } try { const token = await this.engine.createSessionFor( subjectType, subjectId, ttlSeconds, deviceInfo ); return { success: true, data: { valid: true, token, expiresAt: ttlSeconds ? new Date(Date.now() + ttlSeconds * 1e3).toISOString() : void 0 }, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() } }; } catch (error) { throw new HttpAdapterError( `Session creation failed: ${error instanceof Error ? error.message : String(error)}`, 500, "SESSION_CREATION_ERROR" ); } } /** * Check session validity */ async checkSession(req, deviceInfo) { const token = this.extractSessionToken(req); if (!token) { return { success: false, data: { valid: false }, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() } }; } try { const result = await this.engine.checkSession(token, deviceInfo); if (!result.subject) { return { success: false, data: { valid: false }, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() } }; } return { success: true, data: { valid: result.valid, subject: result.subject, token: result.token, metadata: { payload: result.payload, type: result.type } }, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() } }; } catch (error) { return { success: false, data: { valid: false }, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() } }; } } /** * Destroy current session */ async destroySession(req) { const token = this.extractSessionToken(req); if (!token) { throw new ValidationError("No session token provided"); } try { await this.engine.getSessionService().destroySession(token); return { success: true, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() } }; } catch (error) { throw new HttpAdapterError( `Session destruction failed: ${error instanceof Error ? error.message : String(error)}`, 500, "SESSION_DESTRUCTION_ERROR" ); } } /** * List all available plugins */ async listPlugins() { const plugins = this.engine.getAllPlugins(); const data = plugins.map((plugin) => ({ name: plugin.name, description: `${plugin.name} authentication plugin`, steps: (plugin.steps || []).map((step) => ({ name: step.name, description: step.description, method: step.protocol?.http?.method || "POST", path: `${this.config.basePath || ""}/auth/${plugin.name}/${step.name}`, requiresAuth: Boolean(step.protocol?.http?.auth) })) })); return { success: true, data, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() } }; } /** * Get specific plugin details */ async getPlugin(pluginName) { const plugin = this.engine.getPlugin(pluginName); if (!plugin) { throw new NotFoundError(`Plugin not found: ${pluginName}`); } const data = { name: plugin.name, description: `${plugin.name} authentication plugin`, steps: (plugin.steps || []).map((step) => ({ name: step.name, description: step.description, method: step.protocol?.http?.method || "POST", path: `${this.config.basePath || ""}/auth/${plugin.name}/${step.name}`, requiresAuth: Boolean(step.protocol?.http?.auth), inputs: step.inputs || [], inputSchema: step.validationSchema?.toJsonSchema?.() || {}, outputSchema: step.outputs?.toJsonSchema?.() || {} })) }; return { success: true, data, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() } }; } /** * Get full introspection data */ async getIntrospection() { const introspectionData = this.engine.getIntrospectionData(); const httpConfig = { basePath: this.config.basePath || "", tokenConfig: { header: { accessTokenHeader: this.config.headerToken?.accesssTokenHeader || "authorization", refreshTokenHeader: this.config.headerToken?.refreshTokenHeader || "x-refresh-token", useBearer: true // Default to Bearer token in Authorization header }, cookie: { accessTokenName: this.config.cookie?.name || "reauth-session", refreshTokenName: this.config.cookie?.refreshTokenName || "reauth-refresh", enabled: !!this.config.cookie } } }; return { success: true, data: { ...introspectionData, httpConfig }, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() } }; } /** * Health check endpoint */ async healthCheck() { return { success: true, data: { status: "healthy", version: "2.0.0", plugins: this.engine.getAllPlugins().length, endpoints: this.endpoints.size }, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() } }; } /** * Extract session token from request */ extractSessionToken(req) { let accessToken = null; let refreshToken = null; const authHeader = req.headers.authorization; if (authHeader?.startsWith("Bearer ")) { accessToken = authHeader.substring(7); } if (!accessToken && req.headers[this.config.headerToken?.accesssTokenHeader || "x-access-token"]) { accessToken = req.headers[this.config.headerToken?.accesssTokenHeader || "x-access-token"]; } if (!refreshToken && req.headers[this.config.headerToken?.refreshTokenHeader || "x-refresh-token"]) { refreshToken = req.headers[this.config.headerToken?.refreshTokenHeader || "x-refresh-token"]; } if (!accessToken && req.cookies?.[this.config.cookie?.name || "reauth-session"]) { accessToken = req.cookies[this.config.cookie?.name || "reauth-session"]; } if (!refreshToken && req.cookies?.[this.config.cookie?.refreshTokenName || "reauth-refresh"]) { refreshToken = req.cookies[this.config.cookie?.refreshTokenName || "reauth-refresh"]; } if (!accessToken) { this.logger.info("http", "No access token found in request", { path: req.path, hasHeaders: !!req.headers }); return null; } if (accessToken && refreshToken) { this.logger.info("http", "Access token and refresh token found", { path: req.path, hasHeaders: !!req.headers }); return { accessToken, refreshToken }; } if (accessToken && !refreshToken) { this.logger.info("http", "Access token found without refresh token", { path: req.path, hasHeaders: !!req.headers }); return accessToken; } this.logger.info("http", "No access token or refresh token found", { path: req.path, hasHeaders: !!req.headers }); return null; } /** * Extract step input from request */ extractStepInput(req, endpoint, deviceInfo) { const input = { ...req.body, ...req.query, ...req.params, ...req.cookies // Add cookies so OAuth flow can read oauth_state, etc. }; const token = this.extractSessionToken(req); if (token) { input.token = token; } if (deviceInfo) { input.deviceInfo = deviceInfo; } if (req.ip) { input.ip = req.ip; } if (req.userAgent) { input.userAgent = req.userAgent; } return input; } /** * Get current authenticated user from request */ async getCurrentUser(req, deviceInfo) { const token = this.extractSessionToken(req); if (!token) { this.logger.info("http", "No token found in getCurrentUser", { path: req.path, hasHeaders: !!req.headers }); return null; } try { const session = await this.engine.checkSession(token, deviceInfo); if (!session.valid || !session.subject) { this.logger.warn("http", "Session is not valid or subject not found", { hasHeaders: !!req.headers }); return null; } return { subject: session.subject, token: session.token || token, valid: session.valid, metadata: { payload: session.payload, type: session.type } }; } catch (error) { this.logger.error("http", "Session check failed", { hasHeaders: !!req.headers, error }); return null; } } /** * Authenticate request and return user or throw error */ async requireAuthentication(req, deviceInfo) { const user = await this.getCurrentUser(req, deviceInfo); if (!user) { this.logger.warn("http", "Authentication required but no user found", { path: req.path, hasHeaders: !!req.headers }); throw new AuthenticationError("Authentication required"); } return user; } /** * Create framework-specific adapter */ createAdapter(adapter) { return adapter; } /** * Generic route handler factory */ createRouteHandler(handler) { return async (req, res) => { try { const result = await handler(req, res); if (result) { res.status(200).json(result); } } catch (error) { this.handleError(error, res); } }; } /** * Error handler */ handleError(error, res) { if (error instanceof HttpAdapterError) { res.status(error.statusCode).json({ success: false, error: { code: error.code, message: error.message, details: error.details }, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() } }); } else { res.status(500).json({ success: false, error: { code: "INTERNAL_ERROR", message: "An unexpected error occurred" }, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() } }); } } }; // src/utils/factory.ts function createReAuthHttpAdapter(config, logger) { const defaultConfig = { basePath: "/api/v2", cors: { origin: true, credentials: true, allowedHeaders: ["Content-Type", "Authorization"], methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] }, rateLimit: { windowMs: 15 * 60 * 1e3, // 15 minutes max: 100, message: "Too many requests, please try again later." }, security: { helmet: true, sanitizeInput: true, sanitizeOutput: false }, validation: { validateInput: true, maxPayloadSize: 1024 * 1024, // 1MB sanitizeFields: ["email", "username", "name", "description"] }, ...config }; return new ReAuthHttpAdapter(defaultConfig, logger); } function generateApiSpec(adapter) { const endpoints = adapter.getEndpoints(); const paths = {}; for (const endpoint of endpoints) { const path = endpoint.path; const method = endpoint.method.toLowerCase(); if (!paths[path]) { paths[path] = {}; } paths[path][method] = { summary: `Execute ${endpoint.stepName} step for ${endpoint.pluginName} plugin`, description: endpoint.description || `Execute authentication step`, tags: [endpoint.pluginName], parameters: [ { name: "plugin", in: "path", required: true, schema: { type: "string" }, description: "Plugin name" }, { name: "step", in: "path", required: true, schema: { type: "string" }, description: "Step name" } ], requestBody: { content: { "application/json": { schema: endpoint.inputSchema || { type: "object" } } } }, responses: { 200: { description: "Successful response", content: { "application/json": { schema: { type: "object", properties: { success: { type: "boolean" }, data: endpoint.outputSchema || { type: "object" }, meta: { type: "object", properties: { timestamp: { type: "string", format: "date-time" } } } } } } } }, 400: { description: "Validation error", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }, 401: { description: "Authentication required", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }, 500: { description: "Internal server error", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } } }, security: endpoint.requiresAuth ? [{ bearerAuth: [] }] : [] }; } paths["/session"] = { get: { summary: "Check session validity", description: "Verify if the current session is valid", tags: ["Session Management"], security: [{ bearerAuth: [] }], responses: { 200: { description: "Session check result", content: { "application/json": { schema: { $ref: "#/components/schemas/SessionResponse" } } } } } }, post: { summary: "Create new session", description: "Create a new session for a subject", tags: ["Session Management"], requestBody: { content: { "application/json": { schema: { type: "object", properties: { subjectType: { type: "string" }, subjectId: { type: "string" }, ttlSeconds: { type: "number" } }, required: ["subjectType", "subjectId"] } } } }, responses: { 201: { description: "Session created successfully", content: { "application/json": { schema: { $ref: "#/components/schemas/SessionResponse" } } } } } }, delete: { summary: "Destroy session", description: "Destroy the current session", tags: ["Session Management"], security: [{ bearerAuth: [] }], responses: { 200: { description: "Session destroyed successfully", content: { "application/json": { schema: { $ref: "#/components/schemas/SuccessResponse" } } } } } } }; paths["/plugins"] = { get: { summary: "List all plugins", description: "Get a list of all available authentication plugins", tags: ["Plugin Introspection"], responses: { 200: { description: "List of plugins", content: { "application/json": { schema: { $ref: "#/components/schemas/PluginListResponse" } } } } } } }; paths["/plugins/{plugin}"] = { get: { summary: "Get plugin details", description: "Get detailed information about a specific plugin", tags: ["Plugin Introspection"], parameters: [ { name: "plugin", in: "path", required: true, schema: { type: "string" }, description: "Plugin name" } ], responses: { 200: { description: "Plugin details", content: { "application/json": { schema: { $ref: "#/components/schemas/PluginDetailsResponse" } } } }, 404: { description: "Plugin not found", content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } } } } }; paths["/health"] = { get: { summary: "Health check", description: "Get the health status of the authentication service", tags: ["Health"], responses: { 200: { description: "Health status", content: { "application/json": { schema: { $ref: "#/components/schemas/HealthResponse" } } } } } } }; return { openapi: "3.0.3", info: { title: "ReAuth V2 HTTP API", description: "HTTP API for ReAuth V2 Authentication Engine", version: "2.0.0", contact: { name: "ReAuth", url: "https://github.com/SOG-web/reauth" }, license: { name: "MIT", url: "https://opensource.org/licenses/MIT" } }, paths, components: { securitySchemes: { bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" } }, schemas: { ErrorResponse: { type: "object", properties: { success: { type: "boolean", example: false }, error: { type: "object", properties: { code: { type: "string" }, message: { type: "string" }, details: { type: "object" } } }, meta: { type: "object", properties: { timestamp: { type: "string", format: "date-time" } } } } }, SuccessResponse: { type: "object", properties: { success: { type: "boolean", example: true }, meta: { type: "object", properties: { timestamp: { type: "string", format: "date-time" } } } } }, SessionResponse: { type: "object", properties: { success: { type: "boolean", example: true }, data: { type: "object", properties: { valid: { type: "boolean" }, subject: { type: "object" }, token: { type: "string" }, expiresAt: { type: "string", format: "date-time" } } }, meta: { type: "object", properties: { timestamp: { type: "string", format: "date-time" } } } } }, PluginListResponse: { type: "object", properties: { success: { type: "boolean", example: true }, data: { type: "array", items: { type: "object", properties: { name: { type: "string" }, description: { type: "string" }, steps: { type: "array", items: { type: "object", properties: { name: { type: "string" }, description: { type: "string" }, method: { type: "string" }, path: { type: "string" }, requiresAuth: { type: "boolean" } } } } } } }, meta: { type: "object", properties: { timestamp: { type: "string", format: "date-time" } } } } }, PluginDetailsResponse: { type: "object", properties: { success: { type: "boolean", example: true }, data: { type: "object", properties: { name: { type: "string" }, description: { type: "string" }, steps: { type: "array", items: { type: "object", properties: { name: { type: "string" }, description: { type: "string" }, method: { type: "string" }, path: { type: "string" }, requiresAuth: { type: "boolean" }, inputs: { type: "array", items: { type: "string" } }, inputSchema: { type: "object" }, outputSchema: { type: "object" } } } } } }, meta: { type: "object", properties: { timestamp: { type: "string", format: "date-time" } } } } }, HealthResponse: { type: "object", properties: { success: { type: "boolean", example: true }, data: { type: "object", properties: { status: { type: "string", example: "healthy" }, version: { type: "string" }, plugins: { type: "number" }, endpoints: { type: "number" } } }, meta: { type: "object", properties: { timestamp: { type: "string", format: "date-time" } } } } } } } }; } function getPluginNames(endpoints) { const names = /* @__PURE__ */ new Set(); for (const endpoint of endpoints) { names.add(endpoint.pluginName); } return Array.from(names).sort(); } function groupEndpointsByPlugin(endpoints) { const groups = {}; for (const endpoint of endpoints) { if (!groups[endpoint.pluginName]) { groups[endpoint.pluginName] = []; } groups[endpoint.pluginName].push(endpoint); } return groups; } function validateConfig(config) { const errors = []; if (!config.engine) { errors.push("engine is required"); } if (config.basePath && !config.basePath.startsWith("/")) { errors.push("basePath must start with /"); } if (config.rateLimit?.max && config.rateLimit.max <= 0) { errors.push("rateLimit.max must be greater than 0"); } if (config.rateLimit?.windowMs && config.rateLimit.windowMs <= 0) { errors.push("rateLimit.windowMs must be greater than 0"); } if (config.validation?.maxPayloadSize && config.validation.maxPayloadSize <= 0) { errors.push("validation.maxPayloadSize must be greater than 0"); } return { valid: errors.length === 0, errors }; } export { createReAuthHttpAdapter, generateApiSpec, getPluginNames, groupEndpointsByPlugin, validateConfig }; //# sourceMappingURL=factory.js.map //# sourceMappingURL=factory.js.map