UNPKG

@re-auth/http-adapters

Version:

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

531 lines (528 loc) 14.8 kB
'use strict'; // 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() } }); } } }; exports.ReAuthHttpAdapter = ReAuthHttpAdapter; //# sourceMappingURL=base-adapter.cjs.map //# sourceMappingURL=base-adapter.cjs.map