UNPKG

@re-auth/http-adapters

Version:

HTTP adapters for ReAuth authentication framework

222 lines 8.76 kB
import fastifyCookie from '@fastify/cookie'; export class FastifyAuthAdapter { constructor(fastify, engine, config = {}) { this.fastify = fastify; this.engine = engine; this.config = { prefix: '/auth', cookieName: 'auth_token', cookieOptions: { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 24 * 7, // 7 days }, ...config, }; this.setup(); } async setup() { // Register cookie plugin await this.fastify.register(fastifyCookie, { secret: process.env.COOKIE_SECRET, parseOptions: this.config.cookieOptions, }); // Add decorators this.fastify.decorateRequest('user', undefined); this.fastify.decorateRequest('token', undefined); this.fastify.decorateRequest('isAuthenticated', false); // Add auth hook this.fastify.addHook('onRequest', this.authHook.bind(this)); // Register routes this.registerRoutes(); } async authHook(request, reply) { const token = this.extractToken(request); if (token) { try { const session = await this.engine.checkSession(token); if (session.valid && session.entity) { request.user = session.entity; request.token = session.token; request.isAuthenticated = true; } } catch (error) { request.log.warn('Invalid token:', error); } } } registerRoutes() { const plugins = this.engine.getAllPlugins(); this.fastify.register(async (instance) => { // Add auth utility methods instance.decorate('authenticate', this.authenticate.bind(this)); // Register plugin routes plugins.forEach((plugin) => { plugin.steps.forEach((step) => { if (!step.protocol?.http) return; const path = `/${plugin.name}/${step.name}`; const method = step.protocol.http.method.toLowerCase(); // Create route config const routeConfig = { method: step.protocol.http.method, url: path, handler: this.createStepHandler(plugin.name, step.name, step.protocol.http), }; // Add auth preValidation if required if (step.protocol.http.auth) { routeConfig.preValidation = [this.authenticate]; } // Register route instance.route(routeConfig); }); }); }, { prefix: this.config.prefix }); } createStepHandler(pluginName, stepName, httpConfig) { return async (request, reply) => { try { // Extract inputs from request const inputs = await this.extractInputs(request, pluginName, stepName); // Add auth context if authenticated if (request.isAuthenticated) { inputs.entity = request.user; inputs.token = request.token; } // Execute the step const result = await this.engine.executeStep(pluginName, stepName, inputs); // Handle the response return this.handleStepResponse(request, reply, result, httpConfig); } catch (error) { request.log.error(`Error in ${pluginName}.${stepName}:`, error); return this.errorResponse(reply, error); } }; } async extractInputs(request, pluginName, stepName) { const expectedInputs = this.engine.getStepInputs(pluginName, stepName); const inputs = {}; // Extract from body, query, and params expectedInputs.forEach((inputName) => { if (request.body && typeof request.body === 'object' && inputName in request.body) { inputs[inputName] = request.body[inputName]; } else if (request.query && typeof request.query === 'object' && inputName in request.query) { inputs[inputName] = request.query[inputName]; } else if (request.params && typeof request.params === 'object' && inputName in request.params) { inputs[inputName] = request.params[inputName]; } }); // Validate required inputs const missingInputs = expectedInputs.filter((input) => inputs[input] === undefined || inputs[input] === null || inputs[input] === ''); if (missingInputs.length > 0) { throw new Error(`Missing required inputs: ${missingInputs.join(', ')}`); } return inputs; } handleStepResponse(request, reply, result, httpConfig) { const { token, redirect, success, status, ...data } = result; // Handle token (set cookie) if (token) { reply.setCookie(this.config.cookieName, token, this.config.cookieOptions); } //TODO: this stills need digging, Might not be correct or be a bug // Handle logout (clear cookie) if (status === 'logged_out' || (success && !token && request.token)) { reply.clearCookie(this.config.cookieName, this.config.cookieOptions); } // Handle redirect if (redirect) { return reply.redirect(redirect); } // Determine status code const statusCode = this.getStatusCode(result, httpConfig); // Return JSON response return reply.code(statusCode).send({ success, ...data, }); } getStatusCode(result, httpConfig) { // Check if step defines custom status codes if (httpConfig && httpConfig[result.status]) { return httpConfig[result.status]; } // Default status codes if (!result.success) { return result.status === 'unauthorized' ? 401 : 400; } return result.status === 'created' ? 201 : 200; } errorResponse(reply, error) { if (error.name === 'InputValidationError') { return reply.code(400).send({ success: false, error: 'Validation Error', message: error.message, }); } return reply.code(500).send({ success: false, error: 'Internal Server Error', message: process.env.NODE_ENV === 'development' ? error.message : undefined, }); } authenticate(request, reply, done) { if (!request.isAuthenticated) { return reply.code(401).send({ success: false, error: 'Unauthorized' }); } done(); } extractToken(request) { // 1. Check Authorization header const authHeader = request.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { return authHeader.substring(7); } // 2. Check cookies if (request.cookies?.[this.config.cookieName]) { return request.cookies[this.config.cookieName] || null; } // // 3. Check query parameter // if (request.query?.token && typeof request.query.token === 'string') { // return request.query.token; // } return null; } protect(options) { return async (request, reply, done) => { if (!request.isAuthenticated) { return reply.code(401).send({ success: false, error: 'Unauthorized' }); } if (options.roles && !options.roles.includes(request.user.role)) { return reply.code(403).send({ success: false, error: 'Forbidden' }); } if (options.authorize) { const authorized = await options.authorize(request.user, request, reply, done); if (!authorized) { return reply.code(403).send({ success: false, error: 'Forbidden' }); } } done(); }; } } export async function createFastifyAdapter(fastify, engine, config) { return new FastifyAuthAdapter(fastify, engine, config); } export default createFastifyAdapter; export * from './fastify-adapter-v2'; //# sourceMappingURL=index.js.map