UNPKG

@re-auth/http-adapters

Version:

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

933 lines (930 loc) 26.5 kB
import { AutoRouter, cors, json, error } from 'itty-router'; // 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 (error2) { throw new HttpAdapterError( `Step execution failed: ${error2 instanceof Error ? error2.message : String(error2)}`, 500, "STEP_EXECUTION_ERROR", { pluginName: endpoint.pluginName, stepName: endpoint.stepName, error: error2 } ); } } /** * 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 (error2) { throw new HttpAdapterError( `Session creation failed: ${error2 instanceof Error ? error2.message : String(error2)}`, 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 (error2) { 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 (error2) { throw new HttpAdapterError( `Session destruction failed: ${error2 instanceof Error ? error2.message : String(error2)}`, 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 (error2) { this.logger.error("http", "Session check failed", { hasHeaders: !!req.headers, error: error2 }); 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 (error2) { this.handleError(error2, res); } }; } /** * Error handler */ handleError(error2, res) { if (error2 instanceof HttpAdapterError) { res.status(error2.statusCode).json({ success: false, error: { code: error2.code, message: error2.message, details: error2.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() } }); } } }; var IttyRouterAdapter = class { name = "itty-router"; adapter; config; generateDeviceInfo; routePaths = []; logger; constructor(config, generateDeviceInfo, logger) { this.logger = logger || { info: () => { }, warn: () => { }, error: () => { }, success: () => { }, setEnabledTags: () => { }, destroy: () => { } }; this.adapter = new ReAuthHttpAdapter(config, this.logger); this.config = config; this.generateDeviceInfo = generateDeviceInfo; } /** * Create a new AutoRouter with CORS configured */ createRouter(basePath = "", exposeIntrospection = false, corsOptions) { const router = AutoRouter(); if (corsOptions !== void 0) { this.configureCORS(router, corsOptions); } return this.registerRoutes(router, basePath, exposeIntrospection); } /** * Register middleware on an existing router */ registerMiddleware(router, middleware) { if (middleware.before) { if (router.before) { router.before.push(...middleware.before); } else { router.before = [...middleware.before]; } } if (middleware.finally) { if (router.finally) { router.finally.push(...middleware.finally); } else { router.finally = [...middleware.finally]; } } } /** * Configure CORS on an existing router */ configureCORS(router, options) { const { preflight, corsify } = cors(options); if (router.before) { router.before.unshift(preflight); } else { router.before = [preflight]; } if (router.finally) { router.finally.push(corsify); } else { router.finally = [corsify]; } } createUserMiddleware() { return async (request) => { try { const httpReq = this.extractRequest(request); const deviceInfo = await this.generateDeviceInfoInternal(request); const user = await this.adapter.getCurrentUser(httpReq, deviceInfo); request.user = user; request.authenticated = !!user; } catch (error2) { request.user = null; request.authenticated = false; } }; } async generateDeviceInfoInternal(request) { if (!this.generateDeviceInfo) { return {}; } const deviceInfo = await this.generateDeviceInfo(request); return deviceInfo; } /** * Get current user from request */ async getCurrentUser(request) { const user = request.user; if (user !== void 0) { return user; } const httpReq = this.extractRequest(request); const deviceInfo = await this.generateDeviceInfoInternal(request); const u = await this.adapter.getCurrentUser(httpReq, deviceInfo); request.user = u; request.authenticated = !!u; return u; } /** * Extract HTTP request from itty-router Request */ extractRequest(request) { const url = new URL(request.url); const cookies = {}; const cookieHeader = request.headers.get("cookie"); if (cookieHeader) { cookieHeader.split(";").forEach((cookie) => { const parts = cookie.split("="); const key = parts.shift()?.trim(); const value = decodeURIComponent(parts.join("=")); if (key) { cookies[key] = value; } }); } return { method: request.method, url: request.url, path: url.pathname, query: request.query, params: {}, // Will be populated by router body: {}, headers: Object.fromEntries(request.headers.entries()), cookies, ip: request.headers.get("cf-connecting-ip") || request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || void 0, userAgent: request.headers.get("user-agent") || void 0 }; } /** * Send response using Response */ sendResponse(data, statusCode = 200, headers = {}) { return json(data, { status: statusCode, headers }); } /** * Handle error using Response */ handleError(err, statusCode = 500) { return error(statusCode, { success: false, error: { code: "ERROR", message: err.message }, meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() } }); } /** * Register routes on existing itty-router AutoRouter or create a new one */ registerRoutes(router, basePath = "", exposeIntrospection = false) { const targetRouter = router || AutoRouter(); if (exposeIntrospection) { targetRouter.get(`${basePath}/plugins`, this.createPluginListHandler()); targetRouter.get( `${basePath}/plugins/:plugin`, this.createPluginDetailsHandler() ); targetRouter.get( `${basePath}/introspection`, this.createIntrospectionHandler() ); } targetRouter.get(`${basePath}/health`, this.createHealthHandler()); targetRouter.get(`${basePath}/session`, this.createSessionCheckHandler()); const endpoints = this.adapter.getEndpoints(); for (const endpoint of endpoints) { const routePath = `${basePath}${endpoint.path}`; const handler = this.createStepHandler( endpoint.pluginName, endpoint.stepName ); this.routePaths.push(routePath); switch (endpoint.method) { case "POST": targetRouter.post(routePath, handler); break; case "GET": targetRouter.get(routePath, handler); break; case "PUT": targetRouter.put(routePath, handler); break; case "PATCH": targetRouter.patch(routePath, handler); break; case "DELETE": targetRouter.delete(routePath, handler); break; default: throw new Error(`Unsupported HTTP method: ${endpoint.method}`); } } this.logger.info("http", "Route paths registered", { routes: this.routePaths }); return targetRouter; } /** * Get all registered routes */ getRoutes() { return this.routePaths; } /** * Create step execution handler */ createStepHandler(pluginName, stepName) { return async (request) => { try { const httpReq = this.extractRequest(request); httpReq.params = request.params || {}; if (["POST", "PUT", "PATCH"].includes(request.method)) { try { httpReq.body = await request.json(); } catch { httpReq.body = {}; } } const deviceInfo = await this.generateDeviceInfoInternal(request); const result = await this.adapter.executeAuthStep( httpReq, deviceInfo ); const headers = {}; const cookies = []; if (result.secret && typeof result.secret === "object") { for (const [key, value] of Object.entries(result.secret)) { cookies.push( this.createCookieHeader(key, value, { httpOnly: true, secure: true, path: "/", sameSite: "Lax", maxAge: 600 // 10 minutes for OAuth state/verifier }) ); } } if (this.config.cookie && result.data?.token) { const cookie = this.config.cookie; const options = cookie.options; const token = result.data.token; if (typeof token === "string") { cookies.push(this.createCookieHeader(cookie.name, token, options)); } else { cookies.push( this.createCookieHeader(cookie.name, token.accessToken, options) ); if (cookie.refreshOptions && token.refreshToken) { const refreshOptions = cookie.refreshOptions; cookies.push( this.createCookieHeader( cookie.refreshTokenName || "refreshToken", token.refreshToken, refreshOptions ) ); } } } if (cookies.length > 0) { headers["Set-Cookie"] = cookies.join(", "); } if (result.redirect) { return new Response(null, { status: result.status, headers: { ...headers, Location: result.redirect } }); } return this.sendResponse(result, result.status, headers); } catch (error2) { return this.handleError(error2); } }; } /** * Create session check handler */ createSessionCheckHandler() { return async (request) => { try { const httpReq = this.extractRequest(request); const deviceInfo = await this.generateDeviceInfoInternal(request); const result = await this.adapter.checkSession( httpReq, deviceInfo ); return this.sendResponse(result); } catch (error2) { return this.handleError(error2); } }; } /** * Create plugin list handler */ createPluginListHandler() { return async () => { try { const result = await this.adapter.listPlugins(); return this.sendResponse(result); } catch (error2) { return this.handleError(error2); } }; } /** * Create plugin details handler */ createPluginDetailsHandler() { return async (request) => { try { const params = request.params || {}; const plugin = params.plugin; if (!plugin) { throw new Error("Plugin name is required"); } const result = await this.adapter.getPlugin(plugin); return this.sendResponse(result); } catch (error2) { return this.handleError(error2); } }; } /** * Create introspection handler */ createIntrospectionHandler() { return async () => { try { const result = await this.adapter.getIntrospection(); return this.sendResponse(result); } catch (error2) { return this.handleError(error2); } }; } /** * Create health check handler */ createHealthHandler() { return async () => { try { const result = await this.adapter.healthCheck(); return this.sendResponse(result); } catch (error2) { return this.handleError(error2); } }; } /** * Create cookie header string */ createCookieHeader(name, value, options) { let cookie = `${name}=${encodeURIComponent(value)}`; if (options.path) cookie += `; Path=${options.path}`; if (options.domain) cookie += `; Domain=${options.domain}`; if (options.maxAge) cookie += `; Max-Age=${options.maxAge}`; if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`; if (options.httpOnly) cookie += "; HttpOnly"; if (options.secure) cookie += "; Secure"; if (options.sameSite) cookie += `; SameSite=${options.sameSite}`; if (options.partitioned) cookie += "; Partitioned"; return cookie; } /** * Get the base adapter instance */ getAdapter() { return this.adapter; } }; function reAuthRouter(config, generateDeviceInfo, logger) { const adapter = new IttyRouterAdapter(config, generateDeviceInfo, logger); return adapter; } var createReAuthRouter = (routerConfig, config, generateDeviceInfo, logger) => { const adapter = new IttyRouterAdapter(config, generateDeviceInfo, logger); const router = adapter.createRouter( routerConfig.basePath, routerConfig.exposeIntrospection, routerConfig.corsOptions ); return { router, adapter }; }; export { IttyRouterAdapter, createReAuthRouter, reAuthRouter }; //# sourceMappingURL=itty-router.js.map //# sourceMappingURL=itty-router.js.map