Skip to content

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.

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 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:

  1. The old refresh hash is revoked.
  2. A new refresh token is generated.
  3. replaced_by_hash records the lineage.
  4. If a revoked token is reused, the entire family is revoked.
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

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.

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;
}

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.

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.

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());
}