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:
45
apps/api/src/auth/token.service.ts
Normal file
45
apps/api/src/auth/token.service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { randomBytes, createHash } from "node:crypto";
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
import jwt from "jsonwebtoken";
|
||||
import type { EdenEnv } from "@eden/config";
|
||||
import { EDEN_ENV } from "../config/config.constants.js";
|
||||
|
||||
export interface AccessTokenPayload {
|
||||
sub: string; // user id
|
||||
app: "core" | "reseller" | "subscriber";
|
||||
}
|
||||
|
||||
const ACCESS_TOKEN_TTL_SECONDS = 15 * 60; // 15 min — ADR-0003
|
||||
const REFRESH_TOKEN_TTL_DAYS = 30;
|
||||
|
||||
@Injectable()
|
||||
export class TokenService {
|
||||
private readonly secret: string;
|
||||
|
||||
constructor(@Inject(EDEN_ENV) env: EdenEnv) {
|
||||
this.secret = env.JWT_SIGNING_SECRET;
|
||||
}
|
||||
|
||||
signAccessToken(payload: AccessTokenPayload): { token: string; expiresIn: number } {
|
||||
const token = jwt.sign(payload, this.secret, { expiresIn: ACCESS_TOKEN_TTL_SECONDS });
|
||||
return { token, expiresIn: ACCESS_TOKEN_TTL_SECONDS };
|
||||
}
|
||||
|
||||
verifyAccessToken(token: string): AccessTokenPayload {
|
||||
return jwt.verify(token, this.secret) as AccessTokenPayload;
|
||||
}
|
||||
|
||||
/** Opaque, high-entropy refresh token — never a JWT (ADR-0003). Returns the
|
||||
* plaintext (shown once to the client) and its SHA-256 hash (the only
|
||||
* thing persisted). */
|
||||
generateRefreshToken(): { plaintext: string; hash: string; expiresAt: Date } {
|
||||
const plaintext = randomBytes(32).toString("hex"); // 256 bits
|
||||
const hash = this.hashRefreshToken(plaintext);
|
||||
const expiresAt = new Date(Date.now() + REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000);
|
||||
return { plaintext, hash, expiresAt };
|
||||
}
|
||||
|
||||
hashRefreshToken(plaintext: string): string {
|
||||
return createHash("sha256").update(plaintext).digest("hex");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user