bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
193 lines
7.5 KiB
JavaScript
193 lines
7.5 KiB
JavaScript
/**
|
|
* ═══════════════════════════════════════════════════════════════════════════
|
|
* 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 };
|