@nodefony/security 10.0.0-beta.3 → 10.0.0-beta.5
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.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +6 -8
- package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +18 -11
- package/dist/nodefony/command/security-oauth-doctor.js +5 -3
- package/dist/nodefony/command/security-secrets.js +10 -4
- package/dist/nodefony/command/security-user-add.js +9 -3
- package/dist/nodefony/command/security-user-delete.js +5 -3
- package/dist/nodefony/command/security-user-list.js +5 -3
- package/dist/nodefony/command/security-user-password.js +5 -3
- package/dist/nodefony/config/config.js +20 -4
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/contracts/IAccessVoter.js +1 -2
- package/dist/nodefony/errors/AccessDeniedError.js +1 -2
- package/dist/nodefony/errors/ApiKeyError.js +3 -2
- package/dist/nodefony/errors/AuthenticationError.js +1 -2
- package/dist/nodefony/errors/CsrfError.js +1 -2
- package/dist/nodefony/errors/InsecureTransportError.js +1 -2
- package/dist/nodefony/errors/InvalidTargetError.js +1 -2
- package/dist/nodefony/errors/SsrfError.js +1 -2
- package/dist/nodefony/errors/ThrottledError.js +1 -2
- package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
- package/dist/nodefony/errors/WebAuthnError.js +1 -2
- package/dist/nodefony/keycloak/keycloakAdmin.js +1 -2
- package/dist/nodefony/keycloak/keycloakRealm.js +1 -2
- package/dist/nodefony/keycloak/keycloakRealmInput.js +1 -2
- package/dist/nodefony/keycloak/scaffold.js +3 -3
- package/dist/nodefony/keycloak/security-keycloak-realm.js +5 -3
- package/dist/nodefony/scaffold/contribute.js +1 -2
- package/dist/nodefony/service/accessTokenVerifier.js +101 -34
- package/dist/nodefony/service/apiKeys.js +45 -5
- package/dist/nodefony/service/auditService.js +4 -5
- package/dist/nodefony/service/authFlow.js +10 -8
- package/dist/nodefony/service/authorization.js +1 -2
- package/dist/nodefony/service/cors.js +1 -2
- package/dist/nodefony/service/csrf.js +1 -2
- package/dist/nodefony/service/firewall.js +5 -4
- package/dist/nodefony/service/keycloak.js +1 -2
- package/dist/nodefony/service/oauth2.js +29 -7
- package/dist/nodefony/service/securityHeaders.js +1 -2
- package/dist/nodefony/service/tokenService.js +38 -22
- package/dist/nodefony/service/totp.js +4 -5
- package/dist/nodefony/service/webAuthn.js +4 -5
- package/dist/nodefony/service/webhooks.js +4 -5
- package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
- package/dist/nodefony/src/SecuredArea.js +1 -2
- package/dist/nodefony/src/accountBinding.js +96 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +1 -2
- package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
- package/dist/nodefony/src/admin/adminAudit.js +1 -2
- package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
- package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
- package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
- package/dist/nodefony/src/audit/auditBridge.js +1 -2
- package/dist/nodefony/src/audit/auditFilters.js +1 -2
- package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
- package/dist/nodefony/src/audit/readAuditContext.js +1 -2
- package/dist/nodefony/src/audit/recordAudit.js +1 -2
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +8 -113
- package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +42 -4
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +91 -16
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +3 -17
- package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
- package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
- package/dist/nodefony/src/boot/publicRoutesNotice.js +1 -2
- package/dist/nodefony/src/crypto/secretCipher.js +1 -2
- package/dist/nodefony/src/csp.js +1 -2
- package/dist/nodefony/src/csrfToken.js +1 -2
- package/dist/nodefony/src/entryPoint.js +1 -2
- package/dist/nodefony/src/net/ssrfGuard.js +1 -2
- package/dist/nodefony/src/net/transportFailure.js +57 -0
- package/dist/nodefony/src/oauth/clientCredentials.js +1 -2
- package/dist/nodefony/src/oauth/httpJson.js +3 -3
- package/dist/nodefony/src/oauth/metadata.js +5 -16
- package/dist/nodefony/src/oauth/oauth2Client.js +1 -2
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +1 -2
- package/dist/nodefony/src/oauth/providerDiagnosis.js +5 -6
- package/dist/nodefony/src/oauth/providerRoles.js +1 -2
- package/dist/nodefony/src/oauth/providers/github.js +16 -6
- package/dist/nodefony/src/oauth/providers/oidc.js +9 -6
- package/dist/nodefony/src/openToSignup.js +1 -2
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
- package/dist/nodefony/src/realtime/frameAuthorizer.js +2 -3
- package/dist/nodefony/src/rolesWithin.js +57 -0
- package/dist/nodefony/src/sessionAuthentication.js +1 -2
- package/dist/nodefony/src/sessionIdentity.js +29 -3
- package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
- package/dist/nodefony/src/token/AnonymousToken.js +1 -2
- package/dist/nodefony/src/token/JwtKeystore.js +14 -5
- package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +6 -3
- package/dist/nodefony/src/token/UserToken.js +1 -2
- package/dist/nodefony/src/token/jwtRuntime.js +1 -2
- package/dist/nodefony/src/token/scopeGrants.js +109 -0
- package/dist/nodefony/src/token/secretFile.js +1 -2
- package/dist/nodefony/src/token/tokenCriteria.js +1 -2
- package/dist/nodefony/src/token/tokenFilters.js +1 -2
- package/dist/nodefony/src/token/tokenSort.js +1 -2
- package/dist/nodefony/src/token/tokenStatus.js +1 -2
- package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
- package/dist/nodefony/src/totp/totpCipher.js +1 -2
- package/dist/nodefony/src/totp/totpCrypto.js +1 -2
- package/dist/nodefony/src/totp/totpOperations.js +1 -2
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
- package/dist/nodefony/src/transport/CredentialTransportPolicy.js +1 -2
- package/dist/nodefony/src/voter/RoleVoter.js +1 -2
- package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
- package/dist/nodefony/src/voter/voterRegistry.js +1 -2
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
- package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
- package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
- package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
- package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
- package/dist/nodefony/src/webhook/webhookSort.js +1 -2
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
- package/dist/nodefony/testing/index.js +108 -0
- package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +7 -6
- package/dist/types/nodefony/config/config.d.ts +1 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
- package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
- package/dist/types/nodefony/service/oauth2.d.ts +20 -0
- package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +23 -5
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -44
- package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +4 -1
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
- package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
- package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +2 -2
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
- package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
- package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
- package/dist/types/nodefony/testing/index.d.ts +51 -0
- package/docs/api-keys.md +181 -174
- package/docs/audit.md +140 -140
- package/docs/authenticators.md +101 -101
- package/docs/authorization.md +116 -69
- package/docs/cors.md +64 -64
- package/docs/csrf.md +74 -71
- package/docs/external-jwt.md +22 -12
- package/docs/firewall.md +85 -82
- package/docs/headers.md +81 -81
- package/docs/index.md +4 -4
- package/docs/keycloak.md +95 -78
- package/docs/login-page.md +23 -23
- package/docs/oauth2.md +157 -146
- package/docs/obtenir-un-jeton.md +116 -26
- package/docs/tokens.md +128 -122
- package/docs/totp.md +136 -136
- package/docs/webauthn.md +153 -153
- package/docs/webhooks.md +136 -124
- package/package.json +19 -13
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/obtenir-un-jeton.md
CHANGED
|
@@ -5,7 +5,7 @@ lang: fr
|
|
|
5
5
|
module: "@nodefony/security"
|
|
6
6
|
topic: obtenir-un-jeton
|
|
7
7
|
coverageModule: security
|
|
8
|
-
coverageFiles: "security-token,tokenService,secretFile"
|
|
8
|
+
coverageFiles: "security-mcp-token,tokenService,secretFile"
|
|
9
9
|
section: "Sécurité"
|
|
10
10
|
audience: [developer, devops]
|
|
11
11
|
tags:
|
|
@@ -33,7 +33,7 @@ source: "src/packages/@nodefony/security/docs/obtenir-un-jeton.md"
|
|
|
33
33
|
> l'application répond un couple access/refresh. Cette page décrit l'autre voie, celle dont un
|
|
34
34
|
> client MCP ou un script d'exploitation a besoin : **l'application signe elle-même un jeton, en
|
|
35
35
|
> ligne de commande**, sans qu'aucun serveur n'écoute et sans qu'aucun mot de passe ne transite.
|
|
36
|
-
> Ancré sur `src/packages/@nodefony/security/nodefony/command/security-token.ts`.
|
|
36
|
+
> Ancré sur `src/packages/@nodefony/security/nodefony/command/security-mcp-token.ts`.
|
|
37
37
|
|
|
38
38
|
📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **Obtenir un jeton**
|
|
39
39
|
|
|
@@ -43,7 +43,7 @@ Trois questions, et une seule commande y répond.
|
|
|
43
43
|
|
|
44
44
|
```mermaid
|
|
45
45
|
flowchart LR
|
|
46
|
-
A["nodefony security:token"] --> B{"L'application possède<br/>sa clé de signature"}
|
|
46
|
+
A["nodefony security:mcp:token"] --> B{"L'application possède<br/>sa clé de signature"}
|
|
47
47
|
B --> C["Jeton signé pour<br/>UNE audience"]
|
|
48
48
|
C --> D["--write : posé où<br/>l'agent le lit"]
|
|
49
49
|
C --> E["--json : capturé<br/>par un script"]
|
|
@@ -51,11 +51,11 @@ flowchart LR
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
**Aucun serveur n'est en marche.** La commande démarre le noyau jusqu'à `onReady` — services prêts,
|
|
54
|
-
aucune écoute réseau (`security-token.ts
|
|
54
|
+
aucune écoute réseau (`security-mcp-token.ts#options`). C'est ce qui la rend utilisable quand l'application
|
|
55
55
|
ne tourne pas, en intégration continue, ou dans un conteneur d'amorçage.
|
|
56
56
|
|
|
57
57
|
**Le jeton vise une porte, et une seule.** L'audience est celle de la porte visée — la porte MCP par
|
|
58
|
-
défaut, `/nodefony/mcp` (`src/nodefony/src/
|
|
58
|
+
défaut, `/nodefony/mcp` (`src/nodefony/src/cli/aiMcpReport.ts#MCP_ENDPOINT_PATH`). Un jeton d'une autre audience est
|
|
59
59
|
refusé, et c'est toute la raison d'être de la liaison d'audience
|
|
60
60
|
([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html)).
|
|
61
61
|
|
|
@@ -69,7 +69,7 @@ refusé, et c'est toute la raison d'être de la liaison d'audience
|
|
|
69
69
|
- **Audience** — la ressource pour laquelle un jeton est valable. Déclarée par l'émetteur ; un
|
|
70
70
|
porteur ne choisit pas la sienne.
|
|
71
71
|
- **Scope** — un pouvoir nommé, porté par le jeton. `admin:read` lit, `admin:write` mute
|
|
72
|
-
(`src/nodefony/src/kernel/adminPlane/adminCaller.ts
|
|
72
|
+
(`src/nodefony/src/kernel/adminPlane/adminCaller.ts#ADMIN_SCOPE_READ`).
|
|
73
73
|
|
|
74
74
|
## Qu'est-ce que ça résout — la faille du `curl` avec mot de passe
|
|
75
75
|
|
|
@@ -85,13 +85,13 @@ pas de mot de passe, pas de réseau — et rien à effacer d'un historique.
|
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
87
|
# 1) Le jeton, pour la porte MCP de cette application
|
|
88
|
-
npx nodefony security:token
|
|
88
|
+
npx nodefony security:mcp:token
|
|
89
89
|
|
|
90
90
|
# 2) Le poser là où l'agent le lit — jamais dans un fichier suivi par git
|
|
91
|
-
npx nodefony security:token --write
|
|
91
|
+
npx nodefony security:mcp:token --write
|
|
92
92
|
|
|
93
93
|
# 3) Pour un script : la sortie machine
|
|
94
|
-
npx nodefony security:token --json
|
|
94
|
+
npx nodefony security:mcp:token --json
|
|
95
95
|
# {
|
|
96
96
|
# "access_token": "eyJ…",
|
|
97
97
|
# "resource": "http://localhost:5151/nodefony/mcp",
|
|
@@ -136,9 +136,9 @@ Puis `npm run build` — le runtime lit le `dist`, pas la source.
|
|
|
136
136
|
| `-j, --json` | — | sortie machine, sans invite |
|
|
137
137
|
|
|
138
138
|
Le plafond de `--ttl` n'est pas décoratif : un jeton posé dans un fichier **est une clé**, et une
|
|
139
|
-
clé se remplace (`security-token.ts
|
|
139
|
+
clé se remplace (`security-mcp-token.ts#ttlSeconds`).
|
|
140
140
|
|
|
141
|
-
**Posé chez un agent, le jeton vit 8 h par défaut** (`security-token.ts
|
|
141
|
+
**Posé chez un agent, le jeton vit 8 h par défaut** (`security-mcp-token.ts#tokenTtlSeconds`). L'en-tête d'un
|
|
142
142
|
agent est figé : rien ne le rafraîchit, et le défaut de la configuration (15 min, taillé pour un
|
|
143
143
|
client d'API qui renouvelle son jeton) produisait un 401 au bout d'un quart d'heure. Huit heures
|
|
144
144
|
couvrent une journée de travail et bornent une fuite à quelques heures ; `--ttl` explicite
|
|
@@ -147,10 +147,10 @@ d'**expiration** (`expires_at` en `--json`) : c'est elle qui explique le 401 du
|
|
|
147
147
|
|
|
148
148
|
### Où `--write` pose la valeur
|
|
149
149
|
|
|
150
|
-
La variable est `NF_MCP_TOKEN` (`src/nodefony/src/cli/aiMcpReport.ts
|
|
150
|
+
La variable est `NF_MCP_TOKEN` (`src/nodefony/src/cli/aiMcpReport.ts#MCP_TOKEN_ENV`). La commande ne sert que
|
|
151
151
|
les agents dont la présence est **constatée** dans le projet — on ne crée pas la configuration d'un
|
|
152
152
|
outil que personne n'utilise ici. La table des cibles vit au cœur
|
|
153
|
-
(`src/nodefony/src/cli/agentTargets.ts
|
|
153
|
+
(`src/nodefony/src/cli/agentTargets.ts#AGENT_TARGETS`) : Claude, Gemini, Vibe, Codex.
|
|
154
154
|
|
|
155
155
|
Deux garanties qui ne se négocient pas :
|
|
156
156
|
|
|
@@ -177,38 +177,128 @@ codex mcp add nodefony --url http://localhost:5151/nodefony/mcp \
|
|
|
177
177
|
Pour câbler `.mcp.json` d'un seul geste : `npx nodefony ai:mcp --auth`. La déclaration de la porte
|
|
178
178
|
appartient à [devkit](../../devkit/docs/index.md) ; cette page ne fait qu'émettre le porteur.
|
|
179
179
|
|
|
180
|
+
## 🔐 Ce que la porte vérifie — un jeton de cette application face à une ressource
|
|
181
|
+
|
|
182
|
+
Une ressource protégée — une porte MCP d'application (`nodefony create controller <nom> --kind mcp
|
|
183
|
+
--expose oauth`), ou la porte de développement quand elle déclare un serveur d'autorisation — ne
|
|
184
|
+
vérifie pas le jeton elle-même : elle le confie au service `accessTokenVerifier`, avec **son** URI
|
|
185
|
+
comme audience. Ce service reconnaît **toujours** l'émetteur de l'application, dès que sa capacité
|
|
186
|
+
JWT est active (`security.jwt.enabled`, vrai par défaut), **sans rien déclarer** dans
|
|
187
|
+
`resourceServer.issuers` (`src/packages/@nodefony/security/nodefony/service/accessTokenVerifier.ts#AccessTokenVerifierService.#build`).
|
|
188
|
+
Une application qui signe un jeton pour sa propre porte peut donc l'y présenter.
|
|
189
|
+
|
|
190
|
+
**L'aiguillage se fait sur l'émetteur revendiqué, comparé EXACTEMENT** à `security.jwt.issuer`
|
|
191
|
+
(`accessTokenVerifier.ts#AccessTokenVerifierService.#build`). Égal : le jeton est jugé par les règles de l'authentificateur `jwt`,
|
|
192
|
+
par la même fonction (`src/packages/@nodefony/security/nodefony/src/authenticator/JwtAuthenticator.ts#JwtAuthenticator.#verify`).
|
|
193
|
+
Différent : il est jugé par la liste fermée des émetteurs tiers (Keycloak…, voir
|
|
194
|
+
[Accepter les jetons d'un émetteur tiers](external-jwt.md)) — inconnu d'elle, il est refusé sans
|
|
195
|
+
aucune requête sortante.
|
|
196
|
+
|
|
197
|
+
Un jeton de l'application n'est accepté que si **chaque** contrôle passe, dans cet ordre :
|
|
198
|
+
|
|
199
|
+
| # | Contrôle | Refusé, par exemple | Norme |
|
|
200
|
+
| --- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
201
|
+
| 1 | signature par une clé du jeu LOCAL, algorithme `EdDSA` seul | `alg: none`, `HS256` signé avec la clé publique, clé d'un tiers sous le même `kid` | [RFC 8725](https://www.rfc-editor.org/rfc/rfc8725.html) §3.1, §3.5 |
|
|
202
|
+
| 2 | `iss` = l'émetteur de l'application | `https://app.example/` quand l'émetteur est `https://app.example` (aiguillé ailleurs) | RFC 8725 §3.8 |
|
|
203
|
+
| 3 | `aud` contient l'URI de la porte | un jeton émis pour une AUTRE porte de la même application | [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html), RFC 8725 §3.9 |
|
|
204
|
+
| 4 | `typ: at+jwt` | un jeton de rafraîchissement présenté comme jeton d'accès, un `typ` absent | [RFC 9068](https://www.rfc-editor.org/rfc/rfc9068.html), RFC 8725 §3.11 |
|
|
205
|
+
| 5 | `exp` et `nbf` | expiré, pas encore valide | [RFC 7519](https://www.rfc-editor.org/rfc/rfc7519.html) §4.1.4-5 |
|
|
206
|
+
| 6 | `sub` et `jti` présents | un jeton sans identifiant — donc impossible à révoquer | |
|
|
207
|
+
| 7 | `jti` absent de la liste de révocation | un jeton révoqué individuellement | |
|
|
208
|
+
| 8 | émis APRÈS le seuil `invalidBefore` du compte | tout jeton antérieur à un changement de mot de passe ou à une déconnexion globale | |
|
|
209
|
+
| 9 | le compte existe, est actif et n'est pas verrouillé | le jeton d'un compte désactivé, verrouillé ou supprimé | RFC 8725 §3.10 |
|
|
210
|
+
|
|
211
|
+
Les contrôles 7 et 8 viennent **avant** la lecture du compte : un jeton révoqué est refusé sans
|
|
212
|
+
interroger l'annuaire (`JwtAuthenticator.ts#JwtAuthenticator.#verify`). Une PANNE de la liste de révocation est un
|
|
213
|
+
`503` ; une panne de l'annuaire, elle, se lit comme un compte absent — `401`. Dans les deux cas le
|
|
214
|
+
jeton n'est jamais servi : c'est une limite de diagnostic, pas d'accès. Le jeton accepté donne à la porte un principal :
|
|
215
|
+
son sujet (`sub`, l'identifiant du compte local) et ses scopes, qui décident des outils servis.
|
|
216
|
+
|
|
217
|
+
| Verdict du vérificateur | Réponse de la porte |
|
|
218
|
+
| -------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
219
|
+
| principal | servi — les outils réservés que ses scopes ouvrent en plus |
|
|
220
|
+
| refus (l'un des contrôles) | `401` + `WWW-Authenticate`, qui désigne le document RFC 9728 |
|
|
221
|
+
| invérifiable (clés, liste de révocation ou annuaire absents) | `503`, et le journal crie — jamais un jeton accepté par défaut |
|
|
222
|
+
| aucun vérificateur (capacité JWT coupée, aucun émetteur tiers) | `503` |
|
|
223
|
+
|
|
224
|
+
**Révoquer un jeton émis**, c'est révoquer le compte qui le porte : `npx nodefony
|
|
225
|
+
security:user:password <compte>` pose le seuil du contrôle 8 — le prochain appel reçoit `401`, avant
|
|
226
|
+
l'expiration du jeton. C'est éprouvé sur un serveur réel par le banc de vérité du générateur
|
|
227
|
+
(étape « la porte MCP protégée s'ouvre… et se referme révoquée »).
|
|
228
|
+
|
|
229
|
+
**Ce que la règle n'élargit pas.** Aucun jeton nouveau ne devient valide : l'émetteur ne signe que
|
|
230
|
+
pour les audiences de `jwt.audiences`, et l'audience reste exigée à chaque porte. La zone de
|
|
231
|
+
pare-feu `external-jwt` ne prend, elle, que les jetons des émetteurs qu'ELLE déclare : la règle ne
|
|
232
|
+
lui ouvre rien. Et si `resourceServer.issuers` est mal configurée, **aucun** vérificateur n'est
|
|
233
|
+
posé, jetons de l'application compris — une configuration fausse ne se sert pas à moitié
|
|
234
|
+
(`accessTokenVerifier.ts#AccessTokenVerifierService.#build`).
|
|
235
|
+
|
|
236
|
+
> ⚠️ **En production, pose `NF_JWT_ISSUER`.** Sans elle, l'émetteur vaut `"nodefony"`, qui n'est
|
|
237
|
+
> pas une URL : les jetons se vérifient, mais l'application ne publie ni ses clés ni ses métadonnées,
|
|
238
|
+
> et une porte qui annonce `https://…` comme serveur d'autorisation désigne une adresse dont
|
|
239
|
+
> l'émetteur réel n'est pas celui des jetons.
|
|
240
|
+
|
|
241
|
+
Pour une porte d'application, l'audience à déclarer est l'URI publique de la porte. Dans une
|
|
242
|
+
application générée, écris-la **sur place** dans `jwt.audiences` de `nodefony/config/security.ts` :
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
audiences: ctx.isProd
|
|
246
|
+
? []
|
|
247
|
+
: [
|
|
248
|
+
// … les audiences du gabarit
|
|
249
|
+
`${publicBaseUrl(ctx.env)}/api/mcp`,
|
|
250
|
+
],
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
⚠️ N'y importe pas la fonction `…GateResource` du fichier de la porte : ce fichier importe
|
|
254
|
+
`security.ts`, et le cycle ferait lire `publicBaseUrl` avant sa définition — `ReferenceError` au
|
|
255
|
+
démarrage.
|
|
256
|
+
|
|
180
257
|
## 🔄 Renouveler — c'est réémettre, pas rafraîchir
|
|
181
258
|
|
|
182
259
|
Il n'y a pas de refresh token ici : ce jeton est signé hors bande, pour une porte, avec une durée
|
|
183
|
-
courte. Le renouveler, c'est **relancer la commande
|
|
184
|
-
|
|
260
|
+
courte. Le renouveler, c'est **relancer la commande** : `--write` **remplace** le jeton chez les
|
|
261
|
+
agents qui le portent déjà, sans reposer de question, et ne câble aucun agent nouveau sans le
|
|
262
|
+
proposer (`agentTargets.ts#poseVariable`). Puis relancer l'agent, qui lit sa configuration au
|
|
263
|
+
démarrage.
|
|
185
264
|
|
|
186
265
|
```bash
|
|
187
|
-
|
|
188
|
-
# 2) réémettre
|
|
189
|
-
npx nodefony security:token --write --ttl 1440
|
|
266
|
+
npx nodefony security:mcp:token --write --ttl 1440
|
|
190
267
|
```
|
|
191
268
|
|
|
269
|
+
Un jeton expiré ne casse rien de visible chez l'agent : la porte le sert en **anonyme** et retient
|
|
270
|
+
les outils réservés. `nodefony doctor` le signale dans sa famille « Fraîcheur du build », avec
|
|
271
|
+
cette commande pour geste (`freshness.ts#checkAgentTokens`) — pour les fichiers du projet
|
|
272
|
+
seulement : le dossier de l'utilisateur est partagé entre applications, et `nodefony ai:mcp` en
|
|
273
|
+
rend compte.
|
|
274
|
+
|
|
192
275
|
Le flux de rotation **avec détection de rejeu** est celui des refresh tokens de la porte HTTP — il
|
|
193
276
|
est décrit dans [tokens](tokens.md), pas ici.
|
|
194
277
|
|
|
195
278
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
196
279
|
|
|
197
|
-
| Symptôme | Cause | Correction
|
|
198
|
-
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
199
|
-
| `impossible d'émettre un jeton pour cette porte ici` | **en développement** : l'audience n'est pas déclarée à l'émetteur — c'est une liste blanche, pas un défaut ouvert | `use("@nodefony/security", { jwt: { audiences: ["<la porte>"] } })`, puis `npm run build`
|
|
200
|
-
| Le même message, mais l'environnement affiché n'est pas `development` | la porte MCP est servie par un module `policy: "dev"` : **elle n'existe pas** dans cet environnement | `NODE_ENV=development npx nodefony security:token`, ou viser une autre porte avec `--resource`
|
|
201
|
-
| Le jeton est émis, mais avec **moins** de scopes que demandé | l'émetteur retire ceux que ce porteur ne peut pas obtenir, et il le dit ([RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) §3.3) | lire le champ `scopes` de la sortie — c'est celui qui fait foi (`security-token.ts
|
|
202
|
-
| L'agent reçoit `401` alors que le jeton vient d'être posé | la valeur a été écrite dans un dossier que **cet** agent ne lit pas | `--agent <nom>` pour viser explicitement, ou l'`export` dans le shell qui lance l'agent
|
|
203
|
-
| `--write` ne fait rien et n'écrit aucun fichier | aucun agent n'est **constaté** dans ce projet | la commande liste alors les emplacements connus et donne la ligne d'`export`
|
|
280
|
+
| Symptôme | Cause | Correction |
|
|
281
|
+
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
282
|
+
| `impossible d'émettre un jeton pour cette porte ici` | **en développement** : l'audience n'est pas déclarée à l'émetteur — c'est une liste blanche, pas un défaut ouvert | `use("@nodefony/security", { jwt: { audiences: ["<la porte>"] } })`, puis `npm run build` |
|
|
283
|
+
| Le même message, mais l'environnement affiché n'est pas `development` | la porte MCP est servie par un module `policy: "dev"` : **elle n'existe pas** dans cet environnement | `NODE_ENV=development npx nodefony security:mcp:token`, ou viser une autre porte avec `--resource` |
|
|
284
|
+
| Le jeton est émis, mais avec **moins** de scopes que demandé | l'émetteur retire ceux que ce porteur ne peut pas obtenir, et il le dit ([RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) §3.3) | lire le champ `scopes` de la sortie — c'est celui qui fait foi (`security-mcp-token.ts#SecurityMcpToken.generate`) |
|
|
285
|
+
| L'agent reçoit `401` alors que le jeton vient d'être posé | la valeur a été écrite dans un dossier que **cet** agent ne lit pas | `--agent <nom>` pour viser explicitement, ou l'`export` dans le shell qui lance l'agent |
|
|
286
|
+
| `--write` ne fait rien et n'écrit aucun fichier | aucun agent n'est **constaté** dans ce projet | la commande liste alors les emplacements connus et donne la ligne d'`export` |
|
|
204
287
|
|
|
205
288
|
> 🔴 **Le premier réflexe est faux.** Devant un refus, on cherche un serveur éteint — la commande
|
|
206
289
|
> n'en utilise aucun. Le message écarte cette fausse piste dès sa deuxième ligne
|
|
207
|
-
> (`security-token.ts
|
|
290
|
+
> (`security-mcp-token.ts#SecurityMcpToken.generate`) ; il a lui-même été corrigé pour cesser d'accuser une cause unique
|
|
208
291
|
> qu'il ne constatait pas.
|
|
209
292
|
|
|
210
293
|
## 🧪 Tests & couverture
|
|
211
294
|
|
|
295
|
+
- **unit** : `accessTokenVerifier.attack.test.ts` éprouve la vérification d'un jeton de
|
|
296
|
+
l'application face à une ressource — chacun des neuf contrôles ci-dessus par un jeton qui le viole,
|
|
297
|
+
l'aiguillage entre émetteur local et tiers, les entrées hostiles, la configuration fausse — et
|
|
298
|
+
chacune de ses règles a été vue tomber en la débranchant ;
|
|
299
|
+
- **serveur réel** : le banc de vérité du générateur (`verify-generated.mjs`) ouvre la porte
|
|
300
|
+
d'une application engendrée avec `security:mcp:token`, la voit refuser un jeton d'une autre audience,
|
|
301
|
+
puis refuser le premier jeton après `security:user:password` ;
|
|
212
302
|
- **unit** : `securityCommands.test.ts` couvre les deux règles pures de la commande — un jeton
|
|
213
303
|
**mort-né** doit s'annoncer avant d'être copié, et `--ttl` doit s'écrire en minutes, refuser une
|
|
214
304
|
valeur aberrante et se borner à 30 jours (`ttlSeconds` est exportée pour cela).
|