Auth completo (Argon2id + JWT/refresh rotativo), permissões, Companies e OpenAPI
- Auth (apps/api/src/auth/): Argon2id com parâmetros versionados
(ADR-0005), access token JWT de 15min, refresh token opaco de 256 bits
com rotação e detecção de replay (reuso de token já rotacionado revoga
toda a cadeia de sessões do usuário). Endpoints /api/v1/auth/
{login,refresh,logout,me}.
- Permissões resource+action+scope (ADR-0004) via PermissionGuard,
resolvidas do banco a cada request (nunca cacheadas no JWT) — mudança
de papel tem efeito imediato. super_admin com bypass hardcoded,
espelhando a trava já existente no banco.
- Companies (migration + CRUD): leitura aberta a qualquer autenticado,
escrita restrita a companies:manage; validação de CNPJ via
packages/domain-shared (dígito verificador).
- SeedService: cria o primeiro super_admin a partir de
EDEN_SUPERADMIN_EMAIL/PASSWORD só quando `users` está vazia.
- OpenAPI/Swagger em /api/docs; ValidationPipe global com
whitelist+forbidNonWhitelisted (proteção contra mass-assignment).
- packages/database: repositórios (roles/users/sessions/audit/companies)
reutilizáveis por api e worker.
Validado manualmente contra a stack real (não só typecheck): login com
senha errada → 401 genérico; refresh rotaciona e invalida o token
anterior; reuso do token antigo → replay detectado, cadeia revogada;
logout idempotente; audit log com a sequência completa de eventos;
CNPJ inválido rejeitado; mass-assignment rejeitado.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
44
apps/api/src/auth/password.service.ts
Normal file
44
apps/api/src/auth/password.service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import * as argon2 from "argon2";
|
||||
|
||||
/**
|
||||
* Argon2id password hashing with versioned parameters (ADR-0005). Bumping
|
||||
* CURRENT_ALGO_VERSION and its entry in PARAMS_BY_VERSION lets us raise
|
||||
* cost over time without invalidating existing hashes — verification
|
||||
* always uses the parameters embedded in argon2's own encoded hash
|
||||
* string, and a stored user is only re-hashed with the new parameters on
|
||||
* their next successful login (see needsRehash()).
|
||||
*/
|
||||
const PARAMS_BY_VERSION: Record<number, argon2.Options> = {
|
||||
1: {
|
||||
type: argon2.argon2id,
|
||||
memoryCost: 19456, // 19 MiB — OWASP minimum recommendation for argon2id
|
||||
timeCost: 2,
|
||||
parallelism: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const CURRENT_ALGO_VERSION = 1;
|
||||
|
||||
@Injectable()
|
||||
export class PasswordService {
|
||||
async hash(plain: string): Promise<{ hash: string; algoVersion: number }> {
|
||||
const params = PARAMS_BY_VERSION[CURRENT_ALGO_VERSION];
|
||||
const hash = await argon2.hash(plain, params);
|
||||
return { hash, algoVersion: CURRENT_ALGO_VERSION };
|
||||
}
|
||||
|
||||
async verify(hash: string, plain: string): Promise<boolean> {
|
||||
try {
|
||||
return await argon2.verify(hash, plain);
|
||||
} catch {
|
||||
// Malformed/foreign hash — treat as verification failure, never throw
|
||||
// into the caller (which would otherwise leak a 500 instead of 401).
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
needsRehash(algoVersion: number): boolean {
|
||||
return algoVersion !== CURRENT_ALGO_VERSION;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user