Aller au contenu

Authentification et autorisation

Le backend utilise des jetons JWT access sans état plus des refresh tokens rotatifs. Les permissions sont chargées depuis la base à chaque requête ; elles ne sont pas embarquées dans le jeton.

Le jeton d’accès ne porte que l’identité et un identifiant de jeton (jti) :

export interface AccessTokenClaims {
sub: BigIntStr; // admin.utilisateur.id_utilisateur
login: string;
role: string;
jti: string; // pour la denylist Redis
}

TokensService.signAccessToken signe le JWT et retourne sa TTL en secondes :

signAccessToken(user: { id: BigIntStr; login: string; role: string }) {
const jti = randomBytes(16).toString('hex');
const token = this.jwt.sign(
{ login: user.login, role: user.role },
{ subject: user.id, jwtid: jti },
);
// ...
}

Les refresh tokens sont des chaînes opaques base64url de 32 octets. Seul le hachage SHA-256 est persisté :

generateRefreshToken(): RefreshTokenResult {
const raw = randomBytes(32).toString('base64url');
return { raw, hash: sha256(raw) };
}

Sur POST /api/v1/auth/refresh :

  1. L’ancien hachage de refresh est révoqué.
  2. Un nouveau refresh token est généré.
  3. replaced_by_hash enregistre la lignée.
  4. Si un jeton révoqué est réutilisé, toute la famille est révoquée.
Méthode Route Accès Throttle Rôle
POST /api/v1/auth/login public 5/60s par IP Jetons access + refresh
POST /api/v1/auth/inscription public 3/60s par IP Auto-inscription en CLIENT
POST /api/v1/auth/mot-de-passe-oublie public 5/60s par IP Email de réinitialisation
POST /api/v1/auth/reinitialiser-mot-de-passe public 5/60s par IP Applique le nouveau mot de passe depuis le jeton
POST /api/v1/auth/refresh public aucun Rotation du refresh token
POST /api/v1/auth/logout public aucun Révoque refresh + jti access
GET /api/v1/auth/me JWT — Utilisateur courant + permissions

PasswordService utilise bcryptjs avec un coût de 10 :

@Injectable()
export class PasswordService {
async verify(motDePasse: string, hash: string): Promise<boolean> {
try { return await bcrypt.compare(motDePasse, hash); }
catch { return false; }
}
async hash(motDePasse: string): Promise<string> {
return bcrypt.hash(motDePasse, 10);
}
}

La création admin et la réinitialisation hachent directement avec bcrypt.hash(..., 10).

PermissionsService charge les codes de permission d’un rôle avec un cache de 60 secondes :

async forRole(roleCode: string): Promise<ReadonlySet<string>> {
// cache hit ?
const codes = await this.repo.loadPermissionCodes(this.scope.root, roleCode);
return new Set(codes);
}

Le guard vérifie les métadonnées @RequirePermissions après JwtAuthGuard :

@Get('utilisateurs')
@RequirePermissions('ADMIN.VOIR')
listerUtilisateurs(...) { ... }

AuthUser porte aussi des clés de périmètre résolues depuis la base :

export interface AuthUser {
id: BigIntStr;
login: string;
role: string;
permissions: ReadonlySet<string>;
jti: string;
idClient: BigIntStr | null;
idCaisse: BigIntStr | null;
idActiviteScope: BigIntStr | null;
}

auth.module.ts enregistre les deux guards globalement dans l’ordre :

{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: PermissionsGuard },

JwtAuthGuard ignore les routes @Public(), vérifie le Bearer, contrôle la denylist Redis pour le jti, puis construit AuthUser.

PermissionsGuard autorise tout utilisateur authentifié quand aucun @RequirePermissions n’est déclaré ; sinon il vérifie l’ensemble de permissions de l’utilisateur.

src/admin/admin.controller.ts expose la gestion des utilisateurs, rôles et permissions :

Route Permission Rôle
GET /api/v1/admin/utilisateurs ADMIN.VOIR Lister les utilisateurs
POST /api/v1/admin/utilisateurs ADMIN.CREER Créer un utilisateur
PATCH /api/v1/admin/utilisateurs/:id ADMIN.MODIFIER Modifier un utilisateur
POST /api/v1/admin/utilisateurs/:id/reinitialiser-mot-de-passe ADMIN.MODIFIER Réinitialiser le mot de passe
GET /api/v1/admin/roles ADMIN.VOIR Lister les rôles
POST /api/v1/admin/roles ADMIN.CREER Créer un rôle
DELETE /api/v1/admin/roles/:id ADMIN.MODIFIER Supprimer un rôle (après retrait de ses permissions)
GET /api/v1/admin/permissions ADMIN.VOIR Catalogue des permissions

AdminRepository exclut volontairement mot_de_passe des listes de colonnes en lecture.

Le logout révoque le refresh token et ajoute le jti du jeton d’accès à la denylist Redis pour la TTL restante :

if (authorizationHeader?.startsWith('Bearer ')) {
const token = authorizationHeader.slice('Bearer '.length).trim();
const claims = await this.tokens.verifyAccessToken(token);
await this.tokenStore.revoke(claims.jti, this.tokens.accessTtlSeconds());
}