@nodefony/user 10.0.0-alpha.4 → 10.0.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -543
- package/README.md +1 -1
- package/dist/index.js +4 -1
- package/dist/nodefony/errors/WeakPasswordError.js +12 -4
- package/dist/nodefony/service/UserService.js +35 -10
- package/dist/nodefony/src/admin/UserAdminApi.js +10 -2
- package/dist/nodefony/src/password/commonPasswordHashes.js +24 -0
- package/dist/nodefony/src/password/passwordHash.js +25 -0
- package/dist/nodefony/src/password/passwordPolicy.js +188 -0
- package/dist/nodefony/src/password/seedFailure.js +35 -0
- package/dist/types/index.d.ts +6 -1
- package/dist/types/nodefony/contracts/IPasswordBlocklist.d.ts +25 -5
- package/dist/types/nodefony/contracts/index.d.ts +1 -1
- package/dist/types/nodefony/errors/WeakPasswordError.d.ts +10 -3
- package/dist/types/nodefony/service/UserService.d.ts +8 -4
- package/dist/types/nodefony/src/admin/UserAdminApi.d.ts +11 -1
- package/dist/types/nodefony/src/password/commonPasswordHashes.d.ts +21 -0
- package/dist/types/nodefony/src/password/passwordHash.d.ts +7 -0
- package/dist/types/nodefony/src/password/passwordPolicy.d.ts +60 -0
- package/dist/types/nodefony/src/password/seedFailure.d.ts +51 -0
- package/docs/ajouter-des-champs.md +1 -1
- package/docs/index.md +86 -16
- package/package.json +10 -10
|
@@ -3,10 +3,17 @@ import { nodefonyError } from "nodefony";
|
|
|
3
3
|
* Mot de passe refusé par la politique (connu-compromis / interdit) — `code = 400`.
|
|
4
4
|
*
|
|
5
5
|
* Levée par `UserService.createUser`/`changePassword` quand le
|
|
6
|
-
* {@link IPasswordBlocklist} branché rejette le candidat. Le message
|
|
7
|
-
*
|
|
6
|
+
* {@link IPasswordBlocklist} branché rejette le candidat. Le message NOMME la
|
|
7
|
+
* règle enfreinte quand la politique sait la dire, et rien d'autre : ni la
|
|
8
|
+
* source de la liste, ni le mot de passe — il finirait dans un journal.
|
|
8
9
|
*/
|
|
9
10
|
export declare class WeakPasswordError extends nodefonyError {
|
|
10
|
-
|
|
11
|
+
/** La règle enfreinte, telle que la politique l'a nommée (`null` si muette). */
|
|
12
|
+
readonly violation: string | null;
|
|
13
|
+
/**
|
|
14
|
+
* @param violation - règle enfreinte, en français. Omise, le message reste
|
|
15
|
+
* générique — c'est le cas d'une implémentation qui ne rend qu'un booléen.
|
|
16
|
+
*/
|
|
17
|
+
constructor(violation?: string | null);
|
|
11
18
|
}
|
|
12
19
|
export default WeakPasswordError;
|
|
@@ -46,10 +46,14 @@ export declare class UserService extends AbstractCrudService<IPasswordAuthentica
|
|
|
46
46
|
#private;
|
|
47
47
|
protected readonly encoder: IPasswordEncoder;
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
49
|
+
* Politique de mot de passe (NIST SP 800-63B §5.1.1.2) — consultée à la
|
|
50
|
+
* création et au changement, jamais au login.
|
|
51
|
+
*
|
|
52
|
+
* **Posée PAR DÉFAUT**, et c'est le point : un défaut `null` ne se voit pas,
|
|
53
|
+
* et personne ne l'écrasait — `security:user:add compta --password abc`
|
|
54
|
+
* réussissait. L'application REMPLACE cet objet pour durcir ou pour brancher
|
|
55
|
+
* sa propre source ; la mettre à `null` désactive tout contrôle, et c'est
|
|
56
|
+
* alors un geste explicite, pas un oubli.
|
|
53
57
|
*/
|
|
54
58
|
passwordBlocklist: IPasswordBlocklist | null;
|
|
55
59
|
/**
|
|
@@ -65,7 +65,17 @@ export interface IUserRevokedEvent {
|
|
|
65
65
|
id: string;
|
|
66
66
|
identifier: string;
|
|
67
67
|
tenantId: string | null;
|
|
68
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Ce qui a coupé l'accès du porteur.
|
|
70
|
+
*
|
|
71
|
+
* `password_changed` n'est pas une révocation de COMPTE — le compte reste
|
|
72
|
+
* actif — mais c'en est une de ses ACCÈS EN COURS : on change un mot de passe
|
|
73
|
+
* parce qu'il est perdu ou compromis, et laisser vivre les sessions ouvertes
|
|
74
|
+
* laisserait l'accès à qui l'a volé. La cascade est la même, et c'est le
|
|
75
|
+
* but : un seul canal, les abonnés futurs (webhooks) n'ont rien à savoir de
|
|
76
|
+
* la cause.
|
|
77
|
+
*/
|
|
78
|
+
reason: "deleted" | "disabled" | "locked" | "password_changed";
|
|
69
79
|
}
|
|
70
80
|
/**
|
|
71
81
|
* Statut du sous-système utilisateur — miroir consommé par la console Studio
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Empreintes des mots de passe les plus courants — **fichier GÉNÉRÉ, ne pas éditer**.
|
|
3
|
+
*
|
|
4
|
+
* Regénérer :
|
|
5
|
+
* `node scripts/generate-password-blocklist.mjs <fichier-source>`
|
|
6
|
+
*
|
|
7
|
+
* Contenu : les 4 premiers octets du SHA-1 de chaque mot de passe, triés, en
|
|
8
|
+
* base64 d'un `Uint32Array` gros-boutien. Le clair n'est PAS embarqué — ni
|
|
9
|
+
* lisible, ni reconstructible depuis ce fichier.
|
|
10
|
+
*
|
|
11
|
+
* Sources :
|
|
12
|
+
* - `xato-net-10-million-passwords-10000.txt` — 9999 entrées, SHA-256 `c63d5e4ccc31344d662583cc39ca4bd5…`
|
|
13
|
+
*
|
|
14
|
+
* Corpus : SecLists (`danielmiessler/SecLists`), **licence MIT** — attribution
|
|
15
|
+
* portée ici, telle que la licence l'exige. Le corpus lui-même n'est pas
|
|
16
|
+
* redistribué : seules ces empreintes le sont.
|
|
17
|
+
*/
|
|
18
|
+
/** 9999 empreintes triées (39996 octets décodés). */
|
|
19
|
+
export declare const COMMON_PASSWORD_HASHES_BASE64: string;
|
|
20
|
+
/** Nombre d'empreintes — sert de contrôle au décodage. */
|
|
21
|
+
export declare const COMMON_PASSWORD_HASH_COUNT = 9999;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { IPasswordBlocklist, IPasswordSubjectHint } from "../../contracts/IPasswordBlocklist.js";
|
|
2
|
+
/** Réglages d'une politique — tout est optionnel, les défauts sont sains. */
|
|
3
|
+
export interface IPasswordPolicyOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Longueur minimale acceptée. Défaut : 10.
|
|
6
|
+
*
|
|
7
|
+
* Le plancher du NIST est 8 ; on retient 10 parce que le contrôle de liste le
|
|
8
|
+
* permet sans exiger de composition — et parce qu'en deçà, les empreintes des
|
|
9
|
+
* mots de passe courants couvrent déjà presque tout l'espace saisi.
|
|
10
|
+
*/
|
|
11
|
+
minLength?: number;
|
|
12
|
+
/** Mots de passe refusés en propre (noms métier, du produit, du client). */
|
|
13
|
+
blocklist?: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* Fichier de mots de passe refusés, une valeur par ligne (UTF-8). Lu au
|
|
16
|
+
* PREMIER contrôle, comme les empreintes — un fichier absent ou illisible est
|
|
17
|
+
* une erreur FRANCHE, jamais un contrôle qui se tait.
|
|
18
|
+
*/
|
|
19
|
+
blocklistFile?: string | null;
|
|
20
|
+
/** Consulter les empreintes embarquées. Défaut : `true`. */
|
|
21
|
+
checkCommonPasswords?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Défauts de la politique — source unique, relue par les tests. */
|
|
24
|
+
export declare const DEFAULT_PASSWORD_POLICY: Required<IPasswordPolicyOptions>;
|
|
25
|
+
/**
|
|
26
|
+
* Politique par défaut : six règles, de la moins chère à la plus chère.
|
|
27
|
+
*
|
|
28
|
+
* Implémente {@link IPasswordBlocklist} — donc remplaçable par l'application,
|
|
29
|
+
* et composable (une implémentation maison peut l'appeler avant sa propre source).
|
|
30
|
+
*/
|
|
31
|
+
export declare class PasswordPolicy implements IPasswordBlocklist {
|
|
32
|
+
#private;
|
|
33
|
+
readonly options: Required<IPasswordPolicyOptions>;
|
|
34
|
+
/**
|
|
35
|
+
* @param options - réglages ; chaque clé absente prend le défaut sain.
|
|
36
|
+
*/
|
|
37
|
+
constructor(options?: IPasswordPolicyOptions);
|
|
38
|
+
/**
|
|
39
|
+
* La règle enfreinte, ou `null` si le mot de passe est accepté.
|
|
40
|
+
*
|
|
41
|
+
* Le texte rendu nomme la règle SANS jamais citer le mot de passe : il finit
|
|
42
|
+
* dans un message d'erreur, donc potentiellement dans un journal.
|
|
43
|
+
*
|
|
44
|
+
* @param plain - mot de passe candidat.
|
|
45
|
+
* @param subject - ce qu'on sait du compte (son identifiant).
|
|
46
|
+
* @returns la règle enfreinte, en français, ou `null`.
|
|
47
|
+
*/
|
|
48
|
+
violation(plain: string, subject?: IPasswordSubjectHint): Promise<string | null>;
|
|
49
|
+
/**
|
|
50
|
+
* Le mot de passe doit-il être refusé ?
|
|
51
|
+
*
|
|
52
|
+
* Même règle, même code que {@link violation} — une seule implémentation, deux
|
|
53
|
+
* portes : le contrat ne demande qu'un booléen, l'appelant qui veut expliquer
|
|
54
|
+
* prend l'autre.
|
|
55
|
+
*
|
|
56
|
+
* @param plain - mot de passe candidat.
|
|
57
|
+
* @param subject - ce qu'on sait du compte.
|
|
58
|
+
*/
|
|
59
|
+
isBlocked(plain: string, subject?: IPasswordSubjectHint): Promise<boolean>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Le message d'un **semis de compte qui a échoué** — celui que l'erreur, seule,
|
|
3
|
+
* ne peut pas écrire.
|
|
4
|
+
*
|
|
5
|
+
* Le problème est un partage d'information, pas un défaut de code.
|
|
6
|
+
* {@link WeakPasswordError} nomme la règle enfreinte et **rien d'autre** : ni le
|
|
7
|
+
* compte visé, ni la valeur refusée, parce qu'un message d'erreur finit dans un
|
|
8
|
+
* journal. Le seul à savoir QUI était semé, et D'OÙ venait le mot de passe, est
|
|
9
|
+
* l'appelant. Tant que les deux moitiés restent séparées, l'exploitant lit
|
|
10
|
+
* « Mot de passe refusé : contient l'identifiant du compte » sans savoir quel
|
|
11
|
+
* compte, ni quoi corriger — vécu : une application générée ne démarrait plus,
|
|
12
|
+
* et la seule trace utile était une ligne perdue dans un journal détaché.
|
|
13
|
+
*
|
|
14
|
+
* Ce module réunit les deux moitiés, une fois pour tout le monde : le gabarit
|
|
15
|
+
* d'application, le dépôt de développement du framework et toute application
|
|
16
|
+
* qui sème ses propres comptes appellent la MÊME fonction. Recopier ce message
|
|
17
|
+
* dans chaque semis le ferait diverger en silence — et c'est le message, pas le
|
|
18
|
+
* code, qui fait la différence entre dix secondes et une demi-heure.
|
|
19
|
+
*/
|
|
20
|
+
/** Ce que l'appelant sait du semis, et que l'erreur ignore. */
|
|
21
|
+
export interface ISeedFailureContext {
|
|
22
|
+
/** Identifiant du compte qu'on tentait de semer (`"admin"`). */
|
|
23
|
+
identifier: string;
|
|
24
|
+
/**
|
|
25
|
+
* Variable d'environnement qui porte le mot de passe (`"NF_ADMIN_PASSWORD"`).
|
|
26
|
+
*/
|
|
27
|
+
envVar: string;
|
|
28
|
+
/**
|
|
29
|
+
* Le mot de passe venait-il de {@link envVar} ?
|
|
30
|
+
*
|
|
31
|
+
* `false` = il vient d'un défaut écrit dans le code de l'application. Envoyer
|
|
32
|
+
* corriger une variable que personne n'a posée ferait chercher là où il n'y a
|
|
33
|
+
* rien : le remède n'est alors pas le même geste.
|
|
34
|
+
*/
|
|
35
|
+
fromEnv: boolean;
|
|
36
|
+
/** Le compte visait-il un rôle d'administration ? Décide du `--admin` du remède. */
|
|
37
|
+
admin?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Rédige le message d'un semis raté : QUEL compte, QUELLE règle, QUEL geste.
|
|
41
|
+
*
|
|
42
|
+
* Les trois sont indissociables. Un message qui nomme la règle sans le compte
|
|
43
|
+
* laisse chercher lequel ; un message qui nomme le compte sans le geste laisse
|
|
44
|
+
* l'exploitant devant un constat. Et la dernière phrase dit ce que le lecteur se
|
|
45
|
+
* demande immédiatement : l'application tourne-t-elle encore ?
|
|
46
|
+
*
|
|
47
|
+
* @param cause - ce que la création du compte a levé.
|
|
48
|
+
* @param context - ce que l'appelant est seul à savoir.
|
|
49
|
+
* @returns le message à journaliser, sans saut de ligne (il part en `ERROR`).
|
|
50
|
+
*/
|
|
51
|
+
export declare function describeSeedFailure(cause: unknown, context: ISeedFailureContext): string;
|
|
@@ -58,7 +58,7 @@ parfois le pire : cette table n'est pas une table comme les autres.
|
|
|
58
58
|
**La table des utilisateurs est relue à chaque requête portant une session authentifiée.** Ce n'est
|
|
59
59
|
pas un détail d'implémentation, c'est le cœur du modèle : une session ne transporte qu'un
|
|
60
60
|
identifiant, jamais l'utilisateur lui-même. À chaque requête, `SessionAuthenticator.authenticate()`
|
|
61
|
-
(`src/packages/@nodefony/security/nodefony/src/authenticator/SessionAuthenticator.ts:
|
|
61
|
+
(`src/packages/@nodefony/security/nodefony/src/authenticator/SessionAuthenticator.ts:96`) redemande
|
|
62
62
|
l'identité vivante — `resolveSessionIdentity`
|
|
63
63
|
(`src/packages/@nodefony/security/nodefony/src/authenticator/SessionAuthenticator.ts:70`) — pour que
|
|
64
64
|
la désactivation d'un compte, un changement de rôle ou un verrouillage prennent effet
|
package/docs/index.md
CHANGED
|
@@ -24,7 +24,7 @@ status: stable
|
|
|
24
24
|
updated: 2026-07-19
|
|
25
25
|
source: "src/packages/@nodefony/user/docs/index.md"
|
|
26
26
|
coverageModule: user
|
|
27
|
-
coverageFiles: UserService.ts,BaseUser.ts,AnonymousUser.ts,InMemoryUserRepository.ts,userProfile.ts,UserAdminApi.ts,Argon2idEncoder.ts,BcryptEncoder.ts,MigratingEncoder.ts,encoderFromConfig.ts
|
|
27
|
+
coverageFiles: UserService.ts,passwordPolicy.ts,BaseUser.ts,AnonymousUser.ts,InMemoryUserRepository.ts,userProfile.ts,UserAdminApi.ts,Argon2idEncoder.ts,BcryptEncoder.ts,MigratingEncoder.ts,encoderFromConfig.ts
|
|
28
28
|
---
|
|
29
29
|
|
|
30
30
|
# @nodefony/user — l'identité, socle de toute la sécurité
|
|
@@ -289,7 +289,7 @@ use("@nodefony/security", {
|
|
|
289
289
|
|
|
290
290
|
> [!NOTE]
|
|
291
291
|
> Sans section `encoders`, le défaut du schéma Zod est **déjà** un Argon2id sûr
|
|
292
|
-
> (`security/nodefony/config/config.ts:
|
|
292
|
+
> (`security/nodefony/config/config.ts:1110`). Tu ne déclares cette section que pour ajouter un format
|
|
293
293
|
> legacy, ou pour ajuster les coûts.
|
|
294
294
|
|
|
295
295
|
### 2. Déclarer le service `users` (`nodefony/security/provisionUsers.ts`)
|
|
@@ -464,7 +464,7 @@ lieu au seul moment où le mot de passe en clair existe côté serveur, un login
|
|
|
464
464
|
### L'ordre des vérifications
|
|
465
465
|
|
|
466
466
|
`locked` → `disabled` → `no_password` → `bad_credentials`. Cet ordre est fixé
|
|
467
|
-
(`UserService.ts:
|
|
467
|
+
(`UserService.ts:270`), mais il n'est pas observable de l'extérieur : la valeur de retour est `null`
|
|
468
468
|
dans tous les cas, et la raison précise part dans l'événement `onAuthenticationFailure` — donc dans
|
|
469
469
|
l'audit serveur, jamais dans la réponse.
|
|
470
470
|
|
|
@@ -536,7 +536,7 @@ ajoute quatre accès que le `Criteria` générique ne sait pas exprimer.
|
|
|
536
536
|
| `loadUserByIdentifier()` | `IUserProvider` — **lève** `UserNotFoundError` si absent | `UserService.ts:301` |
|
|
537
537
|
| `loadUserByOAuth()` | `IUserProvider` — lit un lien social, ne crée jamais | `UserService.ts:317` |
|
|
538
538
|
| `refreshUser()` | recharge depuis la source (rôles frais, révocation immédiate) | `UserService.ts:331` |
|
|
539
|
-
| `provisionOAuthUser()` | Shadow User : lit, ou crée si la politique l'autorise | `UserService.ts:
|
|
539
|
+
| `provisionOAuthUser()` | Shadow User : lit, ou crée si la politique l'autorise | `UserService.ts:363` |
|
|
540
540
|
| `passwordBlocklist` | champ opt-in — branche ta liste de mots de passe compromis | `UserService.ts:83` |
|
|
541
541
|
|
|
542
542
|
**La distinction à retenir** : `loadUserByOAuth()` **lit** (et lève si le lien est inconnu) ;
|
|
@@ -670,7 +670,7 @@ flowchart TD
|
|
|
670
670
|
|
|
671
671
|
Le compte local est le **Shadow User** : ton application garde sa propre ligne, avec ses propres
|
|
672
672
|
rôles, son propre état actif/verrouillé. Le fournisseur n'est qu'une façon de prouver qu'on est bien
|
|
673
|
-
la personne rattachée à cette ligne (`UserService.ts:
|
|
673
|
+
la personne rattachée à cette ligne (`UserService.ts:363`).
|
|
674
674
|
|
|
675
675
|
### Trois invariants, et pourquoi ils existent
|
|
676
676
|
|
|
@@ -749,23 +749,93 @@ d'identité est un échec explicite, pas une valeur.
|
|
|
749
749
|
Si tu veux seulement valider un couple identifiant/mot de passe contre un système externe, le contrat
|
|
750
750
|
plus étroit `IPasswordVerifier` suffit (`IPasswordVerifier.ts:15`).
|
|
751
751
|
|
|
752
|
-
###
|
|
752
|
+
### La politique de mot de passe — posée par défaut, et remplaçable
|
|
753
753
|
|
|
754
|
-
Le NIST (SP 800-63B §5.1.1.2) recommande de refuser les mots de passe
|
|
755
|
-
|
|
756
|
-
d'
|
|
754
|
+
Le NIST (SP 800-63B §5.1.1.2) recommande de refuser les mots de passe prévisibles — et de NE PAS
|
|
755
|
+
exiger de composition (majuscules, chiffres, caractères spéciaux), qui produit `Password1!` sans
|
|
756
|
+
ajouter d'entropie réelle. Nodefony suit cette lecture : ce qui est mesuré, c'est la
|
|
757
|
+
**prévisibilité**.
|
|
758
|
+
|
|
759
|
+
**La politique est posée d'office** sur tout `UserService` : rien à brancher, et c'est le point —
|
|
760
|
+
un point d'extension que personne ne remplit ne protège personne. Elle est consultée à la
|
|
761
|
+
**création** et au **changement**, jamais au login : là, le clair n'est plus jugeable, et refuser
|
|
762
|
+
une connexion existante enfermerait l'utilisateur dehors.
|
|
763
|
+
|
|
764
|
+
Six règles, dans cet ordre — les gratuites d'abord, la liste en dernier :
|
|
765
|
+
|
|
766
|
+
| # | Ce qui est refusé | Exemple refusé |
|
|
767
|
+
| --- | -------------------------------------------------------- | ---------------------------- |
|
|
768
|
+
| 1 | plus court que `minLength` (défaut **10**) | `abc`, `motdepas` |
|
|
769
|
+
| 2 | contient l'identifiant du compte | `marie.dupont-2026` |
|
|
770
|
+
| 3 | répète un même motif de bout en bout | `aaaaaaaaaa`, `abababababab` |
|
|
771
|
+
| 4 | contient une suite de touches ou de chiffres | `azertyuiop`, `0123456789` |
|
|
772
|
+
| 5 | figure dans la liste de l'application | ce que tu y mets |
|
|
773
|
+
| 6 | figure parmi les ~10 000 mots de passe les plus courants | `basketball`, `password123` |
|
|
774
|
+
|
|
775
|
+
Le refus **nomme la règle** : `Mot de passe refusé : figure parmi les mots de passe les plus
|
|
776
|
+
courants`. C'est ce que rendent `security:user:add` et `security:user:password` (code de sortie non
|
|
777
|
+
nul), et ce que la console d'administration renvoie en 400.
|
|
778
|
+
|
|
779
|
+
#### Régler la politique
|
|
757
780
|
|
|
758
781
|
```ts ignore
|
|
782
|
+
import { PasswordPolicy } from "@nodefony/user";
|
|
783
|
+
|
|
784
|
+
users.passwordBlocklist = new PasswordPolicy({
|
|
785
|
+
minLength: 14, // durcir
|
|
786
|
+
blocklist: ["MaSociete2026", "NomDuProduit"], // quelques mots métier
|
|
787
|
+
blocklistFile: "var/mots-de-passe-interdits.txt", // une politique maintenue à part
|
|
788
|
+
});
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
| Option | Défaut | Ce qu'elle fait |
|
|
792
|
+
| ---------------------- | ------ | ----------------------------------------------------------- |
|
|
793
|
+
| `minLength` | `10` | longueur minimale |
|
|
794
|
+
| `blocklist` | `[]` | valeurs refusées, comparaison insensible à la casse |
|
|
795
|
+
| `blocklistFile` | `null` | fichier UTF-8, une valeur par ligne, lu au premier contrôle |
|
|
796
|
+
| `checkCommonPasswords` | `true` | consulter la liste embarquée |
|
|
797
|
+
|
|
798
|
+
Un **fichier introuvable fait échouer le contrôle**, il ne le rend pas muet : une politique qu'on
|
|
799
|
+
croit en place et qui n'a rien lu est pire qu'une politique absente.
|
|
800
|
+
|
|
801
|
+
#### Brancher sa propre source
|
|
802
|
+
|
|
803
|
+
`IPasswordBlocklist` reste le point d'extension — pour un annuaire d'entreprise, une base maison, ou
|
|
804
|
+
une API k-anonymity (type Have I Been Pwned, qui n'envoie que cinq caractères du hash). Elle
|
|
805
|
+
s'appelle en plus de la politique, pas à sa place :
|
|
806
|
+
|
|
807
|
+
```ts ignore
|
|
808
|
+
const politique = new PasswordPolicy();
|
|
759
809
|
users.passwordBlocklist = {
|
|
760
|
-
async
|
|
761
|
-
|
|
762
|
-
|
|
810
|
+
isBlocked: async (plain, subject) =>
|
|
811
|
+
(await politique.isBlocked(plain, subject)) ||
|
|
812
|
+
(await maSource.connu(plain)),
|
|
813
|
+
violation: async (plain, subject) =>
|
|
814
|
+
(await politique.violation(plain, subject)) ??
|
|
815
|
+
((await maSource.connu(plain))
|
|
816
|
+
? "figure dans notre annuaire de fuites"
|
|
817
|
+
: null),
|
|
763
818
|
};
|
|
764
819
|
```
|
|
765
820
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
821
|
+
Mettre `users.passwordBlocklist = null` désactive tout contrôle. C'est permis — une application qui
|
|
822
|
+
juge les mots de passe en amont n'a pas à payer deux fois — mais c'est alors un **geste explicite**,
|
|
823
|
+
plus un défaut qu'on ne voit pas.
|
|
824
|
+
|
|
825
|
+
#### La liste embarquée, et ce qu'elle coûte
|
|
826
|
+
|
|
827
|
+
~10 000 empreintes de 4 octets (`Uint32Array` trié, recherche binaire) : **40 Ko**, et le module qui
|
|
828
|
+
les porte est chargé par un `import()` dynamique au premier contrôle — une application qui ne crée
|
|
829
|
+
aucun compte ne le lit jamais. Le clair n'est pas embarqué : seules les empreintes le sont, et elles
|
|
830
|
+
ne sont pas réversibles. La troncature rend une collision possible (~2,3 × 10⁻⁶ par test) ; elle
|
|
831
|
+
**refuse** alors un mot de passe sain, ce qui tombe du bon côté.
|
|
832
|
+
|
|
833
|
+
Le corpus vient de [SecLists](https://github.com/danielmiessler/SecLists) (licence MIT, attribution
|
|
834
|
+
portée dans l'artefact). Regénérer l'artefact :
|
|
835
|
+
|
|
836
|
+
```bash
|
|
837
|
+
node scripts/generate-password-blocklist.mjs <fichier-source>
|
|
838
|
+
```
|
|
769
839
|
|
|
770
840
|
### Son propre provisionnement OAuth
|
|
771
841
|
|
|
@@ -939,7 +1009,7 @@ Une erreur d'administration ne doit pas fermer la porte définitivement. Cinq ga
|
|
|
939
1009
|
| supprimer ou désactiver le dernier admin actif | 409 |
|
|
940
1010
|
|
|
941
1011
|
Le comptage passe par `countActiveAdmins()`, un `COUNT` natif au store — jamais un chargement complet
|
|
942
|
-
en mémoire (`UserService.ts:
|
|
1012
|
+
en mémoire (`UserService.ts:174`).
|
|
943
1013
|
|
|
944
1014
|
### Cascade de révocation
|
|
945
1015
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/user",
|
|
3
|
-
"version": "10.0.0-alpha.
|
|
3
|
+
"version": "10.0.0-alpha.6",
|
|
4
4
|
"description": "Contrats utilisateur de Nodefony, indépendants de l'ORM : interface IUser, utilisateurs de base, encodeurs de mot de passe et UserService — consommable sans @nodefony/security",
|
|
5
5
|
"contributors": [],
|
|
6
6
|
"type": "module",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@node-rs/argon2": "^2.0.2",
|
|
42
42
|
"@node-rs/bcrypt": "^1.10.0",
|
|
43
|
-
"@nodefony/orm-core": "^10.0.0-alpha.
|
|
44
|
-
"nodefony": "^10.0.0-alpha.
|
|
43
|
+
"@nodefony/orm-core": "^10.0.0-alpha.6",
|
|
44
|
+
"nodefony": "^10.0.0-alpha.6"
|
|
45
45
|
},
|
|
46
46
|
"peerDependenciesMeta": {
|
|
47
47
|
"@node-rs/bcrypt": {
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@node-rs/argon2": "^2.1
|
|
55
|
+
"@node-rs/argon2": "^2.2.1",
|
|
56
56
|
"@node-rs/bcrypt": "^1.10.8",
|
|
57
|
-
"@nodefony/orm-core": "^10.0.0-alpha.
|
|
58
|
-
"@types/node": "26.
|
|
57
|
+
"@nodefony/orm-core": "^10.0.0-alpha.6",
|
|
58
|
+
"@types/node": "26.5.1",
|
|
59
59
|
"@vitest/coverage-v8": "5.0.0",
|
|
60
|
-
"nodefony": "^10.0.0-alpha.
|
|
60
|
+
"nodefony": "^10.0.0-alpha.6",
|
|
61
61
|
"rimraf": "6.1.3",
|
|
62
62
|
"vitest": "5.0.0"
|
|
63
63
|
},
|
|
@@ -66,11 +66,11 @@
|
|
|
66
66
|
"url": "git+https://github.com/nodefony/nodefony-core.git",
|
|
67
67
|
"directory": "src/packages/@nodefony/user"
|
|
68
68
|
},
|
|
69
|
-
"license": "
|
|
69
|
+
"license": "Apache-2.0",
|
|
70
70
|
"licenses": [
|
|
71
71
|
{
|
|
72
|
-
"type": "
|
|
73
|
-
"url": "
|
|
72
|
+
"type": "Apache-2.0",
|
|
73
|
+
"url": "https://www.apache.org/licenses/LICENSE-2.0"
|
|
74
74
|
}
|
|
75
75
|
],
|
|
76
76
|
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|