4.6 KiB
4.6 KiB
Phase 9: Auth Module
Objetivo
Sistema completo de autenticación y autorización para ABE como plataforma.
Roles y permisos
| Role | Sessions | Findings | Reports | Integrations | Org/Users | Settings | License |
|---|---|---|---|---|---|---|---|
| owner | CRUD | CRUD | CRUD | CRUD | CRUD | CRUD | CRUD |
| admin | CRUD | CRUD | CRUD | CRUD | CRU | CRUD | R |
| member | CR | CRU | CR | R | R | R | R |
| viewer | R | R | R | R | R | R | R |
Better Auth config
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
database: {
// Usar Kysely adapter o direct SQLite
type: 'sqlite',
url: config.db.path,
},
emailAndPassword: { enabled: true },
session: {
maxAge: config.auth.sessionMaxAge,
updateAge: 60 * 60, // refresh cada hora
},
// Organization plugin si disponible, sino implementar manual
});
Si Better Auth no soporta organizaciones directamente, implementar manualmente:
- Tabla organizations (id, name, slug, created_at)
- Tabla org_members (id, org_id, user_id, role, invited_at, joined_at)
CASL AbilityFactory
import { AbilityBuilder, createMongoAbility } from '@casl/ability';
export function defineAbilityFor(role: string) {
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
switch (role) {
case 'owner':
can('manage', 'all');
break;
case 'admin':
can('manage', 'all');
cannot('delete', 'Organization');
cannot('manage', 'License');
can('read', 'License');
break;
case 'member':
can('create', ['Session', 'Finding', 'Report']);
can('read', 'all');
can('update', 'Finding');
break;
case 'viewer':
can('read', 'all');
break;
}
return build();
}
AuthMiddleware — orden de verificación
- Check cookie de session (web UI) via Better Auth
- Check header
Authorization: Bearer <jwt> - Check header
X-ABE-API-Key: <key>(API keys para CI/CD) - Si ninguno → 401
API Key system
- POST /api/auth/api-keys — crear key (retorna key UNA vez, después solo hash)
- GET /api/auth/api-keys — listar (sin mostrar key, solo nombre + último uso)
- DELETE /api/auth/api-keys/:id — revocar
- Keys hasheadas con SHA-256 en DB
- Cada key tiene: name, permissions (array de roles), expiresAt, lastUsedAt
First-run flow
- Backend: si tabla users tiene 0 rows → flag
setupRequired = true - GET /api/auth/setup-required →
{ required: boolean } - Si required, POST /api/auth/setup con { email, password, name, orgName }
- Crea user con role owner + organization default
- Después de setup, requiere login normal
Migraciones
-- users (Better Auth maneja su propia tabla, pero añadir campos custom)
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS organizations (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS org_members (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL REFERENCES organizations(id),
user_id TEXT NOT NULL REFERENCES users(id),
role TEXT NOT NULL DEFAULT 'member',
joined_at INTEGER NOT NULL,
UNIQUE(org_id, user_id)
);
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
org_id TEXT NOT NULL REFERENCES organizations(id),
name TEXT NOT NULL,
key_hash TEXT NOT NULL,
key_prefix TEXT NOT NULL, -- primeros 8 chars para identificar
permissions TEXT NOT NULL DEFAULT '["member"]',
expires_at INTEGER,
last_used_at INTEGER,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS auth_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
token TEXT UNIQUE NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL
);
NOTA sobre Better Auth
Si Better Auth resulta demasiado complejo de integrar con Express puro o tiene incompatibilidades, implementar auth manualmente:
- argon2 para hash passwords
- crypto.randomUUID() para session tokens
- Cookie httpOnly + secure + sameSite para sessions
- Middleware custom que lee cookie → busca en auth_sessions → adjunta user a req
Esto es PERFECTAMENTE VÁLIDO. No over-engineer la auth. La prioridad es que funcione, sea seguro, y tenga RBAC.