@nodefony/security 10.0.0-alpha.8 → 10.0.0-alpha.9
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/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/command/security-user-add.js +2 -18
- package/dist/nodefony/command/security-user-password.js +2 -2
- package/dist/nodefony/config/config.js +2 -2
- package/dist/nodefony/service/auditService.js +3 -2
- package/dist/nodefony/service/tokenService.js +3 -2
- package/dist/nodefony/service/totp.js +5 -3
- package/dist/nodefony/service/webAuthn.js +3 -2
- package/dist/nodefony/service/webhooks.js +5 -3
- package/dist/nodefony/src/token/MemoryTokenStore.js +18 -5
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +15 -3
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +24 -6
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +18 -10
- package/dist/types/nodefony/command/security-user-add.d.ts +0 -16
- package/dist/types/nodefony/contracts/IAuditEvent.d.ts +3 -3
- package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +0 -9
- package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +0 -8
- package/docs/api-keys.md +62 -60
- package/docs/audit.md +77 -77
- package/docs/authenticators.md +55 -43
- package/docs/authorization.md +13 -13
- package/docs/cors.md +29 -29
- package/docs/csrf.md +26 -26
- package/docs/external-jwt.md +4 -4
- package/docs/firewall.md +29 -29
- package/docs/headers.md +26 -26
- package/docs/oauth2.md +60 -60
- package/docs/obtenir-un-jeton.md +2 -2
- package/docs/tokens.md +55 -55
- package/docs/totp.md +73 -72
- package/docs/webauthn.md +81 -81
- package/docs/webhooks.md +43 -37
- package/package.json +15 -12
package/docs/api-keys.md
CHANGED
|
@@ -115,7 +115,7 @@ qu'il arrive côté serveur — révoquer exige d'ajouter… un état serveur (d
|
|
|
115
115
|
lecture qu'on voulait éviter. Une clé d'API vit **des mois** : un jeton auto-porté de six mois qui
|
|
116
116
|
fuite est une porte ouverte de six mois. Le PAT inverse le compromis : sa vérité est dans le store,
|
|
117
117
|
donc `revokedAt` posé = accès coupé à la requête suivante, sans attendre aucune expiration
|
|
118
|
-
(`ApiKeyAuthenticator.authenticate()`, `ApiKeyAuthenticator.ts:
|
|
118
|
+
(`ApiKeyAuthenticator.authenticate()`, `ApiKeyAuthenticator.ts:97-104`).
|
|
119
119
|
|
|
120
120
|
**Ce que Nodefony fait pour que ça reste bon marché** — trois décisions ancrées au code :
|
|
121
121
|
|
|
@@ -193,7 +193,7 @@ export default OrdersController;
|
|
|
193
193
|
### 3. Émettre la clé — par l'API, avec une session
|
|
194
194
|
|
|
195
195
|
L'émission est un **endpoint fourni**, monté seulement si le service `apiKeys` existe
|
|
196
|
-
(`mountApiKeyRoutes()`, `ApiKeyController.ts:
|
|
196
|
+
(`mountApiKeyRoutes()`, `ApiKeyController.ts:192`). Il n'y a **pas** de commande CLI pour créer une
|
|
197
197
|
clé : la création exige une **session BFF** (les routes ne sont pas `bypassFirewall` —
|
|
198
198
|
`ApiKeyController.ts:50-54`), parce que le porteur est **toujours** l'utilisateur courant, jamais un
|
|
199
199
|
paramètre.
|
|
@@ -228,7 +228,7 @@ curl -si https://localhost:5152/api/v1/orders/list | head -1
|
|
|
228
228
|
|
|
229
229
|
> [!WARNING]
|
|
230
230
|
> Le champ `token` de la réponse 201 est la **seule** occasion de lire le secret : il n'est pas
|
|
231
|
-
> stocké, donc pas re-dérivable (`IApiKeyCreated`, `IApiKey.ts:
|
|
231
|
+
> stocké, donc pas re-dérivable (`IApiKeyCreated`, `IApiKey.ts:44`). Le listing ultérieur ne
|
|
232
232
|
> renvoie que le `prefix` public (`#toView()`, `apiKeys.ts:393`). Perdue = ré-émise.
|
|
233
233
|
|
|
234
234
|
## 🔐 Anatomie d'une clé — ce que chaque morceau paie
|
|
@@ -239,7 +239,7 @@ raison est dans le code (`apiKeyFormat.ts:8-10`) : le charset base64url contient
|
|
|
239
239
|
|
|
240
240
|
| Morceau | Taille | Secret ? | À quoi ça sert |
|
|
241
241
|
| -------- | -------------------- | :------: | -------------------------------------------------------------------------------------------------- |
|
|
242
|
-
| `prefix` | ≤ 12 car. minuscules | non | Marque applicative — discrimine du JWT, aide le secret-scanning (`config.ts:
|
|
242
|
+
| `prefix` | ≤ 12 car. minuscules | non | Marque applicative — discrimine du JWT, aide le secret-scanning (`config.ts:736`) |
|
|
243
243
|
| `pubid` | 6 octets → 8 car. | non | Identifiant affichable dans la console (`nf_a1b2c3d4`) — `generateApiKey()` (`apiKeyFormat.ts:92`) |
|
|
244
244
|
| `secret` | 32 octets → 43 car. | **oui** | 256 bits d'entropie — `SECRET_BYTES` (`apiKeyFormat.ts:30`) |
|
|
245
245
|
| `crc` | 4 octets → 6 car. | non | CRC32 du `prefix_pubid+secret` — `crcChunk()` (`apiKeyFormat.ts:63`) |
|
|
@@ -268,7 +268,7 @@ dépendance ni d'une variation de version Node (`crc32()`, `apiKeyFormat.ts:53`)
|
|
|
268
268
|
|
|
269
269
|
Avant même de parser, le firewall doit savoir **quel** authenticator prend la main. C'est
|
|
270
270
|
`looksLikeApiKey()` (`apiKeyFormat.ts:117`) : un simple `startsWith("<prefix>_")`, appelé par
|
|
271
|
-
`ApiKeyAuthenticator.supports()` (`ApiKeyAuthenticator.ts:
|
|
271
|
+
`ApiKeyAuthenticator.supports()` (`ApiKeyAuthenticator.ts:75`). C'est ce qui rend la cohabitation
|
|
272
272
|
`["apikey", "jwt"]` possible dans une même zone — un JWT a la structure `a.b.c`, il ne commence
|
|
273
273
|
jamais par le préfixe.
|
|
274
274
|
|
|
@@ -281,19 +281,19 @@ jamais par le préfixe.
|
|
|
281
281
|
1. **Validation du nom** — non vide, ≤ 100 caractères ; sinon `ApiKeyError` 400 (`#normalizeName()`,
|
|
282
282
|
`apiKeys.ts:337`).
|
|
283
283
|
2. **Validation des scopes** — tableau de chaînes non vides, dédupliquées, et **⊆ catalogue** si
|
|
284
|
-
`allowedScopes` est défini ; sinon 400 (`#normalizeScopes()`, `apiKeys.ts:
|
|
284
|
+
`allowedScopes` est défini ; sinon 400 (`#normalizeScopes()`, `apiKeys.ts:348`).
|
|
285
285
|
3. **Résolution de l'expiration** — `expiresInDays` explicite, sinon le défaut de config, `null` =
|
|
286
286
|
sans expiration ; une valeur non positive lève un 400 (`#resolveExpiry()`, `apiKeys.ts:370`).
|
|
287
287
|
4. **Plafond anti-abus** — on ne compte que les clés **actives** (ni révoquées ni expirées) via
|
|
288
|
-
`#isActive()` (`apiKeys.ts:386`) ; au-delà de `maxPerSubject` → 409 (`apiKeys.ts:
|
|
288
|
+
`#isActive()` (`apiKeys.ts:386`) ; au-delà de `maxPerSubject` → 409 (`apiKeys.ts:55`).
|
|
289
289
|
5. **Génération** — 32 octets aléatoires, `publicPrefix` et `secretHash` dérivés
|
|
290
290
|
(`generateApiKey()`, `apiKeyFormat.ts:92`).
|
|
291
|
-
6. **Écriture** — le `record` de `kind:"pat"` posé au store par `store.put()` (`apiKeys.ts:
|
|
291
|
+
6. **Écriture** — le `record` de `kind:"pat"` posé au store par `store.put()` (`apiKeys.ts:177`).
|
|
292
292
|
7. **Audit** — `apikey.created`, catégorie `token`, avec l'**id public** et les scopes, **jamais le
|
|
293
|
-
secret** (`apiKeys.ts:
|
|
293
|
+
secret** (`apiKeys.ts:183-189`).
|
|
294
294
|
|
|
295
295
|
Le service ne connaît pas le store à la construction : il le résout **paresseusement** du container
|
|
296
|
-
au premier usage (`#resolveStore()`, `apiKeys.ts:
|
|
296
|
+
au premier usage (`#resolveStore()`, `apiKeys.ts:324`) — indépendant de l'ordre de boot. Store
|
|
297
297
|
absent = **503 explicite**, jamais une 500 opaque.
|
|
298
298
|
|
|
299
299
|
### Vérification — `ApiKeyAuthenticator.authenticate()`
|
|
@@ -304,12 +304,12 @@ filtre qui coûte plus cher que la précédente :
|
|
|
304
304
|
| # | Contrôle | Coût | Ancrage |
|
|
305
305
|
| --- | --------------------------------------- | --------------- | ----------------------------------------------------- |
|
|
306
306
|
| 1 | Bearer présent + préfixe | regex | `readBearerHeader()` (`runtime/bearer.ts:68`) |
|
|
307
|
-
| 2 | Longueur, charset, **CRC** | CPU local | `parseApiKey` (`ApiKeyAuthenticator.ts:
|
|
308
|
-
| 3 | Lookup par `secretHash` | 1 lecture store | `findByHash` (`ApiKeyAuthenticator.ts:
|
|
309
|
-
| 4 | `kind:"pat"`, non révoquée, non expirée | en mémoire | `ApiKeyAuthenticator.ts:
|
|
310
|
-
| 5 | Porteur banni ? (`invalidBefore`) | 1 lecture store | `getInvalidBefore` (`ApiKeyAuthenticator.ts:
|
|
307
|
+
| 2 | Longueur, charset, **CRC** | CPU local | `parseApiKey` (`ApiKeyAuthenticator.ts:103`) |
|
|
308
|
+
| 3 | Lookup par `secretHash` | 1 lecture store | `findByHash` (`ApiKeyAuthenticator.ts:109`) |
|
|
309
|
+
| 4 | `kind:"pat"`, non révoquée, non expirée | en mémoire | `ApiKeyAuthenticator.ts:111-118` |
|
|
310
|
+
| 5 | Porteur banni ? (`invalidBefore`) | 1 lecture store | `getInvalidBefore` (`ApiKeyAuthenticator.ts:121`) |
|
|
311
311
|
| 6 | Compte actif et non verrouillé | 1 lecture user | `#resolveUserOrReject` (`ApiKeyAuthenticator.ts:209`) |
|
|
312
|
-
| 7 | `lastUsedAt` (throttlé) | 0 ou 1 écriture | `markUsed` (`ApiKeyAuthenticator.ts:
|
|
312
|
+
| 7 | `lastUsedAt` (throttlé) | 0 ou 1 écriture | `markUsed` (`ApiKeyAuthenticator.ts:187`) |
|
|
313
313
|
|
|
314
314
|
Deux points méritent d'être soulignés parce qu'ils décident du niveau de sécurité réel :
|
|
315
315
|
|
|
@@ -324,13 +324,13 @@ Deux points méritent d'être soulignés parce qu'ils décident du niveau de sé
|
|
|
324
324
|
|
|
325
325
|
En cas de succès, le jeton est promu et porte trois attributs consommés en aval : `scopes`,
|
|
326
326
|
`apiKeyId` et `tenantId` (`ApiKeyAuthenticator.ts:138-140`). Le challenge renvoyé sur un 401 de la
|
|
327
|
-
zone est un simple `Bearer` (`challenge()`, `ApiKeyAuthenticator.ts:
|
|
327
|
+
zone est un simple `Bearer` (`challenge()`, `ApiKeyAuthenticator.ts:205`).
|
|
328
328
|
|
|
329
329
|
### Câblage — d'où viennent le préfixe et le throttle
|
|
330
330
|
|
|
331
331
|
L'authenticator n'est jamais instancié à la main : le firewall le construit depuis le registre, en
|
|
332
332
|
lui injectant la config effective — `registerAuthenticatorFactory("apikey")`
|
|
333
|
-
(`authenticatorRegistry.ts:
|
|
333
|
+
(`authenticatorRegistry.ts:137`), qui lit `prefix` et `lastUsedThrottleS`
|
|
334
334
|
(`authenticatorRegistry.ts:143`). Conséquence pratique : changer `apiKeys.prefix` change **à la
|
|
335
335
|
fois** l'émission et la reconnaissance — les anciennes clés ne sont plus reconnues.
|
|
336
336
|
|
|
@@ -353,13 +353,13 @@ apiKeys: {
|
|
|
353
353
|
**Ce qu'on observe** : `npx nodefony security:user:add ci-bot` (rôle `ROLE_USER` par défaut), login
|
|
354
354
|
en tant que `ci-bot`, puis émission avec `{"scopes":["orders:read"]}`. Demander
|
|
355
355
|
`{"scopes":["orders:write"]}` renvoie **400 `scope not allowed: orders:write`** — refusé à
|
|
356
|
-
l'émission, pas seulement à l'usage (`#normalizeScopes()`, `apiKeys.ts:
|
|
356
|
+
l'émission, pas seulement à l'usage (`#normalizeScopes()`, `apiKeys.ts:348`).
|
|
357
357
|
|
|
358
358
|
> [!TIP]
|
|
359
359
|
> Le catalogue `allowedScopes` de la config est un **complément**, pas la source : la console
|
|
360
360
|
> propose aussi les scopes **découverts sur tes routes** (`@RequireScope`) par
|
|
361
361
|
> `collectDeclaredApiScopes()` (`scopeCatalog.ts:29`), agrégés dans l'endpoint `capabilities`
|
|
362
|
-
> (`ApiKeyController.ts:
|
|
362
|
+
> (`ApiKeyController.ts:97`). Un formulaire de création qui ne ment pas.
|
|
363
363
|
|
|
364
364
|
### Faire tourner une clé sans coupure de service
|
|
365
365
|
|
|
@@ -368,7 +368,7 @@ doit y avoir **aucune** fenêtre pendant laquelle le job échoue.
|
|
|
368
368
|
|
|
369
369
|
**Il n'y a pas de bouton « rotate »** — et c'est délibéré : une rotation atomique impliquerait
|
|
370
370
|
soit deux secrets valides sous le même id (ambigu à auditer), soit une coupure. Le motif est le
|
|
371
|
-
**recouvrement**, rendu possible par le plafond `maxPerSubject` (`apiKeys.ts:
|
|
371
|
+
**recouvrement**, rendu possible par le plafond `maxPerSubject` (`apiKeys.ts:55`) :
|
|
372
372
|
|
|
373
373
|
1. Émettre une **seconde** clé (même porteur, mêmes scopes, nom `CI deploy v2`).
|
|
374
374
|
2. Déployer le nouveau secret dans le CI.
|
|
@@ -376,7 +376,7 @@ soit deux secrets valides sous le même id (ambigu à auditer), soit une coupure
|
|
|
376
376
|
4. **Puis** révoquer la v1.
|
|
377
377
|
|
|
378
378
|
Ce qui rend l'étape 3 fiable : `lastUsedAt` est écrit de façon **throttlée**, pas à chaque requête —
|
|
379
|
-
la fenêtre par défaut est de 60 s (`lastUsedThrottleS`, `config.ts:
|
|
379
|
+
la fenêtre par défaut est de 60 s (`lastUsedThrottleS`, `config.ts:752`). Attends donc une minute
|
|
380
380
|
avant de conclure qu'une clé « ne sert plus ».
|
|
381
381
|
|
|
382
382
|
### Révoquer une clé qui a fuité
|
|
@@ -388,25 +388,25 @@ Deux chemins, selon qui agit :
|
|
|
388
388
|
|
|
389
389
|
| Qui | Endpoint | Portée | Ancrage |
|
|
390
390
|
| ---------- | ------------------------------------------------- | ---------------------- | --------------------------------------- |
|
|
391
|
-
| Le porteur | `DELETE /nodefony/security/api/keys/{id}` | **ses** clés seulement | `revokeForSubject()` (`apiKeys.ts:
|
|
392
|
-
| Un admin | `POST /nodefony/security/api/apikeys/{id}/revoke` | n'importe quelle clé | `revokeAnyPat()` (`apiKeys.ts:
|
|
391
|
+
| Le porteur | `DELETE /nodefony/security/api/keys/{id}` | **ses** clés seulement | `revokeForSubject()` (`apiKeys.ts:303`) |
|
|
392
|
+
| Un admin | `POST /nodefony/security/api/apikeys/{id}/revoke` | n'importe quelle clé | `revokeAnyPat()` (`apiKeys.ts:257`) |
|
|
393
393
|
|
|
394
394
|
**Ce qu'on observe** : la révocation est **idempotente** et prend effet à la requête suivante —
|
|
395
|
-
l'authenticator lit `revokedAt` avant toute autre décision (`ApiKeyAuthenticator.ts:
|
|
395
|
+
l'authenticator lit `revokedAt` avant toute autre décision (`ApiKeyAuthenticator.ts:111-118`).
|
|
396
396
|
Le banc d'intégration le prouve bout en bout : 200 avant, 401 après
|
|
397
|
-
(`apikey-flow.test.ts:
|
|
397
|
+
(`apikey-flow.test.ts:182-191`).
|
|
398
398
|
|
|
399
399
|
Une propriété de sécurité facile à manquer : si la clé n'existe pas **ou** appartient à quelqu'un
|
|
400
400
|
d'autre, le porteur reçoit un **404 indiscernable**, jamais un 403 (`ApiKeyController.ts:139-142`).
|
|
401
401
|
Un 403 dirait « cette clé existe, mais pas à toi » — assez pour énumérer les identifiants des
|
|
402
|
-
autres. Le banc couvre explicitement cet IDOR (`apikey-flow.test.ts:
|
|
402
|
+
autres. Le banc couvre explicitement cet IDOR (`apikey-flow.test.ts:209`).
|
|
403
403
|
|
|
404
404
|
> [!WARNING]
|
|
405
405
|
> Si tu dois couper **toutes** les clés d'un porteur d'un coup (compte compromis, départ), ne les
|
|
406
406
|
> révoque pas une par une : pose le seuil `invalidBefore` du porteur
|
|
407
407
|
> (`revokeAllForSubject`, `ITokenStore.ts:261`). L'authenticator rejette alors toute clé créée avant
|
|
408
408
|
> ce seuil : `getInvalidBefore` est comparé au `createdAt` du record
|
|
409
|
-
> (`ApiKeyAuthenticator.ts:
|
|
409
|
+
> (`ApiKeyAuthenticator.ts:121-124`) — y compris pour les clés que tu aurais oubliées.
|
|
410
410
|
|
|
411
411
|
### Auditer qui a utilisé quoi
|
|
412
412
|
|
|
@@ -417,9 +417,9 @@ Trois sources, et il faut connaître les limites de chacune :
|
|
|
417
417
|
|
|
418
418
|
1. **L'état** — le listing d'administration paginé, tous porteurs confondus : `GET
|
|
419
419
|
/nodefony/security/api/apikeys` (`SecurityAdminApi.ts:394`), servi par `listPagePat()`
|
|
420
|
-
(`apiKeys.ts:
|
|
420
|
+
(`apiKeys.ts:216`). Filtres `subjectId`, `revoked`, fenêtre `limit`/`offset`/`cursor` et tri
|
|
421
421
|
`order=champ:ASC` (`parseTokenListQuery()`, `SecurityAdminApi.ts:126`), plafonnée à 200 entrées
|
|
422
|
-
(`KEYS_MAX_LIMIT`, `SecurityAdminApi.ts:
|
|
422
|
+
(`KEYS_MAX_LIMIT`, `SecurityAdminApi.ts:114`).
|
|
423
423
|
|
|
424
424
|
Le tri n'est accepté que sur les champs que le backend branché **déclare** savoir trier
|
|
425
425
|
(`sortableFields()`, `apiKeys.ts:102` → `ITokenStore.sortableFields`) : `createdAt`, `name`,
|
|
@@ -430,30 +430,33 @@ Trois sources, et il faut connaître les limites de chacune :
|
|
|
430
430
|
du vocabulaire : le placement des valeurs absentes diffère d'un moteur à l'autre, et un tri dont
|
|
431
431
|
l'ordre dépend de la base configurée ne vaut pas mieux qu'un tri absent.
|
|
432
432
|
|
|
433
|
-
2. **Le journal** — les événements d'audit `apikey.created` (`apiKeys.ts:
|
|
434
|
-
(`apiKeys.ts:
|
|
433
|
+
2. **Le journal** — les événements d'audit `apikey.created` (`apiKeys.ts:183-189`) et `apikey.revoked`
|
|
434
|
+
(`apiKeys.ts:268-271` côté admin, `apiKeys.ts:311-313` côté porteur), catégorie `token`. La révocation
|
|
435
435
|
admin trace **l'acteur ET le porteur cible** — voir [audit](./audit.md).
|
|
436
|
-
3. **Le dernier usage** — `lastUsedAt
|
|
436
|
+
3. **Le dernier usage** — `lastUsedAt`, avec l'IP et l'agent de ce dernier appel
|
|
437
|
+
(`lastUsedIp`, `lastUsedUserAgent`, `ITokenStore.ts:135`), écrits ensemble par `markUsed`
|
|
438
|
+
(`ApiKeyAuthenticator.ts:187`). L'IP est lue par l'accesseur du contexte, qui tient compte des
|
|
439
|
+
proxys de confiance ; si elle ne peut pas être lue, le champ reste vide sans faire échouer
|
|
440
|
+
l'authentification.
|
|
437
441
|
|
|
438
|
-
Ce que tu **n'auras pas** : un journal par requête.
|
|
439
|
-
(`
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
alimenter, pas le store de jetons.
|
|
442
|
+
Ce que tu **n'auras pas** : un journal par requête. Le dernier usage est **écrasé** à chaque
|
|
443
|
+
écriture, et cette écriture est throttlée (`lastUsedThrottleS`) : entre deux écritures, les appels
|
|
444
|
+
ne laissent aucune trace dans le store. Pour de la traçabilité par appel, c'est le journal d'audit
|
|
445
|
+
applicatif qu'il faut alimenter, pas le store de jetons.
|
|
443
446
|
|
|
444
447
|
## ⚙️ Configuration
|
|
445
448
|
|
|
446
|
-
Table dérivée du schéma Zod `apiKeysSchema` (`config.ts:
|
|
447
|
-
module (`config.ts:
|
|
449
|
+
Table dérivée du schéma Zod `apiKeysSchema` (`config.ts:733`), branché à la racine de la config du
|
|
450
|
+
module (`config.ts:733`). Toutes les valeurs ci-dessous sont les **défauts réels**.
|
|
448
451
|
|
|
449
452
|
| Option | Type | Défaut | Effet |
|
|
450
453
|
| ------------------- | ---------------- | ------ | -------------------------------------------------------------------------------------- |
|
|
451
|
-
| `enabled` | boolean | `true` | Coupe l'émission ET le listing (l'authenticator reste déclarable) (`config.ts:
|
|
452
|
-
| `prefix` | string ≤ 12 | `"nf"` | Marque des clés ; minuscules/chiffres — discrimine du JWT (`config.ts:
|
|
453
|
-
| `defaultExpiryDays` | number \| null | `90` | Expiration appliquée si l'appelant n'en donne pas ; `null` = jamais (`config.ts:
|
|
454
|
-
| `lastUsedThrottleS` | number (s) | `60` | Coalescence d'écriture de `lastUsedAt` ; `0` = à chaque usage (`config.ts:
|
|
455
|
-
| `maxPerSubject` | number > 0 | `100` | Plafond de clés **actives** par porteur ; au-delà → 409 (`config.ts:
|
|
456
|
-
| `allowedScopes` | string[] \| null | `null` | Catalogue fermé à la création ; `null` = tout scope non vide accepté (`config.ts:
|
|
454
|
+
| `enabled` | boolean | `true` | Coupe l'émission ET le listing (l'authenticator reste déclarable) (`config.ts:735`) |
|
|
455
|
+
| `prefix` | string ≤ 12 | `"nf"` | Marque des clés ; minuscules/chiffres — discrimine du JWT (`config.ts:736`) |
|
|
456
|
+
| `defaultExpiryDays` | number \| null | `90` | Expiration appliquée si l'appelant n'en donne pas ; `null` = jamais (`config.ts:745`) |
|
|
457
|
+
| `lastUsedThrottleS` | number (s) | `60` | Coalescence d'écriture de `lastUsedAt` ; `0` = à chaque usage (`config.ts:752`) |
|
|
458
|
+
| `maxPerSubject` | number > 0 | `100` | Plafond de clés **actives** par porteur ; au-delà → 409 (`config.ts:761`) |
|
|
459
|
+
| `allowedScopes` | string[] \| null | `null` | Catalogue fermé à la création ; `null` = tout scope non vide accepté (`config.ts:770`) |
|
|
457
460
|
|
|
458
461
|
Deux réglages méritent une décision consciente :
|
|
459
462
|
|
|
@@ -468,15 +471,15 @@ Deux réglages méritent une décision consciente :
|
|
|
468
471
|
### Les endpoints — deux portées, jamais mélangées
|
|
469
472
|
|
|
470
473
|
**Console « mes clés »** (le porteur gère les siennes) — montées par `mountApiKeyRoutes()`
|
|
471
|
-
(`ApiKeyController.ts:
|
|
474
|
+
(`ApiKeyController.ts:192`) **seulement si** le service `apiKeys` existe (`framework/index.ts:468`) ;
|
|
472
475
|
sinon 404, zéro surface. Aucune n'est `bypassFirewall` : la zone data plane exige la session BFF.
|
|
473
476
|
|
|
474
477
|
| Méthode | Chemin | Rôle | Ancrage |
|
|
475
478
|
| -------- | ------------------------------------------ | ------------------------------------------------- | ------------------------------------------- |
|
|
476
|
-
| `POST` | `/nodefony/security/api/keys` | Émission → **201** + `token` clair (1×) | `create()` (`ApiKeyController.ts:
|
|
477
|
-
| `GET` | `/nodefony/security/api/keys` | Mes clés, sans secret | `list()` (`ApiKeyController.ts:
|
|
478
|
-
| `GET` | `/nodefony/security/api/keys/capabilities` | Plafond, scopes proposés, préfixe, durée | `capabilities()` (`ApiKeyController.ts:
|
|
479
|
-
| `DELETE` | `/nodefony/security/api/keys/{id}` | Révoque **ma** clé ; 404 sinon (anti-énumération) | `revoke()` (`ApiKeyController.ts:
|
|
479
|
+
| `POST` | `/nodefony/security/api/keys` | Émission → **201** + `token` clair (1×) | `create()` (`ApiKeyController.ts:66`) |
|
|
480
|
+
| `GET` | `/nodefony/security/api/keys` | Mes clés, sans secret | `list()` (`ApiKeyController.ts:114`) |
|
|
481
|
+
| `GET` | `/nodefony/security/api/keys/capabilities` | Plafond, scopes proposés, préfixe, durée | `capabilities()` (`ApiKeyController.ts:97`) |
|
|
482
|
+
| `DELETE` | `/nodefony/security/api/keys/{id}` | Révoque **ma** clé ; 404 sinon (anti-énumération) | `revoke()` (`ApiKeyController.ts:127`) |
|
|
480
483
|
|
|
481
484
|
**Administration** (gouvernance, réponse à incident) — data plane `SecurityAdminApi`, RBAC
|
|
482
485
|
`ROLE_NODEFONY_ADMIN` :
|
|
@@ -490,7 +493,7 @@ sinon 404, zéro surface. Aucune n'est `bypassFirewall` : la zone data plane exi
|
|
|
490
493
|
Les deux espaces de chemins sont **disjoints** (`keys` vs `apikeys`) — aucune collision, et une
|
|
491
494
|
console d'admin ne peut pas atterrir par erreur sur l'endpoint personnel.
|
|
492
495
|
|
|
493
|
-
Codes d'erreur mappés par duck-typing sur `code` (`#renderApiKeyError()`, `ApiKeyController.ts:
|
|
496
|
+
Codes d'erreur mappés par duck-typing sur `code` (`#renderApiKeyError()`, `ApiKeyController.ts:165`) :
|
|
494
497
|
**400** validation (nom, scope, durée), **409** plafond atteint, **503** clés indisponibles (store
|
|
495
498
|
absent ou `enabled:false`).
|
|
496
499
|
|
|
@@ -514,8 +517,8 @@ import type {
|
|
|
514
517
|
```
|
|
515
518
|
|
|
516
519
|
Les contrats vivent dans `IApiKey.ts` : `IApiKeyView` (`IApiKey.ts:6`), `IApiKeyCreated`
|
|
517
|
-
(`IApiKey.ts:
|
|
518
|
-
(`IApiKey.ts:
|
|
520
|
+
(`IApiKey.ts:44`), `IApiKeyCapabilities` (`IApiKey.ts:55`), `ICreateApiKeyOptions`
|
|
521
|
+
(`IApiKey.ts:69`). Les signatures détaillées vivent dans le graphe TSDoc (`.ai/symbols.json`) —
|
|
519
522
|
cette page explique l'usage, elle ne recopie pas les prototypes.
|
|
520
523
|
|
|
521
524
|
## 🧑⚖️ Scopes — ce que la clé a le droit de faire
|
|
@@ -588,12 +591,12 @@ révocation ne traverse pas. Le détail de la résolution, des avertissements et
|
|
|
588
591
|
| Schéma `Bearer` (transport) | RFC 6750 §2.1 | `readBearerHeader()` (`runtime/bearer.ts:68`) |
|
|
589
592
|
| `invalid_token` → 401 + challenge | RFC 6750 §3.1 · RFC 7235 | `challenge()` (`ApiKeyAuthenticator.ts:205`) |
|
|
590
593
|
| Secret **jamais** stocké en clair | OWASP ASVS (secret storage) | `hashApiKey()` (`apiKeyFormat.ts:70`) |
|
|
591
|
-
| Secret montré une seule fois | Pratique « shown once » | `IApiKeyCreated.token` (`IApiKey.ts:
|
|
594
|
+
| Secret montré une seule fois | Pratique « shown once » | `IApiKeyCreated.token` (`IApiKey.ts:46`) |
|
|
592
595
|
| Anti-énumération des ressources | OWASP API1:2023 (BOLA/IDOR) | 404 indiscernable (`ApiKeyController.ts:139-142`) |
|
|
593
596
|
| Message d'échec uniforme | OWASP API2:2023 (Broken Auth) | `INVALID_TOKEN` (`ApiKeyAuthenticator.ts:17`) |
|
|
594
|
-
| Révocation immédiate côté serveur | OWASP API2:2023 | `revokedAt` vérifié (`ApiKeyAuthenticator.ts:
|
|
597
|
+
| Révocation immédiate côté serveur | OWASP API2:2023 | `revokedAt` vérifié (`ApiKeyAuthenticator.ts:111-118`) |
|
|
595
598
|
| Entropie du secret (≥ 128 bits) | NIST SP 800-63B | 32 octets aléatoires (`apiKeyFormat.ts:30`) |
|
|
596
|
-
| Plafond de ressources par acteur | OWASP API4:2023 (Resource Consumption) | `maxPerSubject` (`apiKeys.ts:
|
|
599
|
+
| Plafond de ressources par acteur | OWASP API4:2023 (Resource Consumption) | `maxPerSubject` (`apiKeys.ts:55`) |
|
|
597
600
|
|
|
598
601
|
## ⚡ Performance & mémoire
|
|
599
602
|
|
|
@@ -645,14 +648,13 @@ pas masqué à l'affichage. Voir aussi l'écran **Audit** pour les événements
|
|
|
645
648
|
| 503 « API keys unavailable » | Store non provisionné (`TokenService` absent/désactivé) (`apiKeys.ts:330`) | Vérifier `jwt`/`tokenStore` — le `TokenService` pose le store |
|
|
646
649
|
| 401 à la création de clé | Ces routes exigent une **session** (pas de `bypassFirewall`) | Se connecter d'abord (`/nodefony/security/api/auth/login`) |
|
|
647
650
|
| Le token clair est introuvable après coup | Seul `sha256` est stocké — non re-dérivable (`apiKeyFormat.ts:70`) | Émettre une nouvelle clé, révoquer l'ancienne |
|
|
648
|
-
| 409 « API key limit reached » | Plafond de clés **actives** atteint (`apiKeys.ts:
|
|
649
|
-
| 400 « scope not allowed » | Scope hors du catalogue `allowedScopes` (`apiKeys.ts:
|
|
651
|
+
| 409 « API key limit reached » | Plafond de clés **actives** atteint (`apiKeys.ts:144-147`) | Révoquer les clés inutilisées ou relever `maxPerSubject` |
|
|
652
|
+
| 400 « scope not allowed » | Scope hors du catalogue `allowedScopes` (`apiKeys.ts:363`) | Ajouter le scope au catalogue, ou corriger la demande |
|
|
650
653
|
| Toutes les clés rejetées après un changement de config | `prefix` modifié → les anciennes ne sont plus reconnues (`authenticatorRegistry.ts:142`) | Garder le `prefix` STABLE après la première émission |
|
|
651
654
|
| Clé valide mais 403 sur la route | Autorisation, pas authentification : scope manquant — `ScopeVoter.vote()` (`ScopeVoter.ts:50`) | Émettre une clé portant le scope exigé par `@RequireScope` |
|
|
652
|
-
| Clé rejetée alors qu'elle n'est ni expirée ni révoquée | Porteur désactivé/verrouillé, ou seuil `invalidBefore` (`ApiKeyAuthenticator.ts:
|
|
655
|
+
| Clé rejetée alors qu'elle n'est ni expirée ni révoquée | Porteur désactivé/verrouillé, ou seuil `invalidBefore` (`ApiKeyAuthenticator.ts:121-124`) | Réactiver le compte, ou réémettre après le bannissement |
|
|
653
656
|
| 404 en révoquant la clé d'un autre porteur | Anti-énumération volontaire, jamais 403 (`ApiKeyController.ts:139-142`) | Attendu — passer par l'endpoint d'administration |
|
|
654
657
|
| `lastUsedAt` qui ne bouge pas tout de suite | Écriture throttlée, 60 s par défaut (`ApiKeyAuthenticator.ts:127-134`) | Attendre la fenêtre, ou `lastUsedThrottleS: 0` (coût : 1 écriture/req) |
|
|
655
|
-
| `lastUsedIp` / `lastUsedUserAgent` toujours vides | `markUsed` n'envoie que l'horodatage (`ApiKeyAuthenticator.ts:133`) | Emplacements réservés — tracer par le journal d'audit applicatif |
|
|
656
658
|
| Révocation sans effet entre pods | Store `memory` en production (per-pod) | Store durable partagé — voir [tokens](./tokens.md) |
|
|
657
659
|
| Listing d'admin sans `total` sur Redis | Comptage exact refusé (O(N)) — pagination par curseur | Attendu : capacité réduite annoncée, paginer par `nextCursor` |
|
|
658
660
|
|