@nodefony/security 10.0.0-beta.1 → 10.0.0-beta.3
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/README.md +7 -5
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +44 -10
- package/dist/nodefony/command/security-oauth-doctor.js +66 -0
- package/dist/nodefony/command/security-secrets.js +42 -25
- package/dist/nodefony/command/security-token.js +46 -15
- package/dist/nodefony/command/security-user-delete.js +5 -4
- package/dist/nodefony/command/security-user-password.js +4 -11
- package/dist/nodefony/config/config.js +80 -5
- package/dist/nodefony/contracts/index.js +1 -2
- package/dist/nodefony/errors/InsecureTransportError.js +24 -0
- package/dist/nodefony/errors/index.js +2 -1
- package/dist/nodefony/keycloak/keycloakAdmin.js +255 -0
- package/dist/nodefony/keycloak/keycloakRealm.js +260 -0
- package/dist/nodefony/keycloak/keycloakRealmInput.js +124 -0
- package/dist/nodefony/keycloak/scaffold.js +231 -0
- package/dist/nodefony/keycloak/security-keycloak-realm.js +128 -0
- package/dist/nodefony/scaffold/contribute.js +20 -0
- package/dist/nodefony/service/authFlow.js +111 -11
- package/dist/nodefony/service/firewall.js +111 -21
- package/dist/nodefony/service/keycloak.js +209 -0
- package/dist/nodefony/service/oauth2.js +379 -26
- package/dist/nodefony/service/securityHeaders.js +12 -1
- package/dist/nodefony/service/tokenService.js +57 -6
- package/dist/nodefony/service/totp.js +51 -6
- package/dist/nodefony/service/webAuthn.js +20 -2
- package/dist/nodefony/src/SecuredArea.js +14 -3
- package/dist/nodefony/src/admin/SecurityAdminApi.js +70 -40
- package/dist/nodefony/src/admin/adminAudit.js +0 -1
- package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +66 -6
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +38 -15
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +1 -0
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +13 -3
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +9 -2
- package/dist/nodefony/src/authenticator/bearer.js +1 -2
- package/dist/nodefony/src/boot/publicRoutesNotice.js +27 -0
- package/dist/nodefony/src/entryPoint.js +39 -0
- package/dist/nodefony/src/oauth/clientCredentials.js +40 -0
- package/dist/nodefony/src/oauth/metadata.js +36 -9
- package/dist/nodefony/src/oauth/oauth2Client.js +60 -33
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +39 -6
- package/dist/nodefony/src/oauth/providerDiagnosis.js +283 -0
- package/dist/nodefony/src/oauth/providerRoles.js +79 -0
- package/dist/nodefony/src/oauth/providers/github.js +32 -6
- package/dist/nodefony/src/oauth/providers/oidc.js +82 -3
- package/dist/nodefony/src/openToSignup.js +47 -0
- package/dist/nodefony/src/sessionAuthentication.js +26 -0
- package/dist/nodefony/src/token/JwtKeystore.js +78 -46
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +40 -13
- package/dist/nodefony/src/token/secretFile.js +59 -2
- package/dist/nodefony/src/transport/CredentialTransportPolicy.js +51 -0
- package/dist/types/index.d.ts +26 -6
- package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
- package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
- package/dist/types/nodefony/command/security-token.d.ts +9 -0
- package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
- package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
- package/dist/types/nodefony/config/config.d.ts +63 -1
- package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
- package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
- package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
- package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
- package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
- package/dist/types/nodefony/errors/index.d.ts +1 -0
- package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
- package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
- package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
- package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
- package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
- package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
- package/dist/types/nodefony/service/authFlow.d.ts +35 -3
- package/dist/types/nodefony/service/firewall.d.ts +17 -3
- package/dist/types/nodefony/service/keycloak.d.ts +115 -0
- package/dist/types/nodefony/service/oauth2.d.ts +125 -9
- package/dist/types/nodefony/service/securityHeaders.d.ts +2 -1
- package/dist/types/nodefony/service/tokenService.d.ts +7 -1
- package/dist/types/nodefony/service/totp.d.ts +28 -4
- package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
- package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +54 -12
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +18 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +13 -0
- package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
- package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
- package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
- package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
- package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
- package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
- package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
- package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
- package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
- package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
- package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
- package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
- package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
- package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
- package/docs/api-keys.md +18 -18
- package/docs/audit.md +24 -22
- package/docs/authenticators.md +62 -29
- package/docs/authorization.md +16 -16
- package/docs/cors.md +30 -30
- package/docs/csrf.md +35 -33
- package/docs/external-jwt.md +68 -5
- package/docs/firewall.md +78 -45
- package/docs/headers.md +35 -35
- package/docs/index.md +15 -7
- package/docs/keycloak.md +818 -0
- package/docs/lexique.md +15 -15
- package/docs/login-page.md +361 -0
- package/docs/oauth2.md +242 -125
- package/docs/obtenir-un-jeton.md +18 -11
- package/docs/tokens.md +138 -86
- package/docs/totp.md +113 -71
- package/docs/webauthn.md +115 -115
- package/docs/webhooks.md +21 -21
- package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/account/theme.properties +14 -0
- package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/admin/theme.properties +12 -0
- package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
- package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
- package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
- package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/email/theme.properties +8 -0
- package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
- package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
- package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
- package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
- package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/login/template.ftl +423 -0
- package/keycloak/themes/nodefony/login/theme.properties +17 -0
- package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
- package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
- package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
- package/package.json +17 -15
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Consomme [`@nodefony/user`](https://www.npmjs.com/package/@nodefony/user).
|
|
|
25
25
|
(cookie opaque BFF, révocable, `HttpOnly; Secure; SameSite`) ; les **API/agents** portent leur
|
|
26
26
|
preuve à chaque requête (**JWT** signé / clé API). Jamais « full stateless » : la session reste
|
|
27
27
|
la fondation web (révocation immédiate), le JWT est réservé au sans-état machine-à-machine.
|
|
28
|
-
- **Config type-safe** : schéma **Zod** (
|
|
28
|
+
- **Config type-safe** : schéma **Zod** (19 sections, la plupart désactivables par `enabled`), **introspectable** (Studio
|
|
29
29
|
génère son formulaire d'édition). L'app configure via `use("@nodefony/security", { … })`.
|
|
30
30
|
- **En-têtes natifs** (sans la lib `helmet`) — 0 dépendance, nonce CSP par requête.
|
|
31
31
|
|
|
@@ -67,7 +67,8 @@ export default defineConfig((ctx) => ({
|
|
|
67
67
|
|
|
68
68
|
> Une zone se déclare idéalement **par module** (override `module-security`, dans la config du
|
|
69
69
|
> module) pour vivre au plus près de ses routes. Authenticators fournis : `anonymous`, `session`,
|
|
70
|
-
> `userpassword`, `jwt`, `
|
|
70
|
+
> `userpassword`, `jwt`, `external-jwt`, `apikey`. WebAuthn, OAuth2 et TOTP n'en sont pas : ils
|
|
71
|
+
> ouvrent une session BFF, que l'authenticator `session` identifie ensuite. La chaîne est validée
|
|
71
72
|
> au boot (`mode: "first"` par défaut = le premier qui reconnaît authentifie ; `"all"` = tous
|
|
72
73
|
> requis, ex. mTLS + JWT). Config invalide → firewall **fail-closed** (tout rejeté).
|
|
73
74
|
|
|
@@ -117,7 +118,7 @@ csrf: {
|
|
|
117
118
|
> ouvrir la lecture CORS des réponses au JavaScript tiers. Une origine listée dans `cors.origins` est
|
|
118
119
|
> toutefois aussi acceptée (ce que CORS autorise explicitement n'est pas du CSRF).
|
|
119
120
|
|
|
120
|
-
Le token synchronizer renforcé
|
|
121
|
+
Le token synchronizer renforcé s'active par route avec `@CsrfProtect` ; `@CsrfExempt` en exclut une route.
|
|
121
122
|
|
|
122
123
|
### CORS — l'inverse du CSRF
|
|
123
124
|
|
|
@@ -141,8 +142,9 @@ const schema = securityConfigJsonSchema(); // JSON Schema → formulaire d'édit
|
|
|
141
142
|
## Journal d'audit (événements de sécurité)
|
|
142
143
|
|
|
143
144
|
Le journal trace les **transitions d'état** de sécurité (login, refus, jeton émis/révoqué, verrou
|
|
144
|
-
WS) — distinct du log de trafic (1 PDU/requête). Émission **explicite** par point sensible
|
|
145
|
-
|
|
145
|
+
WS) — distinct du log de trafic (1 PDU/requête). Émission **explicite** par point sensible, sur les
|
|
146
|
+
chemins froids (login, logout, émission ou révocation de jeton, refus) ; jamais par requête nominale :
|
|
147
|
+
le trafic courant ne passe pas par l'audit (cold-path)
|
|
146
148
|
→ aucun coût ajouté au hot-path nominal. Activé par défaut (`audit.enabled`, OWASP A09), coût nul si
|
|
147
149
|
désactivé. Lecture : `GET /nodefony/security/api/audit/events` (RBAC `ROLE_NODEFONY_ADMIN`).
|
|
148
150
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { JwtKeystore } from "./nodefony/src/token/JwtKeystore.js";
|
|
1
2
|
import config from "./nodefony/config/config.js";
|
|
2
3
|
import { SecuredArea } from "./nodefony/src/SecuredArea.js";
|
|
3
4
|
import { LoginThrottler } from "./nodefony/src/throttle/LoginThrottler.js";
|
|
5
|
+
import { InsecureTransportError } from "./nodefony/errors/InsecureTransportError.js";
|
|
6
|
+
import { CredentialTransportPolicy, shouldEnforceCredentialTransport } from "./nodefony/src/transport/CredentialTransportPolicy.js";
|
|
4
7
|
import { RoleHierarchyWalker } from "./nodefony/src/RoleHierarchyWalker.js";
|
|
5
8
|
import { CsrfTokenManager } from "./nodefony/src/csrfToken.js";
|
|
6
9
|
import { CsrfError } from "./nodefony/errors/CsrfError.js";
|
|
@@ -26,13 +29,13 @@ import { getAuthenticatorFactory, listAuthenticatorFactories, registerAuthentica
|
|
|
26
29
|
import { recordAudit } from "./nodefony/src/audit/recordAudit.js";
|
|
27
30
|
import { readAuditContext } from "./nodefony/src/audit/readAuditContext.js";
|
|
28
31
|
import { Firewall } from "./nodefony/service/firewall.js";
|
|
32
|
+
import { amrForFactor } from "./nodefony/src/sessionAuthentication.js";
|
|
29
33
|
import { AuthFlow } from "./nodefony/service/authFlow.js";
|
|
30
34
|
import { InvalidTargetError } from "./nodefony/errors/InvalidTargetError.js";
|
|
31
35
|
import { TOKEN_DEFAULT_ORDER, TOKEN_SORTABLE_FIELDS } from "./nodefony/src/token/tokenSort.js";
|
|
32
36
|
import { matchesTokenStatus, tokenStatusOf } from "./nodefony/src/token/tokenStatus.js";
|
|
33
37
|
import { MemoryTokenStore } from "./nodefony/src/token/MemoryTokenStore.js";
|
|
34
38
|
import { getTokenStoreFactory, listTokenStores, registerTokenStore } from "./nodefony/src/token/tokenStoreRegistry.js";
|
|
35
|
-
import { JwtKeystore } from "./nodefony/src/token/JwtKeystore.js";
|
|
36
39
|
import { TokenService } from "./nodefony/service/tokenService.js";
|
|
37
40
|
import { RemoteJwtVerifier } from "./nodefony/src/token/RemoteJwtVerifier.js";
|
|
38
41
|
import { AccessTokenVerifierService } from "./nodefony/service/accessTokenVerifier.js";
|
|
@@ -44,11 +47,13 @@ import { Authorization } from "./nodefony/service/authorization.js";
|
|
|
44
47
|
import { MemoryWebAuthnCredentialStore } from "./nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js";
|
|
45
48
|
import { getWebAuthnStoreFactory, listWebAuthnStores, registerWebAuthnStore } from "./nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js";
|
|
46
49
|
import { WebAuthnService } from "./nodefony/service/webAuthn.js";
|
|
50
|
+
import { KeycloakAdminClient, KeycloakAdminError, keycloakAdminTarget, keycloakLoginThemeCheck, readKeycloakLoginTheme } from "./nodefony/keycloak/keycloakAdmin.js";
|
|
47
51
|
import { OAuth2Client, OAuth2RequestError, OAuth2Tokens, createCodeChallenge, generateCodeVerifier, generateState } from "./nodefony/src/oauth/oauth2Client.js";
|
|
48
52
|
import { discoverAuthorizationServer } from "./nodefony/src/oauth/metadata.js";
|
|
49
|
-
import { createDiscoveredOidcProvider, createOidcProvider } from "./nodefony/src/oauth/providers/oidc.js";
|
|
53
|
+
import { createDiscoveredOidcProvider, createOidcProvider, readLogoutTokenClaims } from "./nodefony/src/oauth/providers/oidc.js";
|
|
50
54
|
import { createGithubProvider } from "./nodefony/src/oauth/providers/github.js";
|
|
51
|
-
import { getOAuthProviderFactory, listOAuthProviders, registerOAuthProvider } from "./nodefony/src/oauth/oauthProviderRegistry.js";
|
|
55
|
+
import { getOAuthProviderFactory, listOAuthProviders, oauthProviderRequiresIssuer, registerOAuthProvider } from "./nodefony/src/oauth/oauthProviderRegistry.js";
|
|
56
|
+
import { diagnoseOAuthProvider } from "./nodefony/src/oauth/providerDiagnosis.js";
|
|
52
57
|
import { OAuth2Service } from "./nodefony/service/oauth2.js";
|
|
53
58
|
import { TOKEN_FACETS } from "./nodefony/src/token/tokenFilters.js";
|
|
54
59
|
import { ApiKeyError } from "./nodefony/errors/ApiKeyError.js";
|
|
@@ -69,17 +74,22 @@ import { getWebhookStoreFactory, listWebhookStores, registerWebhookStore } from
|
|
|
69
74
|
import { SsrfError } from "./nodefony/errors/SsrfError.js";
|
|
70
75
|
import { assertPublicUrl, isBlockedAddress } from "./nodefony/src/net/ssrfGuard.js";
|
|
71
76
|
import { WebhookService } from "./nodefony/service/webhooks.js";
|
|
77
|
+
import { KeycloakService } from "./nodefony/service/keycloak.js";
|
|
72
78
|
import SecuritySecrets from "./nodefony/command/security-secrets.js";
|
|
73
79
|
import SecurityUserAdd from "./nodefony/command/security-user-add.js";
|
|
74
80
|
import SecurityUserList from "./nodefony/command/security-user-list.js";
|
|
75
81
|
import SecurityUserDelete from "./nodefony/command/security-user-delete.js";
|
|
76
82
|
import SecurityUserPassword from "./nodefony/command/security-user-password.js";
|
|
83
|
+
import { publicRoutesNotice } from "./nodefony/src/boot/publicRoutesNotice.js";
|
|
77
84
|
import SecurityToken from "./nodefony/command/security-token.js";
|
|
85
|
+
import SecurityKeycloakRealm from "./nodefony/keycloak/security-keycloak-realm.js";
|
|
86
|
+
import SecurityOAuthDoctor from "./nodefony/command/security-oauth-doctor.js";
|
|
78
87
|
import { createSecurityAdminApi, parseAuditQuery, registerSecurityAdminApi } from "./nodefony/src/admin/SecurityAdminApi.js";
|
|
79
|
-
import { registerUserRevocationCascade } from "./nodefony/src/admin/userRevocationCascade.js";
|
|
80
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
81
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
88
|
+
import { registerUserRevocationCascade, usersEventBusNotice } from "./nodefony/src/admin/userRevocationCascade.js";
|
|
89
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
90
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
82
91
|
import { tokenStatusCriteria } from "./nodefony/src/token/tokenCriteria.js";
|
|
92
|
+
import { requestClientCredentialsToken } from "./nodefony/src/oauth/clientCredentials.js";
|
|
83
93
|
import { AccessDeniedError } from "./nodefony/errors/AccessDeniedError.js";
|
|
84
94
|
import "./nodefony/errors/index.js";
|
|
85
95
|
import "./nodefony/contracts/index.js";
|
|
@@ -96,7 +106,7 @@ import { registerUserAdminApi } from "@nodefony/user";
|
|
|
96
106
|
* + Zod validée au boot (fail-closed si invalide). Consomme `@nodefony/user`
|
|
97
107
|
* (IUser/IUserProvider/IPasswordVerifier) — jamais l'inverse.
|
|
98
108
|
*
|
|
99
|
-
* Livré : firewall (zones, mode first|all, challenge RFC
|
|
109
|
+
* Livré : firewall (zones, mode first|all, challenge RFC 9110 §11), authenticators
|
|
100
110
|
* `anonymous`/`userpassword` (Basic RFC 7617)/`session` (BFF, J3), flux
|
|
101
111
|
* login/logout/me (`AuthFlow`, anti-fixation + throttling partagé).
|
|
102
112
|
* Jwt/oauth2/mtls/apikey, CORS, CSRF, autorisation par décorateurs et data
|
|
@@ -111,6 +121,8 @@ let Security = class Security extends Module {
|
|
|
111
121
|
this.addCommand(SecurityUserDelete);
|
|
112
122
|
this.addCommand(SecurityUserPassword);
|
|
113
123
|
this.addCommand(SecurityToken);
|
|
124
|
+
this.addCommand(SecurityKeycloakRealm);
|
|
125
|
+
this.addCommand(SecurityOAuthDoctor);
|
|
114
126
|
}
|
|
115
127
|
/**
|
|
116
128
|
* JSON Schema de la config security → data plane admin (config riche Studio).
|
|
@@ -135,7 +147,28 @@ let Security = class Security extends Module {
|
|
|
135
147
|
registerSecurityAdminApi(registry, container);
|
|
136
148
|
registerUserAdminApi(registry, container);
|
|
137
149
|
}
|
|
138
|
-
|
|
150
|
+
const kernel = this.kernel;
|
|
151
|
+
if (kernel && container) {
|
|
152
|
+
registerUserRevocationCascade(kernel, container);
|
|
153
|
+
kernel.once("onPostReady", () => {
|
|
154
|
+
const notice = usersEventBusNotice(container, kernel);
|
|
155
|
+
if (notice) kernel.reportBootNotice(notice);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Déclare au bilan de démarrage les routes métier laissées PUBLIQUES.
|
|
162
|
+
*
|
|
163
|
+
* `onReady` et pas `onBoot` : les zones sont compilées par le pare-feu à son
|
|
164
|
+
* propre `onBoot`, posé APRÈS celui du module — à `onBoot` la liste est encore
|
|
165
|
+
* vide. `onReady` précède l'écriture du bilan (`var/last-boot.json`).
|
|
166
|
+
*/
|
|
167
|
+
async onKernelReady() {
|
|
168
|
+
const firewall = this.kernel?.container?.get("firewall");
|
|
169
|
+
if (!firewall) return this;
|
|
170
|
+
const notice = publicRoutesNotice(firewall.describe().zones);
|
|
171
|
+
if (notice) this.kernel?.reportBootNotice(notice);
|
|
139
172
|
return this;
|
|
140
173
|
}
|
|
141
174
|
};
|
|
@@ -150,8 +183,9 @@ Security = __decorate([services([
|
|
|
150
183
|
OAuth2Service,
|
|
151
184
|
AuditService,
|
|
152
185
|
TotpService,
|
|
153
|
-
WebhookService
|
|
186
|
+
WebhookService,
|
|
187
|
+
KeycloakService
|
|
154
188
|
]), __decorateMetadata("design:paramtypes", [typeof Kernel === "undefined" ? Object : Kernel])], Security);
|
|
155
189
|
var security_default = Security;
|
|
156
190
|
//#endregion
|
|
157
|
-
export { AccessDeniedError, AccessTokenVerifierService, AnonymousAuthenticator, AnonymousToken, ApiKeyAuthenticator, ApiKeyError, ApiKeyService, AuditService, AuthFlow, AuthenticationError, Authorization, Cors, Csrf, CsrfError, CsrfTokenManager, ExternalJwtAuthenticator, Firewall, InvalidTargetError, JwtAuthenticator, JwtKeystore, LoginThrottler, MemoryAuditStore, MemoryTokenStore, MemoryTotpSecretStore, MemoryWebAuthnCredentialStore, MemoryWebhookStore, OAuth2Client, OAuth2RequestError, OAuth2Service, OAuth2Tokens, RemoteJwtVerifier, RoleHierarchyWalker, RoleVoter, ScopeVoter, SecuredArea, SecurityHeaders, SessionAuthenticator, SsrfError, TOKEN_DEFAULT_ORDER, TOKEN_FACETS, TOKEN_SORTABLE_FIELDS, ThrottledError, TokenService, TotpService, UnverifiableTokenError, UserPasswordAuthenticator, UserToken, VoterVote, WEBHOOK_DEFAULT_ORDER, WEBHOOK_SORTABLE_FIELDS, WebAuthnService, WebhookService, assertPublicUrl, base32Decode, beginTotpEnrollment, confirmTotpEnrollment, createCodeChallenge, createDiscoveredOidcProvider, createGithubProvider, createOidcProvider, createSecurityAdminApi, decryptSecret, security_default as default, defineSecurityConfig, deriveKey, deriveTotpKey, disableTotp, discoverAuthorizationServer, encryptSecret, generateApiKey, generateCodeVerifier, generateEphemeralKey, generateState, getAuditStoreFactory, getAuthenticatorFactory, getOAuthProviderFactory, getTokenStoreFactory, getTotpStoreFactory, getWebAuthnStoreFactory, getWebhookStoreFactory, hashApiKey, isBlockedAddress, listAuditStores, listAuthenticatorFactories, listOAuthProviders, listTokenStores, listTotpStores, listVoterFactories, listWebAuthnStores, listWebhookStores, looksLikeApiKey, matchesTokenStatus, parseApiKey, parseAuditQuery, peekIssuer, readAuditContext, recordAudit, registerAuditStore, registerAuthenticatorFactory, registerOAuthProvider, registerSecurityAdminApi, registerTokenStore, registerTotpStore, registerVoterFactory, registerWebAuthnStore, registerWebhookStore, resolveJwtRuntime, securityConfigJsonSchema, tokenStatusCriteria, tokenStatusOf, totpCode, totpStatus, verifyTotpLogin };
|
|
191
|
+
export { AccessDeniedError, AccessTokenVerifierService, AnonymousAuthenticator, AnonymousToken, ApiKeyAuthenticator, ApiKeyError, ApiKeyService, AuditService, AuthFlow, AuthenticationError, Authorization, Cors, CredentialTransportPolicy, Csrf, CsrfError, CsrfTokenManager, ExternalJwtAuthenticator, Firewall, InsecureTransportError, InvalidTargetError, JwtAuthenticator, JwtKeystore, KeycloakAdminClient, KeycloakAdminError, KeycloakService, LoginThrottler, MemoryAuditStore, MemoryTokenStore, MemoryTotpSecretStore, MemoryWebAuthnCredentialStore, MemoryWebhookStore, OAuth2Client, OAuth2RequestError, OAuth2Service, OAuth2Tokens, RemoteJwtVerifier, RoleHierarchyWalker, RoleVoter, ScopeVoter, SecuredArea, SecurityHeaders, SessionAuthenticator, SsrfError, TOKEN_DEFAULT_ORDER, TOKEN_FACETS, TOKEN_SORTABLE_FIELDS, ThrottledError, TokenService, TotpService, UnverifiableTokenError, UserPasswordAuthenticator, UserToken, VoterVote, WEBHOOK_DEFAULT_ORDER, WEBHOOK_SORTABLE_FIELDS, WebAuthnService, WebhookService, amrForFactor, assertPublicUrl, base32Decode, beginTotpEnrollment, confirmTotpEnrollment, createCodeChallenge, createDiscoveredOidcProvider, createGithubProvider, createOidcProvider, createSecurityAdminApi, decryptSecret, security_default as default, defineSecurityConfig, deriveKey, deriveTotpKey, diagnoseOAuthProvider, disableTotp, discoverAuthorizationServer, encryptSecret, generateApiKey, generateCodeVerifier, generateEphemeralKey, generateState, getAuditStoreFactory, getAuthenticatorFactory, getOAuthProviderFactory, getTokenStoreFactory, getTotpStoreFactory, getWebAuthnStoreFactory, getWebhookStoreFactory, hashApiKey, isBlockedAddress, keycloakAdminTarget, keycloakLoginThemeCheck, listAuditStores, listAuthenticatorFactories, listOAuthProviders, listTokenStores, listTotpStores, listVoterFactories, listWebAuthnStores, listWebhookStores, looksLikeApiKey, matchesTokenStatus, oauthProviderRequiresIssuer, parseApiKey, parseAuditQuery, peekIssuer, readAuditContext, readKeycloakLoginTheme, readLogoutTokenClaims, recordAudit, registerAuditStore, registerAuthenticatorFactory, registerOAuthProvider, registerSecurityAdminApi, registerTokenStore, registerTotpStore, registerVoterFactory, registerWebAuthnStore, registerWebhookStore, requestClientCredentialsToken, resolveJwtRuntime, securityConfigJsonSchema, shouldEnforceCredentialTransport, tokenStatusCriteria, tokenStatusOf, totpCode, totpStatus, verifyTotpLogin };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Command } from "nodefony";
|
|
2
|
+
//#region nodefony/command/security-oauth-doctor.ts
|
|
3
|
+
const options = {
|
|
4
|
+
helpGroup: "COMPTES ET SECRETS",
|
|
5
|
+
showBanner: false,
|
|
6
|
+
kernelEvent: "onReady",
|
|
7
|
+
quietBoot: true
|
|
8
|
+
};
|
|
9
|
+
const GREEN = "\x1B[32m";
|
|
10
|
+
const RED = "\x1B[31m";
|
|
11
|
+
const YELLOW = "\x1B[33m";
|
|
12
|
+
const DIM = "\x1B[2m";
|
|
13
|
+
const RESET = "\x1B[0m";
|
|
14
|
+
/** Codes de sortie (sysexits). */
|
|
15
|
+
const EXIT_FAILED = 1;
|
|
16
|
+
const EXIT_CONFIG = 78;
|
|
17
|
+
const LABELS = {
|
|
18
|
+
discovery: "découverte",
|
|
19
|
+
authorization: "URL de retour",
|
|
20
|
+
token: "secret",
|
|
21
|
+
theme: "thème"
|
|
22
|
+
};
|
|
23
|
+
function mark(check) {
|
|
24
|
+
switch (check.status) {
|
|
25
|
+
case "ok": return `${GREEN}✓${RESET}`;
|
|
26
|
+
case "failed": return `${RED}✗${RESET}`;
|
|
27
|
+
case "inconclusive": return `${YELLOW}?${RESET}`;
|
|
28
|
+
case "skipped": return `${DIM}–${RESET}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `nodefony security:oauth:doctor` — dit POURQUOI un fournisseur OAuth refuse
|
|
33
|
+
* l'application, sans qu'un humain ait à se connecter.
|
|
34
|
+
*
|
|
35
|
+
* Trois verdicts distincts là où le journal ne montre qu'un 401 : l'émetteur
|
|
36
|
+
* se déclare autrement que la configuration ne l'écrit, l'URL de retour n'est
|
|
37
|
+
* pas enregistrée, le secret est faux. La même analyse alimente
|
|
38
|
+
* `nodefony doctor --live` (famille « Fournisseurs OAuth ») et l'endpoint
|
|
39
|
+
* d'administration `security/oauth/diagnosis` : une seule implémentation, celle
|
|
40
|
+
* du service `oauth2`.
|
|
41
|
+
*
|
|
42
|
+
* Valable pour tout fournisseur : les sondes passent par le fournisseur que le
|
|
43
|
+
* login construit, Keycloak n'y a rien de particulier.
|
|
44
|
+
*/
|
|
45
|
+
var SecurityOAuthDoctor = class extends Command {
|
|
46
|
+
constructor(cli) {
|
|
47
|
+
super("security:oauth:doctor", "diagnostique le branchement des fournisseurs OAuth", cli, options);
|
|
48
|
+
this.addOption("-p, --provider <nom>", "un seul fournisseur de oauth2.providers");
|
|
49
|
+
this.addOption("-j, --json", "sortie JSON (scripts/CI)");
|
|
50
|
+
}
|
|
51
|
+
async generate(opts) {
|
|
52
|
+
const oauth = this.kernel?.container?.get("oauth2");
|
|
53
|
+
const diagnoses = oauth?.isEnabled() === true ? await oauth.diagnose(opts.provider) : [];
|
|
54
|
+
if (opts.json) process.stdout.write(`${JSON.stringify(diagnoses, null, 2)}\n`);
|
|
55
|
+
else if (diagnoses.length === 0) process.stderr.write("aucun fournisseur OAuth à diagnostiquer — `security.oauth2` est désactivé ou ne déclare aucun fournisseur\n");
|
|
56
|
+
else for (const diagnosis of diagnoses) {
|
|
57
|
+
process.stdout.write(`${diagnosis.ok ? GREEN : RED}${diagnosis.provider}${RESET}\n`);
|
|
58
|
+
for (const check of diagnosis.checks) process.stdout.write(` ${mark(check)} ${LABELS[check.name].padEnd(14)}${check.message}\n`);
|
|
59
|
+
}
|
|
60
|
+
if (diagnoses.length === 0) process.exitCode = EXIT_CONFIG;
|
|
61
|
+
else if (diagnoses.some((d) => !d.ok)) process.exitCode = EXIT_FAILED;
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
export { SecurityOAuthDoctor as default };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readIfPresentSync } from "../src/token/secretFile.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { generateKeySet } from "../src/token/JwtKeystore.js";
|
|
3
|
+
import { Command, ENV_FILE, diskManifestReader, manifestFileWith, readManifestCode } from "nodefony";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { appendFileSync } from "node:fs";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
6
7
|
import { spawnSync } from "node:child_process";
|
|
7
8
|
//#region nodefony/command/security-secrets.ts
|
|
8
9
|
const options = {
|
|
@@ -45,11 +46,11 @@ const ROLES = {
|
|
|
45
46
|
},
|
|
46
47
|
NF_CSRF_SECRET: {
|
|
47
48
|
protected: "signe les jetons anti-rejeu des mutations (`@CsrfProtect`)",
|
|
48
|
-
without: "en
|
|
49
|
+
without: "démarrage REFUSÉ en production (code 78) — une valeur tirée au hasard différerait d'un exemplaire à l'autre"
|
|
49
50
|
},
|
|
50
|
-
|
|
51
|
-
protected: "signe les JWT (
|
|
52
|
-
without: "
|
|
51
|
+
NF_JWT_KEYSET: {
|
|
52
|
+
protected: "signe les jetons JWT (paire Ed25519) — la MÊME pour tous les pods et workers",
|
|
53
|
+
without: "démarrage REFUSÉ en production dès que l'application émet des jetons — sinon 401 au hasard derrière plusieurs exemplaires, et tous les jetons perdus au redémarrage"
|
|
53
54
|
}
|
|
54
55
|
};
|
|
55
56
|
/**
|
|
@@ -71,22 +72,24 @@ var SecuritySecrets = class extends Command {
|
|
|
71
72
|
super("security:secrets", "engendre les clés de chiffrement du module security", cli, options);
|
|
72
73
|
this.addOption("-j, --json", "sortie JSON (scripts/CI)");
|
|
73
74
|
this.addOption("-w, --write", "écrit les clés manquantes dans le .env du projet (jamais de remplacement)");
|
|
75
|
+
this.addOption("-e, --env", "les 4 secrets en lignes CLÉ=valeur (docker run --env-file, gestionnaire de secrets)");
|
|
76
|
+
this.addOption("-k, --jwt-keyset", "génère la valeur de NF_JWT_KEYSET (une ligne JSON, à ranger dans le gestionnaire de secrets)");
|
|
74
77
|
}
|
|
75
78
|
/** Racine du projet (kernel booté) — repli cwd. */
|
|
76
79
|
#root() {
|
|
77
80
|
return this.kernel?.path ?? process.cwd();
|
|
78
81
|
}
|
|
79
82
|
/**
|
|
80
|
-
* `true` si `.env
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
+
* `true` si `.env` est SUIVI par git — y écrire des secrets les mènerait au
|
|
84
|
+
* commit (`.env` doit être gitignoré). Best-effort : git absent / hors repo →
|
|
85
|
+
* `false` (on écrit).
|
|
83
86
|
*/
|
|
84
87
|
#dotenvTracked() {
|
|
85
88
|
try {
|
|
86
89
|
return spawnSync("git", [
|
|
87
90
|
"ls-files",
|
|
88
91
|
"--error-unmatch",
|
|
89
|
-
|
|
92
|
+
ENV_FILE
|
|
90
93
|
], {
|
|
91
94
|
cwd: this.#root(),
|
|
92
95
|
stdio: "ignore"
|
|
@@ -104,15 +107,24 @@ var SecuritySecrets = class extends Command {
|
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
async generate(opts) {
|
|
110
|
+
if (opts.jwtKeyset) {
|
|
111
|
+
process.stdout.write(await generateKeySet() + "\n");
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
107
114
|
const gen = () => randomBytes(32).toString("base64");
|
|
108
115
|
const secrets = {};
|
|
109
116
|
for (const k of KEYS) secrets[k] = gen();
|
|
110
117
|
if (opts.json) {
|
|
118
|
+
secrets["NF_JWT_KEYSET"] = await generateKeySet();
|
|
111
119
|
process.stdout.write(JSON.stringify(secrets, null, 2) + "\n");
|
|
112
120
|
return this;
|
|
113
121
|
}
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
if (opts.env) {
|
|
123
|
+
secrets["NF_JWT_KEYSET"] = await generateKeySet();
|
|
124
|
+
process.stdout.write(Object.entries(secrets).map(([k, v]) => `${k}=${v}`).join("\n") + "\n");
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
const dotenv = this.#read(ENV_FILE);
|
|
116
128
|
const envTs = this.#read("env.ts");
|
|
117
129
|
const cfgTs = readManifestCode(this.#root(), diskManifestReader);
|
|
118
130
|
const cfgFile = manifestFileWith(this.#root(), diskManifestReader, /satisfies\s+ISecurityConfigInput/);
|
|
@@ -130,18 +142,18 @@ var SecuritySecrets = class extends Command {
|
|
|
130
142
|
process.stdout.write(s);
|
|
131
143
|
};
|
|
132
144
|
w(`\n${BOLD}🔐 Secrets du module security${RESET} ${DIM}— 4 secrets, 3 fichiers${RESET}\n\n`);
|
|
133
|
-
for (const [name, role] of Object.entries(ROLES)) w(` ${(name === "
|
|
134
|
-
const tokenStillThere = /^\s*NF_MCP_TOKEN\s*=/m.test(
|
|
145
|
+
for (const [name, role] of Object.entries(ROLES)) w(` ${(name === "NF_JWT_KEYSET" ? cfgTs.includes(name) : new RegExp(`^\\s*${name}\\s*=`, "m").test(dotenv)) ? "\x1B[32m✓" : "\x1B[33m○"}${RESET} ${BOLD}${name.padEnd(16)}${RESET}${DIM}${role.protected}${RESET}\n ${DIM}sans → ${role.without}${RESET}\n`);
|
|
146
|
+
const tokenStillThere = /^\s*NF_MCP_TOKEN\s*=/m.test(dotenv);
|
|
135
147
|
w(`\n${DIM} · NF_MCP_TOKEN n'est PAS un secret de cette application, et n'a rien à\n faire dans cette liste : c'est un jeton qu'elle ÉMET, présenté par un\n agent pour entrer. Aucun code d'ici ne le lit. Il se pose chez l'agent\n qui le porte — nodefony security:token --write.${RESET}\n`);
|
|
136
|
-
if (tokenStillThere) w(`${YELLOW} ⚠ une ligne NF_MCP_TOKEN traîne encore dans .env
|
|
148
|
+
if (tokenStillThere) w(`${YELLOW} ⚠ une ligne NF_MCP_TOKEN traîne encore dans .env — rien ne la lit,\n tu peux la retirer.${RESET}\n`);
|
|
137
149
|
w(`\n${YELLOW}⚠ rien ne se tape dans le terminal : chaque bloc se colle dans le fichier indiqué.${RESET}\n\n`);
|
|
138
|
-
w(`${BOLD}1. Fichier ${CYAN}.env
|
|
150
|
+
w(`${BOLD}1. Fichier ${CYAN}.env${RESET}${BOLD} — les valeurs${RESET} ${DIM}(valeurs du poste, gitignoré ; la notice est .env.example)${RESET}\n`);
|
|
139
151
|
if (missingInDotenv.length === 0) w(` ${GREEN}✓ les 3 clés y sont déjà${RESET} ${DIM}(rien à faire — rotation = remplacer la valeur à la main)${RESET}\n\n`);
|
|
140
|
-
else if (opts.write && this.#dotenvTracked()) w(` ${YELLOW}⚠ .env
|
|
152
|
+
else if (opts.write && this.#dotenvTracked()) w(` ${YELLOW}⚠ .env est suivi par git — je n'y écris PAS de secrets.${RESET}\n ${DIM}Ajoute \`.env\` au .gitignore (et \`git rm --cached .env\`), puis relance --write ;\n ou colle les lignes ci-dessous à la main :${RESET}\n\n` + missingInDotenv.map((k) => ` ${k}=${secrets[k]}`).join("\n") + `\n\n`);
|
|
141
153
|
else if (opts.write) {
|
|
142
|
-
const block = (
|
|
143
|
-
appendFileSync(path.resolve(this.#root(),
|
|
144
|
-
w(` ${GREEN}✓ écrit dans .env
|
|
154
|
+
const block = (dotenv && !dotenv.endsWith("\n") ? "\n" : "") + `# clés security — générées par \`nodefony security:secrets\`\n` + missingInDotenv.map((k) => `${k}=${secrets[k]}`).join("\n") + "\n";
|
|
155
|
+
appendFileSync(path.resolve(this.#root(), ENV_FILE), block);
|
|
156
|
+
w(` ${GREEN}✓ écrit dans .env${RESET} ${DIM}(${missingInDotenv.join(", ")} — les clés déjà présentes n'ont pas été touchées)${RESET}\n\n`);
|
|
145
157
|
} else w(` colle ces lignes ${DIM}(ou relance avec ${RESET}${CYAN}--write${RESET}${DIM} pour que je les écrive)${RESET} :\n\n` + missingInDotenv.map((k) => ` ${k}=${secrets[k]}`).join("\n") + `\n ${DIM}(en prod : Secret k8s / vault — jamais en git)${RESET}\n\n`);
|
|
146
158
|
w(`${BOLD}2. Fichier ${CYAN}env.ts${RESET}${BOLD} — la déclaration typée${RESET} ${DIM}(env.ts est le seul lecteur de process.env)${RESET}\n`);
|
|
147
159
|
if (missingInEnvTs.length === 0) w(` ${GREEN}✓ déjà déclarées${RESET}\n\n`);
|
|
@@ -150,11 +162,16 @@ var SecuritySecrets = class extends Command {
|
|
|
150
162
|
if (missingInCfg.length === 0) w(` ${GREEN}✓ déjà câblées${RESET}\n\n`);
|
|
151
163
|
else if (cfgIsFragment) w(` complète le descripteur ${CYAN}securityConfig${RESET} :\n\n` + missingInCfg.map((k) => WIRING[k]).join("\n") + `\n\n`);
|
|
152
164
|
else w(" complète l'entrée security du manifeste modules :\n\n use(\"@nodefony/security\", {\n" + missingInCfg.map((k) => WIRING[k]).join("\n") + `\n }),\n\n`);
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
const jwtDeclared = envTs.includes("NF_JWT_KEYSET");
|
|
166
|
+
const jwtWired = cfgTs.includes("NF_JWT_KEYSET");
|
|
167
|
+
w(`${BOLD}4. La clé de SIGNATURE des jetons${RESET} ${DIM}(NF_JWT_KEYSET — production seulement)${RESET}\n`);
|
|
168
|
+
if (jwtDeclared && jwtWired) w(` ${GREEN}✓ déclarée dans env.ts et câblée dans ${cfgRel}${RESET}\n`);
|
|
169
|
+
else {
|
|
170
|
+
if (!jwtDeclared) w(` ${CYAN}env.ts${RESET}, dans le defineEnv({ … }) :\n\n NF_JWT_KEYSET: envString({ optional: true }),\n\n`);
|
|
171
|
+
if (!jwtWired) w(` ${CYAN}${cfgRel}${RESET}, entrée security :\n\n jwt: {\n keystore: {\n keySetJson: ctx.env.NF_JWT_KEYSET,\n dir: ctx.isProd ? undefined : "var/keys",\n },\n },\n\n`);
|
|
172
|
+
}
|
|
173
|
+
w(` ${DIM}Développement : rien à poser, la clé vit dans var/keys/.\n Production : UNE valeur pour tous les pods et workers, générée une fois —\n ${RESET}${CYAN}npx nodefony security:secrets --jwt-keyset${RESET}${DIM} — puis rangée dans le\n gestionnaire de secrets et injectée en NF_JWT_KEYSET. Jamais dans .env,\n jamais dans git. STABLE : la changer refuse les jetons en vol ; pour une\n rotation, ajouter la nouvelle clé au jeu, la rendre « active », garder l'ancienne.${RESET}\n\n`);
|
|
174
|
+
w(`${DIM}Pourquoi 3 fichiers ? .env porte la VALEUR (secret du poste, gitignoré ;\nen production, le gestionnaire de secrets) ; env.ts la DÉCLARE\n(catalogue typé, validé au boot) ; ${cfgRel} la CÂBLE au module.\nLes étapes 2 et 3 ne se font qu'une fois — ensuite seule l'étape 1 vit.${RESET}\n\nRelance le serveur : plus aucun warning « clé ÉPHÉMÈRE » au boot.\n\n`);
|
|
158
175
|
return this;
|
|
159
176
|
}
|
|
160
177
|
};
|
|
@@ -38,6 +38,36 @@ function ttlSeconds(raw) {
|
|
|
38
38
|
if (minutes > TTL_MAX_MINUTES) return /* @__PURE__ */ new Error(`--ttl est borné à ${TTL_MAX_MINUTES} minutes (30 jours) — un jeton posé dans un fichier est une clé, et une clé se remplace`);
|
|
39
39
|
return minutes * 60;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Durée d'un jeton POSÉ dans un agent : 8 h, une journée de travail.
|
|
43
|
+
*
|
|
44
|
+
* L'en-tête d'un agent est figé, rien ne le rafraîchit : le défaut de la
|
|
45
|
+
* configuration (15 min, taillé pour un client d'API qui renouvelle son jeton)
|
|
46
|
+
* y produisait un 401 au bout d'un quart d'heure, qui n'accusait pas
|
|
47
|
+
* l'expiration. Huit heures évitent la relance dans la journée et bornent une
|
|
48
|
+
* fuite à quelques heures. Le défaut global (`jwt.accessTtlS`) ne bouge pas :
|
|
49
|
+
* le relever allongerait TOUS les jetons de l'application.
|
|
50
|
+
*/
|
|
51
|
+
const AGENT_TTL_MINUTES = 480;
|
|
52
|
+
/**
|
|
53
|
+
* Durée demandée à l'émetteur : `--ttl` explicite, sinon 8 h quand le jeton
|
|
54
|
+
* part chez les agents, sinon rien — la configuration décide.
|
|
55
|
+
*
|
|
56
|
+
* @param raw - la valeur de `--ttl` telle que tapée, ou rien
|
|
57
|
+
* @param write - le jeton sera posé dans la configuration des agents
|
|
58
|
+
* @returns les secondes, `undefined` pour le défaut de configuration, une `Error` sinon
|
|
59
|
+
*/
|
|
60
|
+
function tokenTtlSeconds(raw, write) {
|
|
61
|
+
if (raw !== void 0) return ttlSeconds(raw);
|
|
62
|
+
return write ? AGENT_TTL_MINUTES * 60 : void 0;
|
|
63
|
+
}
|
|
64
|
+
/** « 8 h », « 1 h 30 », « 15 min » — la durée telle qu'on la dit. */
|
|
65
|
+
function humanDuration(seconds) {
|
|
66
|
+
const minutes = Math.round(seconds / 60);
|
|
67
|
+
if (minutes < 60) return `${minutes} min`;
|
|
68
|
+
const rest = minutes % 60;
|
|
69
|
+
return rest === 0 ? `${minutes / 60} h` : `${Math.floor(minutes / 60)} h ${rest}`;
|
|
70
|
+
}
|
|
41
71
|
const GREEN = "\x1B[32m";
|
|
42
72
|
const YELLOW = "\x1B[33m";
|
|
43
73
|
const DIM = "\x1B[2m";
|
|
@@ -69,7 +99,7 @@ var SecurityToken = class extends Command {
|
|
|
69
99
|
this.addOption("-s, --scope <scopes>", `scopes demandés, séparés par des espaces (défaut : « ${ADMIN_SCOPE_READ} » ; ajouter « ${ADMIN_SCOPE_WRITE} » pour les mutations)`);
|
|
70
100
|
this.addOption("-r, --resource <uri>", "audience visée (défaut : la porte MCP de cette application)");
|
|
71
101
|
this.addOption("-a, --agent <noms>", "agents à servir, séparés par des virgules (défaut : ceux détectés ; « none » pour aucun)");
|
|
72
|
-
this.addOption("-t, --ttl <duree>", "durée de validité, en minutes (défaut : celle de la config, 15 min)");
|
|
102
|
+
this.addOption("-t, --ttl <duree>", "durée de validité, en minutes (défaut : 8 h avec --write, sinon celle de la config, 15 min)");
|
|
73
103
|
this.addOption("-w, --write", `pose ${MCP_TOKEN_ENV} dans la configuration des agents présents`);
|
|
74
104
|
this.addOption("-j, --json", "sortie JSON (scripts/CI)");
|
|
75
105
|
}
|
|
@@ -239,7 +269,15 @@ var SecurityToken = class extends Command {
|
|
|
239
269
|
const resource = opts.resource ?? this.#defaultResource();
|
|
240
270
|
const requestedScopes = (opts.scope ?? "").split(/\s+/u).filter(Boolean);
|
|
241
271
|
const scopes = requestedScopes.length > 0 ? requestedScopes : [ADMIN_SCOPE_READ];
|
|
242
|
-
|
|
272
|
+
let write = opts.write === true;
|
|
273
|
+
if (!write && !opts.json && process.stdin.isTTY) {
|
|
274
|
+
await this.loadPrompts();
|
|
275
|
+
write = await this.prompts.confirm({
|
|
276
|
+
message: `Poser ${MCP_TOKEN_ENV} dans la configuration des agents présents ?`,
|
|
277
|
+
default: true
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const ttlS = tokenTtlSeconds(opts.ttl, write);
|
|
243
281
|
if (ttlS instanceof Error) {
|
|
244
282
|
this.log(ttlS.message, "ERROR");
|
|
245
283
|
process.exitCode = 1;
|
|
@@ -271,30 +309,23 @@ var SecurityToken = class extends Command {
|
|
|
271
309
|
const token = issued.access_token;
|
|
272
310
|
const accordes = issued.scope.split(/\s+/u).filter(Boolean);
|
|
273
311
|
const nonAccordes = scopes.filter((s) => !accordes.includes(s));
|
|
312
|
+
const expiresAt = new Date(Date.now() + issued.expires_in * 1e3);
|
|
274
313
|
if (opts.json) {
|
|
275
314
|
process.stdout.write(`${JSON.stringify({
|
|
276
315
|
access_token: token,
|
|
277
316
|
resource,
|
|
278
317
|
scopes: accordes,
|
|
279
318
|
requested: scopes,
|
|
280
|
-
expires_in: issued.expires_in
|
|
319
|
+
expires_in: issued.expires_in,
|
|
320
|
+
expires_at: expiresAt.toISOString()
|
|
281
321
|
}, null, 2)}\n`);
|
|
282
322
|
return this;
|
|
283
323
|
}
|
|
284
324
|
const w = (s) => {
|
|
285
325
|
process.stdout.write(s);
|
|
286
326
|
};
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
await this.loadPrompts();
|
|
290
|
-
write = await this.prompts.confirm({
|
|
291
|
-
message: `Poser ${MCP_TOKEN_ENV} dans la configuration des agents présents ?`,
|
|
292
|
-
default: true
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
if (this.#ephemeralKey()) w(`\n${YELLOW}⚠ Clé de signature ÉPHÉMÈRE — ce jeton sera REFUSÉ.${RESET}\n${DIM} Cette application n'a pas de clé persistante : chaque process en génère\n une au démarrage. Le jeton ci-dessous n'est vérifiable que par le process\n qui vient de le signer — pas par le serveur en marche, qui a la sienne.\n → déclare une source de clés dans nodefony.config.ts :\n use("@nodefony/security", { jwt: { keystore: { dir: "var/keys" } } })\n ou, en production, keySetJson depuis l'environnement.${RESET}\n`);
|
|
296
|
-
const minutes = Math.round(issued.expires_in / 60);
|
|
297
|
-
w(`\n${BOLD}🔑 Jeton d'accès${RESET} ${DIM}— compte ${identifier}, audience ${resource}${RESET}\n${DIM} valable ${minutes} min${accordes.length ? `, scopes : ${accordes.join(" ")}` : ", aucun scope"}${RESET}\n\n`);
|
|
327
|
+
if (this.#ephemeralKey()) w(`\n${YELLOW}⚠ Clé de signature ÉPHÉMÈRE — ce jeton sera REFUSÉ.${RESET}\n${DIM} Cette application n'a pas de clé persistante : chaque process en génère\n une au démarrage. Le jeton ci-dessous n'est vérifiable que par le process\n qui vient de le signer — pas par le serveur en marche, qui a la sienne.\n → déclare une source de clés dans nodefony.config.ts :\n use("@nodefony/security", { jwt: { keystore: { dir: "var/keys" } } })\n ou, en production, keySetJson depuis l'environnement (NF_JWT_KEYSET —\n npx nodefony security:secrets --jwt-keyset).${RESET}\n`);
|
|
328
|
+
w(`\n${BOLD}🔑 Jeton d'accès${RESET} ${DIM}— compte ${identifier}, audience ${resource}${RESET}\n${DIM} valable ${humanDuration(issued.expires_in)}, expire le ${expiresAt.toLocaleString()}${accordes.length ? `, scopes : ${accordes.join(" ")}` : ", aucun scope"}${RESET}\n\n`);
|
|
298
329
|
if (nonAccordes.length > 0) w(`${DIM} ⚠️ non accordé(s) : ${nonAccordes.join(" ")} — réservé(s) au rôle d'administration, que le compte « ${identifier} » ne porte pas.${RESET}\n\n`);
|
|
299
330
|
if (write) {
|
|
300
331
|
const requested = requestedAgents(opts.agent);
|
|
@@ -333,4 +364,4 @@ var SecurityToken = class extends Command {
|
|
|
333
364
|
}
|
|
334
365
|
};
|
|
335
366
|
//#endregion
|
|
336
|
-
export { SecurityToken as default, ttlSeconds };
|
|
367
|
+
export { SecurityToken as default, tokenTtlSeconds, ttlSeconds };
|
|
@@ -30,9 +30,10 @@ const ADMIN_ROLE = "ROLE_NODEFONY_ADMIN";
|
|
|
30
30
|
* seul recours est une écriture directe en base. Le même garde-fou existe côté
|
|
31
31
|
* data plane (`UserAdminApi`) — ici il est constaté, pas supposé.
|
|
32
32
|
*
|
|
33
|
-
* La
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* La suppression passe par `users.delete`, qui émet `onUserRevoked`
|
|
34
|
+
* (`reason: "deleted"`) et ATTEND la cascade de `@nodefony/security` : sessions
|
|
35
|
+
* et jetons éjectés, secret TOTP et passkeys retirés. La commande n'émet rien
|
|
36
|
+
* elle-même — le service est le seul émetteur du cycle de vie d'un compte.
|
|
36
37
|
*/
|
|
37
38
|
var SecurityUserDelete = class extends Command {
|
|
38
39
|
constructor(cli) {
|
|
@@ -95,7 +96,7 @@ var SecurityUserDelete = class extends Command {
|
|
|
95
96
|
process.exitCode = 1;
|
|
96
97
|
return this;
|
|
97
98
|
}
|
|
98
|
-
w(`${GREEN}✓ compte supprimé${RESET} — ${BOLD}${identifier}${RESET}\n${DIM} ses sessions
|
|
99
|
+
w(`${GREEN}✓ compte supprimé${RESET} — ${BOLD}${identifier}${RESET}\n${DIM} ses sessions, ses jetons et ses facteurs (2FA, passkeys) sont retirés.${RESET}\n\n`);
|
|
99
100
|
return this;
|
|
100
101
|
}
|
|
101
102
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { AUTH_LOGIN_PATH, CONSOLE_DATA_RUN_PROFILE, Command, askPasswordMasked } from "nodefony";
|
|
2
|
-
import { USER_REVOKED_EVENT } from "@nodefony/user";
|
|
3
2
|
//#region nodefony/command/security-user-password.ts
|
|
4
3
|
const options = {
|
|
5
4
|
runProfile: CONSOLE_DATA_RUN_PROFILE,
|
|
@@ -34,10 +33,10 @@ const RESET = "\x1B[0m";
|
|
|
34
33
|
*
|
|
35
34
|
* 🔒 **Les sessions et les jetons du compte sont RÉVOQUÉS**, et ce n'est pas une
|
|
36
35
|
* option : on change un mot de passe parce qu'il est compromis ou perdu, et
|
|
37
|
-
* laisser vivre les sessions ouvertes laisserait l'accès à qui l'a volé.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
36
|
+
* laisser vivre les sessions ouvertes laisserait l'accès à qui l'a volé.
|
|
37
|
+
* `UserService.changePassword` émet la révocation lui-même — la même que pour
|
|
38
|
+
* le plan d'administration et la page « mon mot de passe » : un seul émetteur,
|
|
39
|
+
* aucun transport ne peut l'oublier.
|
|
41
40
|
*/
|
|
42
41
|
var SecurityUserPassword = class extends Command {
|
|
43
42
|
constructor(cli) {
|
|
@@ -100,12 +99,6 @@ var SecurityUserPassword = class extends Command {
|
|
|
100
99
|
process.exitCode = 1;
|
|
101
100
|
return this;
|
|
102
101
|
}
|
|
103
|
-
this.kernel?.fire(USER_REVOKED_EVENT, {
|
|
104
|
-
id: user.id,
|
|
105
|
-
identifier: user.identifier,
|
|
106
|
-
tenantId: null,
|
|
107
|
-
reason: "password_changed"
|
|
108
|
-
});
|
|
109
102
|
process.stdout.write(`\n${GREEN}✓ mot de passe changé${RESET} — ${BOLD}${user.identifier}${RESET}\n${DIM} ses sessions et ses jetons sont révoqués : il devra se reconnecter.${RESET}\n${DIM} connexion : POST ${AUTH_LOGIN_PATH}${RESET}\n` + (opts.password ? ` ${YELLOW}⚠ mot de passe passé en argument — pense à purger l'historique shell${RESET}\n` : "") + "\n");
|
|
110
103
|
return this;
|
|
111
104
|
}
|