/** * ═══════════════════════════════════════════════════════════════════════════ * AAMOS Ledger — Auth & RBAC Module * ═══════════════════════════════════════════════════════════════════════════ * * Stöder: * • RS256 (production) — verifierar JWT med publik nyckel * • HS256 (development) — verifierar JWT med delad secret * • Service-to-Service — intern token i Authorization-header * * Roller: * • admin — fulla rättigheter * • accountant — skapa/kontera verifikationer * • viewer — läs-only */ import jwt from 'jsonwebtoken'; import { readFileSync } from 'fs'; // ── Konfiguration (läses dynamiskt för testbarhet) ───────────────────────── function getNodeEnv() { return process.env.NODE_ENV || 'development'; } function getJwtSecret() { return process.env.AMOS_JWT_SECRET; } function getJwtPublicKeyPath() { return process.env.JWT_PUBLIC_KEY_PATH; } function getInternalToken() { return process.env.LEDGER_INTERNAL_TOKEN; } function loadPublicKey() { const path = getJwtPublicKeyPath(); if (!path) return null; try { return readFileSync(path, 'utf8'); } catch (e) { console.warn('[auth] Kunde inte läsa publik nyckel:', e.message); return null; } } // ── Validering av miljövariabler vid startup ──────────────────────────────── export function validateAuthConfig() { const errors = []; const nodeEnv = getNodeEnv(); const jwtSecret = getJwtSecret(); const publicKey = loadPublicKey(); const internalToken = getInternalToken(); if (nodeEnv === 'production') { if (!publicKey) { errors.push('JWT_PUBLIC_KEY_PATH måste sättas i production (RS256)'); } if (!internalToken) { errors.push('LEDGER_INTERNAL_TOKEN måste sättas i production'); } } else { if (!jwtSecret || jwtSecret.length < 32) { errors.push('AMOS_JWT_SECRET måste vara minst 32 tecken i development'); } } if (errors.length > 0) { console.error('[auth] Konfigurationsfel:', errors.join('; ')); throw new Error('Auth-konfiguration ofullständig: ' + errors.join('; ')); } console.log(`[auth] Konfiguration OK (${nodeEnv === 'production' ? 'RS256' : 'HS256'})`); } // ── Roll-hierarki ─────────────────────────────────────────────────────────── const ROLE_HIERARCHY = { admin: ['admin', 'accountant', 'viewer'], accountant: ['accountant', 'viewer'], viewer: ['viewer'], }; function hasRole(userRoles, requiredRole) { if (!Array.isArray(userRoles)) userRoles = [userRoles]; const effectiveRoles = new Set(); for (const r of userRoles) { const expanded = ROLE_HIERARCHY[r] || [r]; expanded.forEach(er => effectiveRoles.add(er)); } return effectiveRoles.has(requiredRole); } // ── Middleware: Autentisering ─────────────────────────────────────────────── export function authenticate(req, res, next) { const authHeader = req.headers['authorization']; const nodeEnv = getNodeEnv(); const jwtSecret = getJwtSecret(); const publicKey = loadPublicKey(); const internalToken = getInternalToken(); // 1. Service-to-service token (högst prioritet) if (internalToken && authHeader === `Bearer ${internalToken}`) { req.auth = { type: 'service', roles: ['admin'], user_id: 'system', tenant_id: req.headers['x-tenant-id'] || 'wavult-group' }; return next(); } // 2. JWT-verifiering if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ ok: false, error: 'Authorization-header saknas eller är ogiltig' }); } const token = authHeader.slice(7); try { let decoded; if (nodeEnv === 'production') { if (!publicKey) { return res.status(500).json({ ok: false, error: 'Publik nyckel ej tillgänglig' }); } decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] }); } else { if (!jwtSecret) { return res.status(500).json({ ok: false, error: 'JWT-secret ej konfigurerat' }); } decoded = jwt.verify(token, jwtSecret, { algorithms: ['HS256'] }); } // Validera att JWT innehåller nödvändiga claims if (!decoded.sub && !decoded.user_id) { return res.status(401).json({ ok: false, error: 'JWT saknar user_id/sub claim' }); } const roles = decoded.roles || decoded.role || []; if (nodeEnv === 'production' && (!roles || roles.length === 0)) { return res.status(403).json({ ok: false, error: 'JWT saknar roller — åtkomst nekad' }); } req.auth = { type: 'user', user_id: decoded.sub || decoded.user_id, roles: Array.isArray(roles) ? roles : [roles], tenant_id: decoded.tenant_id || req.headers['x-tenant-id'] || 'wavult-group', email: decoded.email, }; next(); } catch (e) { if (e.name === 'TokenExpiredError') { return res.status(401).json({ ok: false, error: 'Token har gått ut' }); } if (e.name === 'JsonWebTokenError') { return res.status(401).json({ ok: false, error: 'Ogiltig token' }); } console.error('[auth] JWT-verifieringsfel:', e.message); return res.status(401).json({ ok: false, error: 'Autentisering misslyckades' }); } } // ── Middleware: RBAC ──────────────────────────────────────────────────────── export function requireRole(role) { return (req, res, next) => { if (!req.auth) { return res.status(401).json({ ok: false, error: 'Ej autentiserad' }); } if (!hasRole(req.auth.roles, role)) { return res.status(403).json({ ok: false, error: `Åtkomst nekad — kräver roll: ${role}` }); } next(); }; } // ── Middleware: Tenant-isolering ──────────────────────────────────────────── export function requireTenantMatch(req, res, next) { if (!req.auth) { return res.status(401).json({ ok: false, error: 'Ej autentiserad' }); } const headerTenant = req.headers['x-tenant-id']; const bodyTenant = req.body?.tenant_id; const authTenant = req.auth.tenant_id; // Service-konton får komma åt alla tenants if (req.auth.type === 'service') { // Sätt tenant från header om det finns, annars default req.tenant_id = headerTenant || bodyTenant || authTenant; return next(); } // Användare får endast komma åt sin egen tenant const requestedTenant = headerTenant || bodyTenant || authTenant; if (requestedTenant && requestedTenant !== authTenant) { return res.status(403).json({ ok: false, error: 'Tenant-åtkomst nekad' }); } req.tenant_id = authTenant; next(); } // ── Export ────────────────────────────────────────────────────────────────── export default { authenticate, requireRole, requireTenantMatch, validateAuthConfig };