fix(auth): corrige 4 failles de la revue de securite sur la PR #90

Anti-enumeration cassee sur /auth/forgot-password : l'envoi SMTP etait
synchrone dans le chemin de reponse, donc un email existant prenait plus
de temps qu'un email inconnu (et pouvait renvoyer 500 si le relais SMTP
echouait, contre 202 sinon). L'envoi part desormais en BackgroundTasks,
apres que la reponse 202 a ete envoyee au client, avec un try/except qui
logue plutot que de laisser une exception SMTP remonter.

confirm_password_reset() ne revalidait pas is_active/kind du compte avant
de changer le mot de passe : un compte desactive dans les 15 minutes
suivant l'emission du lien pouvait quand meme voir son mot de passe
change et son must_change_password efface.

Les plages [A-ZA-Y]/[a-za-y] de la regle de complexite incluaient par
erreur x et / (U+00D7, U+00F7), donc un mot de passe sans aucune
majuscule ou minuscule pouvait passer la validation.

Le validateur frontend (JS, \w ASCII) et le validateur backend (Python,
\w Unicode) divergeaient sur les caracteres accentues : un mot de passe
comme "Securite1" passait cote front puis se faisait rejeter en 422 cote
back. Les deux cotes utilisent maintenant le meme jeu explicite de
caracteres speciaux (SPECIAL_CHARACTERS, partage aussi avec cli.py).
This commit is contained in:
Johan LEROY
2026-09-17 14:35:14 +02:00
parent 916b5d246a
commit 921da48eb1
8 changed files with 156 additions and 16 deletions
@@ -0,0 +1,26 @@
import { FormControl } from '@angular/forms';
import { passwordValidators } from './password.validator';
function estValide(motDePasse: string): boolean {
return new FormControl(motDePasse, passwordValidators).valid;
}
describe('passwordValidators', () => {
it('accepte un mot de passe couvrant les quatre classes', () => {
expect(estValide('Un-mot-de-passe1!')).toBe(true);
});
it('accepte un mot de passe accentué (alignement avec le backend, ex: "Sécurité1")', () => {
expect(estValide('Sécurité1!')).toBe(true);
});
it('refuse un mot de passe sans majuscule même avec un "×" ou un "÷"', () => {
expect(estValide('abcdefg1×')).toBe(false);
expect(estValide('abcdefg1÷')).toBe(false);
});
it('refuse un mot de passe sans minuscule même avec un "×" ou un "÷"', () => {
expect(estValide('ABCDEFG1×')).toBe(false);
expect(estValide('ABCDEFG1÷')).toBe(false);
});
});
@@ -1,3 +1,9 @@
// Contrainte : `PASSWORD_PATTERN` doit rester identique au validateur Pydantic de
// `app/schemas/auth.py` côté backend (mêmes plages de majuscules/minuscules, excluant
// × et ÷, mêmes chiffres 0-9, même jeu de caractères spéciaux). `\w`/`\d` divergent entre
// JavaScript (ASCII) et Python (Unicode) : une négation aurait accepté ou rejeté un même
// mot de passe différemment d'un côté à l'autre (ex. "Sécurité1").
import { Validators } from '@angular/forms';
export const PASSWORD_MIN_LENGTH = 8;
@@ -5,7 +11,11 @@ export const PASSWORD_MAX_LENGTH = 128;
export const PASSWORD_HINT =
'8 à 128 caractères, avec au moins 1 majuscule, 1 minuscule, 1 chiffre et 1 caractère spécial';
const PASSWORD_PATTERN = /^(?=.*[A-ZÀ-Ý])(?=.*[a-zà-ÿ])(?=.*\d)(?=.*[^\w\s]).*$/;
const SPECIAL_CHARACTERS = '!@#$%^&*()\\-_=+[\\]{};:,.?';
const PASSWORD_PATTERN = new RegExp(
`^(?=.*[A-ZÀ-ÖØ-Þ])(?=.*[a-zà-öø-þ])` +
`(?=.*[0-9])(?=.*[${SPECIAL_CHARACTERS}]).*$`,
);
export const passwordValidators = [
Validators.required,