Authentication and authorization
The backend uses stateless JWT access tokens plus rotating refresh tokens. Permissions are loaded from the database on every request; they are not embedded in the token.
Access token
Section titled “Access token”The access token carries only identity and a token id (jti):
export interface AccessTokenClaims { sub: BigIntStr; // admin.utilisateur.id_utilisateur login: string; role: string; jti: string; // for Redis denylist}TokensService.signAccessToken signs the JWT and returns its TTL in seconds:
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 }, ); // ...}Refresh token
Section titled “Refresh token”Refresh tokens are opaque 32-byte base64url strings. Only the SHA-256 hash is persisted:
generateRefreshToken(): RefreshTokenResult { const raw = randomBytes(32).toString('base64url'); return { raw, hash: sha256(raw) };}On POST /api/v1/auth/refresh:
- The old refresh hash is revoked.
- A new refresh token is generated.
replaced_by_hashrecords the lineage.- If a revoked token is reused, the entire family is revoked.
Auth endpoints
Section titled “Auth endpoints”| Method | Route | Access | Throttle | Purpose |
|---|---|---|---|---|
POST |
/api/v1/auth/login |
public | 5/60s per IP | Access + refresh tokens |
POST |
/api/v1/auth/inscription |
public | 3/60s per IP | Self-registration as CLIENT |
POST |
/api/v1/auth/mot-de-passe-oublie |
public | 5/60s per IP | Password-reset email |
POST |
/api/v1/auth/reinitialiser-mot-de-passe |
public | 5/60s per IP | Apply new password from token |
POST |
/api/v1/auth/refresh |
public | none | Rotate refresh token |
POST |
/api/v1/auth/logout |
public | none | Revoke refresh + access jti |
GET |
/api/v1/auth/me |
JWT | — | Current user + permissions |
Password hashing
Section titled “Password hashing”PasswordService uses bcryptjs with cost 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); }}Admin creation and reset hash with bcrypt.hash(..., 10) directly.
Permissions and RBAC
Section titled “Permissions and RBAC”PermissionsService loads a role’s permission codes with a 60-second cache:
async forRole(roleCode: string): Promise<ReadonlySet<string>> { // cache hit? const codes = await this.repo.loadPermissionCodes(this.scope.root, roleCode); return new Set(codes);}The guard checks @RequirePermissions metadata after JwtAuthGuard:
@Get('utilisateurs')@RequirePermissions('ADMIN.VOIR')listerUtilisateurs(...) { ... }AuthUser also carries scoping keys resolved from the database:
export interface AuthUser { id: BigIntStr; login: string; role: string; permissions: ReadonlySet<string>; jti: string; idClient: BigIntStr | null; idCaisse: BigIntStr | null; idActiviteScope: BigIntStr | null;}Global guards
Section titled “Global guards”auth.module.ts registers both guards globally in order:
{ provide: APP_GUARD, useClass: JwtAuthGuard },{ provide: APP_GUARD, useClass: PermissionsGuard },JwtAuthGuard skips @Public() routes, verifies the Bearer, checks the Redis denylist for the jti, then builds AuthUser.
PermissionsGuard allows any authenticated user when no @RequirePermissions is declared; otherwise it checks the user’s permission set.
Admin module
Section titled “Admin module”src/admin/admin.controller.ts exposes user, role, and permission management:
| Route | Permission | Purpose |
|---|---|---|
GET /api/v1/admin/utilisateurs |
ADMIN.VOIR |
List users |
POST /api/v1/admin/utilisateurs |
ADMIN.CREER |
Create user |
PATCH /api/v1/admin/utilisateurs/:id |
ADMIN.MODIFIER |
Update user |
POST /api/v1/admin/utilisateurs/:id/reinitialiser-mot-de-passe |
ADMIN.MODIFIER |
Reset password |
GET /api/v1/admin/roles |
ADMIN.VOIR |
List roles |
POST /api/v1/admin/roles |
ADMIN.CREER |
Create role |
DELETE /api/v1/admin/roles/:id |
ADMIN.MODIFIER |
Delete role (after removing role permissions) |
GET /api/v1/admin/permissions |
ADMIN.VOIR |
Permission catalogue |
AdminRepository deliberately excludes mot_de_passe from read column lists.
Token revocation on logout
Section titled “Token revocation on logout”Logout revokes the refresh token and adds the access token’s jti to the Redis denylist for the remaining access-token TTL:
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());}