Files
boc/aamos-ledger/auth.mjs
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- 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
2026-06-29 16:24:48 +00:00

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 };