@aventii/auth (1.0.0)
Installation
@aventii:registry=npm install @aventii/auth@1.0.0"@aventii/auth": "1.0.0"About this package
# @aventii/auth
Noyau générique d'inscription, authentification, credentials et sessions pour les microservices Node.js Aventii.
`@aventii/auth` est extrait du mécanisme Access/PMC sans embarquer les règles propres à Rapidoo.
**Le package sait créer et authentifier un compte. Il ne sait pas ce que ce compte fait dans le produit.**
## Installation
```bash
npm install @aventii/auth
npm install mongodb # repository Mongo natif
npm install express # router HTTP fourni
```
Prérequis : Node.js >= 22. Le package est CommonJS.
## Périmètre
La V1 couvre le salt anti-énumération, l'inscription en deux étapes, la validation mail et téléphone, le login, les access/refresh tokens persistants, la rotation refresh, le logout, forgot/reset password, la validation explicite de token, le middleware HTTP, la désactivation de compte et les nettoyages.
Elle ne gère pas le profil, les paiements, les documents d'identité, les fichiers, les règles pays ou les rôles métier.
## API publique
```js
const {
createAuth,
createAuthRouter,
createAuthMiddleware,
createMongoAuthRepository,
createVersionedPepperProvider,
AUTH_ACCOUNT_STATUS,
AuthError,
normalizeMail,
generateRegistrationToken,
hashOpaqueToken,
generatePhoneValidationCode
} = require('@aventii/auth');
```
## Architecture
```text
microservice
/ | \
@aventii/db @aventii/security métier produit
| | |
Mongo repository primitives policies/actions
\ | /
@aventii/auth
```
Le microservice compose les capacités. Le cœur auth ne dépend pas directement de `@aventii/db`; le repository Mongo est le seul composant qui connaît MongoDB.
## Statuts
```text
active · waitingForMailValidation · disabled
```
Ils sont exposés via `AUTH_ACCOUNT_STATUS` et conservent le schéma historique PMC.
## Identifiants
Cette V1 conserve volontairement les `_id: ObjectId` Mongo classiques. Le repository Mongo convertit les identifiants sérialisés en `ObjectId`. Aucun UUID n'est introduit ici.
## Création du moteur
```js
const auth = createAuth({
repository,
tokenSecret,
fakeSaltSecret,
pepperProvider,
generateFingerprint
});
```
Une configuration produit peut aussi injecter `policies`, `actions`, `hooks`, `buildAccessTokenClaims`, `normalizeMail`, `timing` et `enforceFingerprint`.
Le package ne lit jamais `process.env` et ne récupère aucun secret distant lui-même.
## Contrat repository
Le cœur exige :
```text
findUserByMail · findUserById · createUser · updateUser · completeRegistration · prepareMailValidation · consumeMailValidationToken · consumePhoneValidationCode · disableUser · createAccessToken · findAccessTokenById · findReusableAccessToken · invalidateAccessToken · deleteExpiredAccessTokens · createRefreshToken · findReusableRefreshToken · findValidRefreshTokenById · extendRefreshToken · revokeRefreshToken · revokeRefreshTokensByUserId · deleteExpiredRefreshTokens · deleteExpiredIncompleteRegistrations · deleteExpiredUnverifiedMailRegistrations
```
Le cœur ignore le moteur de persistance. La V1 fournit uniquement MongoDB natif.
## Repository Mongo
```js
const repository = createMongoAuthRepository({
getUsersCollection: () => database.getDb('Auth').collection('Users'),
getAccessTokensCollection: () => database.getDb('Auth').collection('Tokens'),
getRefreshTokensCollection: () => database.getDb('Auth').collection('RefreshTokens')
});
```
Les getters sont résolus à chaque opération pour rester compatibles avec un remplacement de connexion par `@aventii/db`.
## Normalisation mail
Le comportement historique est conservé :
```js
normalizeMail('John+promo@Example.com');
```
Le normaliseur trim, passe en minuscules et retire `+tag` du local-part. Il peut être remplacé via `createAuth({ normalizeMail })`.
## Salt anti-énumération
`register()` génère un salt aléatoire de 16 bytes, soit 32 caractères hexadécimaux.
`getSalt(mail)` renvoie le vrai salt pour un compte existant. Sinon :
```text
HMAC-SHA256(fakeSaltSecret, mail brut)
↓
32 premiers caractères hex
```
Le client reçoit ainsi une réponse de même forme que le compte existe ou non.
## Register
```js
const result = await auth.register({
mail: 'alice@example.com',
accountData: {
firstName: 'Alice',
nationality: 'CM'
}
});
```
`accountData` est opaque. Le moteur ajoute ou écrase seulement :
```text
mail · salt · status · registrationTokenHash · registrationTokenExpiresAt · registrationTokenAttempts
```
Le token brut est généré sur 256 bits et seul son SHA-256 est persisté.
## Ce qui reste au produit pendant l'inscription
Le package ne valide pas :
```text
firstName / lastName
dateOfBirth / gender
nationality / pays de service
type de compte
balance initiale
CGU
Mobile Money
règles de téléphone
```
Ces règles sont appliquées avant `auth.register()`.
## Tentatives d'inscription
Valeurs historiques par défaut :
```text
token TTL 60 s · max attempts 5 · penalty from 2 · penalty TTL 3 s
```
Un mauvais token incrémente `registrationTokenAttempts`. À partir de la deuxième erreur l'expiration est raccourcie; à la cinquième hash et expiration sont neutralisés.
L'échec de cette mise à jour secondaire est volontairement ignoré, comme dans le contrôleur extrait.
## Complete registration
```js
await auth.completeRegistration({
mail,
clientHash,
registrationToken
});
```
`clientHash` doit être une chaîne SHA-256 hexadécimale de 64 caractères. Workflow :
```text
validation du token d'inscription
↓
pepper courant
↓
bcrypt(clientHash + pepper)
↓
transition atomique en base
↓
suppression registrationToken*
↓
validation mail éventuelle
```
Work factor par défaut : `12`.
**## Pepper versionné
const pepperProvider = createVersionedPepperProvider({
currentVersion: () => config.PASSWORD_PEPPER_CURRENT,
sources: [
version => config[`PASSWORD_PEPPER_V${version}`],
version => keyService.get(`SERVER_PEPPER_V${version}`),
() => hsm.getSecret('AUTH_PEPPER')
]
});
Le provider reçoit une liste ordonnée et non vide de fonctions sources. Chaque source reçoit systématiquement la version demandée et peut retourner une chaîne ou une Promise<string>.
Toutes les sources sont résolues indépendamment puis leurs valeurs sont combinées dans l'ordre déclaré. La dérivation finale utilise SHA-256 avec un préfixage par longueur de chaque composante afin d'éviter toute ambiguïté de concaténation.
source 0(version)
source 1(version)
...
source N(version)
↓
composantes ordonnées
↓
encodage longueur:valeur
↓
SHA-256
↓
pepper final
Le package ne connaît ni la nature des sources, ni leur localisation, ni leur mécanisme de récupération. Il n'existe plus de notion local, remote, cacheRemote ou clearRemoteCache() dans le provider.
Si une source doit être mise en cache, ce cache appartient à la source elle-même ou à l'infrastructure qui la fournit.
Migration du pepper**
Au login, un credential valide utilisant une ancienne version est automatiquement rehashé avec la version courante :
```text
compare ancien pepper
↓ succès
version ancienne ?
↓ oui
rehash courant
↓
update hash + pepperVersion
```
## Validation mail
Par défaut `mailVerificationRequired: true`, donc le compte naît avec `waitingForMailValidation`.
Après completion, un token e-mail de 256 bits est créé, seul son SHA-256 est persisté, avec un TTL de 24 h.
`actions.deliverMailValidation` est **best-effort** : une panne du mail ne défait pas la completion déjà enregistrée.
## Validate mail
```js
await auth.validateMail(token);
```
Le repository consomme atomiquement le token encore valide puis passe le compte de `waitingForMailValidation` à `active`.
Les champs du token sont supprimés et `hooks.onMailValidated` est exécuté en best-effort.
Si la vérification mail est désactivée, `register()` crée directement un compte `active` et completion déclenche `onRegistrationCompleted`.
## Validation téléphone
```js
await auth.validatePhone({ userId, code });
```
Le repository Mongo utilise par défaut :
```text
phoneValidationToken · verification.verifiedPhone
```
Ces chemins sont configurables via `phoneTokenField` et `phoneVerifiedField`.
`generatePhoneValidationCode()` produit le format historique `xxxx-xxxx`. Le package ne décide pas comment ce code est envoyé.
## Login
```js
const result = await auth.login({
mail,
password,
request,
context
});
```
`password` désigne la valeur credential fournie par le client; le package ne cherche pas à savoir comment le front l'a dérivée.
Workflow :
```text
find user
policy beforeCredential
hash présent ?
bcrypt.compare(password + pepper)
migration pepper éventuelle
status accepté ?
policy afterCredential
fingerprint
access token
refresh token
```
## Politique login
```js
policies: {
login: async ({ phase, user, context, request }) => {
...
}
}
```
`phase` vaut `beforeCredential` ou `afterCredential`.
Rapidoo pourra y remettre le contrôle nationalité/pays ou la restriction du portail admin sans contaminer le package.
## Audit login
`hooks.onAuthEvent` reçoit notamment :
```js
{
eventType: 'login_failed',
reasonKey: 'WRONG_PASSWORD',
user,
request
}
```
Les raisons génériques incluent `USER_NOT_FOUND`, `ACCOUNT_NOT_VALIDATED`, `WRONG_PASSWORD`, `MAIL_NOT_VALIDATED`, `ACCOUNT_DISABLED`.
Le hook est best-effort; le produit décide comment hash l'IP ou persister l'audit.
## Access tokens
La signature/vérification est déléguée à `@aventii/security`. Un document contient au minimum :
```text
userId · issuedAt · expiresAt · deleteAt · deviceFingerprint · passwordVersion
```
Défauts : expiration +1 h, purge +7 j.
## Claims produit
```js
buildAccessTokenClaims: ({ user, context }) => ({
type: user.type,
scope: ['read', 'write'],
issuer: 'authServer@my-product'
})
```
Le package ajoute ensuite ses claims réservés. Il ne connaît ni `pmc-admin`, ni les rôles Rapidoo.
## Réutilisation access token
Un access token non expiré du même `userId + fingerprint` peut être réutilisé.
Compatibilité historique importante : le repository ne filtre pas un éventuel champ `valid` dans `findReusableAccessToken()` ni dans `findAccessTokenById()`.
Cette sémantique est documentée et n'est pas corrigée silencieusement en V1.
## Refresh tokens
Le client conserve l'_id Mongo du refresh token. Le document contient :
```text
userId · deviceFingerprint · passwordVersion · issuedAt · expiresAt · deleteAt · valid
```
Défauts : expiration +15 j, purge +45 j.
Au login, un refresh token `valid:true`, non expiré, du même user+fingerprint+passwordVersion peut être réutilisé et prolongé.
## Rotation refresh
```js
await auth.refresh({
refreshTokenId,
request,
context
});
```
Workflow :
```text
refresh exact valid:true ?
fingerprint identique ?
non expiré ?
user existe ?
non disabled ?
passwordVersion identique ?
↓
révoque ancien
↓
nouveau refresh
↓
nouvel access
```
Le refresh est donc rotatif.
## Password version
Un reset incrémente `passwordVersion`. Les refresh tokens portant une ancienne version sont refusés.
La V1 ne modifie pas la sémantique historique de vérification des access tokens.
## Logout
```js
await auth.logout(refreshTokenId);
```
La révocation est best-effort. Une panne Mongo ne doit pas empêcher le microservice de nettoyer ses cookies.
## Forgot password
```js
await auth.requestPasswordReset(mail);
```
Un compte absent ou disabled produit un résultat accepté sans révéler son existence. Pour un compte actif :
```text
token brut 256 bits
↓
SHA-256 persisté
↓
expiration +15 min
↓
livraison éventuelle
```
## Livraison du reset
`actions.deliverPasswordReset` est **bloquante** lorsqu'elle est fournie.
Le token est persisté avant la tentative de livraison; une panne du mail peut donc remonter une erreur après mutation.
Cette asymétrie avec la validation mail conserve le comportement réel extrait.
## Reset password
```js
await auth.resetPassword({
mail,
token,
newHash
});
```
Le moteur vérifie compte, statut, expiration et token, puis compare le SHA-256 via `timingSafeEqual`. Ensuite :
```text
bcrypt(newHash + currentPepper) · hash remplacé · pepperVersion courante · passwordVersion + 1 · resetToken = null · resetExpires = null
```
## Vérification token
```js
const payload = await auth.verifyAccessToken(token, request);
const { payload, user } = await auth.authenticate(token, request);
```
`validateToken({ token, request, context })` ajoute ensuite une éventuelle `policies.token`.
Le produit peut ainsi contrôler issuer/origine/admin sans code spécifique dans `@aventii/auth`.
## Désactivation
```js
await auth.disableAccount({
actorUserId,
targetUserId,
password,
context
});
```
Par défaut, `actor == target`. Le credential de l'acteur est vérifié. `policies.disableAccount` peut autoriser un administrateur.
Après succès :
```text
target.status = disabled · target.disabledAt = now · refresh tokens target => valid:false
```
Les access tokens ne sont pas invalidés en masse en V1, conformément au comportement extrait.
## Nettoyage
Le package n'installe aucun cron :
```js
await auth.cleanupExpired();
```
Cela délègue la suppression des access tokens expirés, refresh tokens expirés et inscriptions initiales incomplètes.
Optionnellement :
```js
await auth.cleanupExpired({
unverifiedMailBefore: new Date(...)
});
```
supprime les comptes en attente de validation mail avant la date donnée.
## Hooks et actions
Hooks best-effort :
```text
onRegistrationCompleted · onMailValidated · onAccountDisabled · onAuthEvent · onHookError
```
Actions :
```text
deliverMailValidation best-effort · deliverPasswordReset bloquante
```
Le package ne connaît aucun service mail ou notification concret.
## Middleware HTTP
```js
const authMiddleware = createAuthMiddleware({ auth });
```
Par défaut le token est lu dans `req.cookies.token`. En succès :
```js
req.user = user;
req.auth = { user, payload };
```
Un bypass externe peut être injecté :
```js
bypass: req => isValidInternalToken(req.headers.authorization)
```
Le package ne connaît pas les tokens internes ni les exceptions de routes produit.
## Router Express
```js
const router = createAuthRouter({
auth,
resolveRegistration,
cookieOptionsResolver
});
```
Routes relatives :
```text
GET /salt
POST /register
POST /register/complete
POST /mail/validate
POST /login
POST /refresh
POST /logout
POST /phone/validate/:id
POST /password/forgot
POST /password/reset
POST /token/validate
PATCH /account/disable/:id
```
## resolveRegistration
C'est la frontière principale avec le produit :
```js
resolveRegistration: async req => {
const accountData = validateMyProductRegistration(req);
return {
mail: accountData.mail,
accountData
};
}
```
Rapidoo y conserve âge, nationalité, téléphone, CGU, balance, Mobile Money et type de compte.
## Cookies
Le router ne connaît aucun domaine :
```js
cookieOptionsResolver: req => ({
httpOnly: true,
secure: true,
sameSite: 'none',
domain: resolveDomain(req),
path: '/'
})
```
Noms par défaut : `token`, `refresh_token`. Durées par défaut : 1 h et 15 j.
Les tokens privés de validation mail et de reset ne sont jamais exposés dans les réponses HTTP du router.
## Timing par défaut
```text
workFactor 12 · registrationTokenTtlMs 60 s · registrationMaxAttempts 5 · registrationPenaltyAfterAttempts 2 · registrationPenaltyTtlMs 3 s · mailValidationTtlMs 24 h · passwordResetTtlMs 15 min · accessTokenTtlMs 1 h · accessTokenRetentionMs 7 j · refreshTokenTtlMs 15 j · refreshTokenRetentionMs 45 j · revokedRefreshRetentionMs 30 j
```
## Articulation avec les autres packages
`@aventii/security` fournit les primitives de signature/vérification et le fingerprint est injecté. `enforceFingerprint` vaut `true` par défaut et peut être désactivé explicitement.
`@aventii/db` reste responsable de la connexion; le repository reçoit seulement des getters de collection.
`@aventii/link` peut être utilisé par les actions mail sans dépendance directe.
`@aventii/files` et `@aventii/document-validation` sont totalement indépendants de `@aventii/auth`.
Un futur identity-service pourra donc assembler les trois sans couplage.
## Hors périmètre
Le package ne gère pas :
-
profil public ou avatar ;
-
balance, transactions, top-up ou withdrawal ;
-
signalements ;
-
documents d'identité ou véhicule ;
-
stockage fichier ;
-
notifications métier concrètes ;
-
pays, numérotation, âge, CGU, devise ou Mobile Money ;
-
rôle admin prédéfini ;
-
audit Mongo imposé ;
-
service de clés distant ;
-
`.env`, runtime global, CORS, rate limiting ou Proof of Work.
## Persistance future
Le contrat repository rend une autre persistance possible.
La V1 fournit uniquement MongoDB natif car c'est l'implémentation réellement extraite.
Aucun adapter Mongoose ou SQL spéculatif n'est ajouté.
## Reconnexion de Rapidoo
La future réécriture du MS devra principalement fournir :
```text
resolveRegistration
policies.login
policies.token
policies.disableAccount
buildAccessTokenClaims
deliverMailValidation
deliverPasswordReset
onAuthEvent
onRegistrationCompleted / onMailValidated
cookieOptionsResolver
bypass du middleware
```
Le protocole auth lui-même reste dans le package.
## Erreurs
`AuthError` expose :
```js
{
name: 'AuthError',
message: '[auth] ...',
code,
status,
userId,
details,
cause
}
```
Les erreurs de contrat de construction utilisent `TypeError`.
## Tests
```bash
npm test
```
La suite utilise `node:test` et `node:assert/strict`.
Aucun Mongo réel, Express réel, bcrypt natif ou service distant n'est nécessaire aux tests.
Les tests couvrent salt, register, tentatives, completion, e-mail, téléphone, login, claims, réutilisation tokens, migration pepper, policies, audit, refresh rotation, passwordVersion, forgot/reset, token validation, disable, logout best-effort, cleanup, repository Mongo, cookies, router, middleware et bypass.
## Résumé
`@aventii/auth` possède :
```text
compte
credential
inscription
sessions
access token
refresh token
validation mail
validation téléphone
forgot/reset
désactivation
```
Le produit possède :
```text
qui peut s'inscrire · avec quelles données · dans quel pays · avec quel rôle · avec quelles conséquences
```
**Auth prouve qu'un compte peut accéder au système. Le microservice compose cette capacité avec le métier du produit.**
Dependencies
Dependencies
| ID | Version |
|---|---|
| @aventii/security | ^1.2.0 |
| bcrypt | ^6.0.0 |
Peer Dependencies
| ID | Version |
|---|---|
| express | >=5 |
| mongodb | >=6 |