@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/keycloak.md
CHANGED
|
@@ -36,8 +36,8 @@ source: "src/packages/@nodefony/security/docs/keycloak.md"
|
|
|
36
36
|
> du realm suffit à le décrire. Le travail se passe surtout dans Keycloak : créer le realm, déclarer
|
|
37
37
|
> un client **confidentiel**, enregistrer l'URL de retour au caractère près. Cette page fait le
|
|
38
38
|
> chemin entier, puis montre comment le jeton Keycloak de la même personne ouvre aussi ton API, sur
|
|
39
|
-
> le **même compte**. Ancré sur le fournisseur `createDiscoveredOidcProvider()` (`oidc.ts
|
|
40
|
-
> sur `OAuth2Service` (`oauth2.ts
|
|
39
|
+
> le **même compte**. Ancré sur le fournisseur `createDiscoveredOidcProvider()` (`oidc.ts#createDiscoveredOidcProvider`) et
|
|
40
|
+
> sur `OAuth2Service` (`oauth2.ts#OAuth2Service`).
|
|
41
41
|
|
|
42
42
|
📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **Keycloak**
|
|
43
43
|
|
|
@@ -98,19 +98,19 @@ Pour ton application, cela bloque trois risques concrets :
|
|
|
98
98
|
|
|
99
99
|
**Aucun code propre à Keycloak.** Un serveur OpenID Connect publie lui-même ses points d'entrée ;
|
|
100
100
|
le fournisseur `keycloak` les **découvre** à partir de l'émetteur, et c'est tout ce qu'il sait faire
|
|
101
|
-
(`createDiscoveredOidcProvider()`, `oidc.ts
|
|
101
|
+
(`createDiscoveredOidcProvider()`, `oidc.ts#createDiscoveredOidcProvider`). Changer de realm, c'est changer une URL.
|
|
102
102
|
|
|
103
103
|
**Une configuration fausse arrête le démarrage.** Le fournisseur `keycloak` est enregistré avec
|
|
104
|
-
`requiresIssuer: true` (`oauthProviderRegistry.ts
|
|
104
|
+
`requiresIssuer: true` (`oauthProviderRegistry.ts#withKeycloakThemeHint`). Au boot, un émetteur absent, ou qui n'est
|
|
105
105
|
pas une URL `https` sans requête ni fragment, lève une erreur qui nomme la clé
|
|
106
|
-
(`checkProviderIssuer()`, `oauth2.ts
|
|
106
|
+
(`checkProviderIssuer()`, `oauth2.ts#checkProviderIssuer`). Un Keycloak **éteint**, lui, ne bloque rien : le bouton
|
|
107
107
|
disparaît de l'écran de connexion et revient tout seul quand le realm répond à nouveau
|
|
108
|
-
(`#isReachable()`, `oauth2.ts
|
|
108
|
+
(`#isReachable()`, `oauth2.ts#OAuth2Service.#isReachable`).
|
|
109
109
|
|
|
110
110
|
**L'identité est la paire `(keycloak, sub)`, jamais l'email.** Au premier login, l'application crée
|
|
111
|
-
un compte local lié à cette paire (`UserService.provisionOAuthUser()`, `UserService.ts
|
|
111
|
+
un compte local lié à cette paire (`UserService.provisionOAuthUser()`, `UserService.ts#UserService.provisionOAuthUser`). Un
|
|
112
112
|
compte local qui porte déjà le même identifiant sans ce lien n'est **jamais** rattaché en silence :
|
|
113
|
-
la connexion est refusée (`UserService.ts
|
|
113
|
+
la connexion est refusée (`UserService.ts#UserService.changePassword`).
|
|
114
114
|
|
|
115
115
|
**Keycloak authentifie, l'application autorise.** Les rôles du realm ne sont pas lus : le compte
|
|
116
116
|
reçoit `defaultRoles` à sa création, puis la base locale fait foi. Une élévation se fait dans
|
|
@@ -263,7 +263,7 @@ lui, en a un de test — Mailpit (`--profile keycloak`, interface `http://localh
|
|
|
263
263
|
développement de l'application, et l'application **reconnaît d'office l'autorité qui l'a
|
|
264
264
|
signé** : en développement, le service de certificats l'ajoute aux autorités de confiance du
|
|
265
265
|
processus dès le démarrage, commandes en ligne comprises (`addDefaultCaCertificate()`,
|
|
266
|
-
`certificates.ts
|
|
266
|
+
`certificates.ts#addDefaultCaCertificate`). La variable `NODE_EXTRA_CA_CERTS` n'est plus nécessaire.
|
|
267
267
|
|
|
268
268
|
Le realm généré porte aussi, comme celui du dépôt, un client **machine** `<app>-machine` (compte de
|
|
269
269
|
service, `client_credentials`). Ses jetons portent l'audience de la zone `partner` que le gabarit
|
|
@@ -397,10 +397,42 @@ complète : sinon l'émetteur change selon qui demande, et l'application refuse
|
|
|
397
397
|
Les trois valeurs entrent par `env.ts`, seul lecteur des variables d'environnement. Le fournisseur
|
|
398
398
|
n'est déclaré que si les trois sont présentes : pas de bouton mort quand une variable manque.
|
|
399
399
|
|
|
400
|
-
```
|
|
400
|
+
```ts ignore
|
|
401
|
+
// nodefony/config/security.ts — le fragment du module, importé par le manifeste
|
|
402
|
+
import type { ConfigContext } from "nodefony";
|
|
403
|
+
import type { ISecurityConfigInput } from "@nodefony/security";
|
|
404
|
+
import type { env } from "../../env";
|
|
405
|
+
|
|
406
|
+
export const securityConfig = (ctx: ConfigContext<typeof env>) =>
|
|
407
|
+
({
|
|
408
|
+
oauth2: {
|
|
409
|
+
// Rôles posés à la CRÉATION du compte local ; ceux du realm ne sont pas lus.
|
|
410
|
+
defaultRoles: ["ROLE_USER"],
|
|
411
|
+
providers: {
|
|
412
|
+
...(ctx.env.NF_KEYCLOAK_ISSUER &&
|
|
413
|
+
ctx.env.NF_KEYCLOAK_CLIENT_ID &&
|
|
414
|
+
ctx.env.NF_KEYCLOAK_CLIENT_SECRET
|
|
415
|
+
? {
|
|
416
|
+
keycloak: {
|
|
417
|
+
issuer: ctx.env.NF_KEYCLOAK_ISSUER,
|
|
418
|
+
clientId: ctx.env.NF_KEYCLOAK_CLIENT_ID,
|
|
419
|
+
clientSecret: ctx.env.NF_KEYCLOAK_CLIENT_SECRET,
|
|
420
|
+
redirectUri: `${ctx.env.NF_OAUTH_REDIRECT_BASE}/nodefony/security/api/oauth2/keycloak/callback`,
|
|
421
|
+
// Libellé du bouton ; omis, il vaut « Keycloak ».
|
|
422
|
+
label: "Compte entreprise",
|
|
423
|
+
},
|
|
424
|
+
}
|
|
425
|
+
: {}),
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
}) satisfies ISecurityConfigInput;
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
```typescript ignore
|
|
401
432
|
// env.ts — SEUL lecteur de process.env (catalogue typé, validé au boot).
|
|
402
433
|
// nodefony.config.ts — `ctx.env` EST ce catalogue (typé par le paramètre générique).
|
|
403
434
|
import { defineConfig, defineEnv, envString, use } from "nodefony";
|
|
435
|
+
import { securityConfig } from "./nodefony/config/security";
|
|
404
436
|
|
|
405
437
|
export const env = defineEnv({
|
|
406
438
|
// Émetteur = URL du realm, en https (la découverte et le contrôle de `iss` en partent).
|
|
@@ -415,28 +447,7 @@ export default defineConfig<typeof env>((ctx) => ({
|
|
|
415
447
|
modules: [
|
|
416
448
|
"@nodefony/http",
|
|
417
449
|
"@nodefony/framework",
|
|
418
|
-
use("@nodefony/security",
|
|
419
|
-
oauth2: {
|
|
420
|
-
// Rôles posés à la CRÉATION du compte local ; ceux du realm ne sont pas lus.
|
|
421
|
-
defaultRoles: ["ROLE_USER"],
|
|
422
|
-
providers: {
|
|
423
|
-
...(ctx.env.NF_KEYCLOAK_ISSUER &&
|
|
424
|
-
ctx.env.NF_KEYCLOAK_CLIENT_ID &&
|
|
425
|
-
ctx.env.NF_KEYCLOAK_CLIENT_SECRET
|
|
426
|
-
? {
|
|
427
|
-
keycloak: {
|
|
428
|
-
issuer: ctx.env.NF_KEYCLOAK_ISSUER,
|
|
429
|
-
clientId: ctx.env.NF_KEYCLOAK_CLIENT_ID,
|
|
430
|
-
clientSecret: ctx.env.NF_KEYCLOAK_CLIENT_SECRET,
|
|
431
|
-
redirectUri: `${ctx.env.NF_OAUTH_REDIRECT_BASE}/nodefony/security/api/oauth2/keycloak/callback`,
|
|
432
|
-
// Libellé du bouton ; omis, il vaut « Keycloak ».
|
|
433
|
-
label: "Compte entreprise",
|
|
434
|
-
},
|
|
435
|
-
}
|
|
436
|
-
: {}),
|
|
437
|
-
},
|
|
438
|
-
},
|
|
439
|
-
}),
|
|
450
|
+
use("@nodefony/security", securityConfig(ctx)),
|
|
440
451
|
],
|
|
441
452
|
}));
|
|
442
453
|
```
|
|
@@ -455,14 +466,16 @@ par une autorité qu'elle connaît. Trois cas :
|
|
|
455
466
|
`NF_TLS_CA_FILE` alimente la clé `certificates.extraCaFiles` du module `@nodefony/http` : un ou
|
|
456
467
|
plusieurs fichiers PEM, absolus ou relatifs à la racine de l'application, une chaîne entière
|
|
457
468
|
(racine + intermédiaires) pouvant tenir dans un seul fichier. Ils sont ajoutés aux autorités du
|
|
458
|
-
processus au démarrage, avant tout service (`trustCaFile()`, `certificates.ts
|
|
469
|
+
processus au démarrage, avant tout service (`trustCaFile()`, `certificates.ts#trustCaFile`), et servent à
|
|
459
470
|
**tous** ses appels sortants : découverte, point de jeton, jeu de clés. Un fichier introuvable, ou
|
|
460
471
|
sans certificat, **interrompt le démarrage** en nommant le chemin (code 78) — plutôt qu'un
|
|
461
|
-
`fetch failed` au premier clic d'un utilisateur.
|
|
472
|
+
`fetch failed` au premier clic d'un utilisateur. Seule exception : l'autorité de développement pas
|
|
473
|
+
encore engendrée est ignorée en mode `development` (le journal le dit) ; en production, elle doit
|
|
474
|
+
exister — `npx nodefony http:certificates` l'engendre. C'est l'équivalent, porté par la configuration, de
|
|
462
475
|
`NODE_EXTRA_CA_CERTS` : même effet, sans syntaxe de shell à traduire sous Windows.
|
|
463
476
|
|
|
464
477
|
Pourquoi la deuxième ligne du tableau : l'autorité de développement n'est reconnue d'office qu'en
|
|
465
|
-
mode `development` (`Certificate.trustDevelopmentAuthority()`, `certificates.ts
|
|
478
|
+
mode `development` (`Certificate.trustDevelopmentAuthority()`, `certificates.ts#Certificate.trustDevelopmentAuthority`). Lancée
|
|
466
479
|
en production — pour essayer le build sur le poste, par exemple —, l'application ne lui fait plus
|
|
467
480
|
confiance sans qu'on le lui dise, et c'est voulu : une application de production qui adopterait en
|
|
468
481
|
silence une autorité de développement ouvrirait une porte qu'aucune configuration ne montre. Le
|
|
@@ -548,14 +561,14 @@ keycloak: {
|
|
|
548
561
|
1. Keycloak rend un jeton d'accès qui porte `"resource_access": { "mon-app": { "roles": ["admin"] } }`
|
|
549
562
|
— c'est là, et seulement là par défaut, que Keycloak met les rôles (ni dans l'ID token, ni
|
|
550
563
|
dans _userinfo_).
|
|
551
|
-
2. `mapProviderRoles()` (`providerRoles.ts
|
|
552
|
-
3. Le compte de bob reçoit `ROLE_ADMIN` (`reconcileProviderRoles()`, `user/nodefony/src/providerRoles.ts
|
|
564
|
+
2. `mapProviderRoles()` (`providerRoles.ts#mapProviderRoles`) traduit `admin` → `ROLE_ADMIN` par la table.
|
|
565
|
+
3. Le compte de bob reçoit `ROLE_ADMIN` (`reconcileProviderRoles()`, `user/nodefony/src/providerRoles.ts#reconcileProviderRoles` côté
|
|
553
566
|
`@nodefony/user`).
|
|
554
567
|
|
|
555
568
|
**Les règles, à connaître avant de l'activer :**
|
|
556
569
|
|
|
557
570
|
- **Recalcul à chaque connexion ET à chaque jeton d'API.** Un rôle retiré dans Keycloak disparaît
|
|
558
|
-
au login suivant, et dès le jeton suivant sur l'API (`syncOAuthRoles()`, `UserService.ts
|
|
571
|
+
au login suivant, et dès le jeton suivant sur l'API (`syncOAuthRoles()`, `UserService.ts#UserService.syncOAuthRoles`).
|
|
559
572
|
Une session déjà ouverte garde ses droits jusqu'à la reconnexion.
|
|
560
573
|
- **Un rôle Keycloak absent de la table est ignoré**, jamais recopié tel quel : l'annuaire ne
|
|
561
574
|
fabrique pas de droit que l'application n'a pas déclaré.
|
|
@@ -571,7 +584,7 @@ keycloak: {
|
|
|
571
584
|
- **Rôles de plateforme (`ROLE_NODEFONY_*`) refusés par défaut.** Ils ouvrent la console
|
|
572
585
|
d'administration, la génération de code, les secrets : les faire venir de Keycloak fait de
|
|
573
586
|
l'administrateur du realm un administrateur de l'instance. La table qui en contient un refuse
|
|
574
|
-
le démarrage (`config.ts
|
|
587
|
+
le démarrage (`config.ts#oauthProviderSchema`) — sauf `allowPlatformRoles: true` ÉCRIT sur le fournisseur, que
|
|
575
588
|
chaque démarrage rappelle par un avertissement. Sinon, ce rôle se donne à la main
|
|
576
589
|
(`security:user:add --admin`, console d'administration).
|
|
577
590
|
|
|
@@ -597,31 +610,35 @@ coché.
|
|
|
597
610
|
jetons :
|
|
598
611
|
|
|
599
612
|
```typescript ignore
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
613
|
+
// nodefony/config/security.ts
|
|
614
|
+
import type { ISecurityConfigInput } from "@nodefony/security";
|
|
615
|
+
|
|
616
|
+
export const securityConfig = () =>
|
|
617
|
+
({
|
|
618
|
+
resourceServer: {
|
|
619
|
+
issuers: [
|
|
620
|
+
// Les clés publiques du realm sont découvertes par l'émetteur.
|
|
621
|
+
{
|
|
622
|
+
issuer: "https://sso.example.com/realms/mon-entreprise",
|
|
623
|
+
algorithms: ["RS256"],
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
},
|
|
627
|
+
areas: {
|
|
628
|
+
api: {
|
|
629
|
+
pattern: "^/api",
|
|
630
|
+
authenticators: ["external-jwt"],
|
|
631
|
+
stateless: true,
|
|
632
|
+
// L'audience exigée : celle que le mapper inscrit dans le jeton.
|
|
633
|
+
resource: "https://api.example.com",
|
|
607
634
|
},
|
|
608
|
-
],
|
|
609
|
-
},
|
|
610
|
-
areas: {
|
|
611
|
-
api: {
|
|
612
|
-
pattern: "^/api",
|
|
613
|
-
authenticators: ["external-jwt"],
|
|
614
|
-
stateless: true,
|
|
615
|
-
// L'audience exigée : celle que le mapper inscrit dans le jeton.
|
|
616
|
-
resource: "https://api.example.com",
|
|
617
635
|
},
|
|
618
|
-
}
|
|
619
|
-
});
|
|
636
|
+
}) satisfies ISecurityConfigInput;
|
|
620
637
|
```
|
|
621
638
|
|
|
622
639
|
**Le même compte, pas un second.** Quand l'émetteur d'un jeton est celui d'un fournisseur de
|
|
623
640
|
connexion déclaré (`oauth2.providers.keycloak.issuer`), l'authenticator cherche d'abord le compte lié
|
|
624
|
-
à `(keycloak, sub)` — celui que le login par navigateur a créé (`
|
|
641
|
+
à `(keycloak, sub)` — celui que le login par navigateur a créé (`ExternalSubjectResolver.ts#IExternalIssuerBinding.issuer`).
|
|
625
642
|
Sans ce lien, le jeton désignerait un compte distinct, sous l'identifiant `<émetteur>#<sub>`.
|
|
626
643
|
|
|
627
644
|
## ☸️ En cluster — joindre Keycloak par son adresse interne
|
|
@@ -701,17 +718,17 @@ son adresse interne (constaté, pas déduit) :
|
|
|
701
718
|
**Ce qui reste vérifié**, et ne se relâche jamais :
|
|
702
719
|
|
|
703
720
|
- **L'émetteur est comparé à l'identique.** Seul le lieu de lecture change
|
|
704
|
-
(`metadataCandidateUrls()`, `authorizationServer.ts
|
|
721
|
+
(`metadataCandidateUrls()`, `authorizationServer.ts#metadataCandidateUrls`) : un document lu à l'adresse interne
|
|
705
722
|
qui se déclare d'un autre émetteur est refusé.
|
|
706
723
|
- **Le `http` n'est admis que sur l'origine EXACTE de `discoveryUrl`** — même schéma, même hôte,
|
|
707
724
|
même port —, et seulement pour le point de jeton et le jeu de clés
|
|
708
|
-
(`isBackChannelTransportAllowed()`, `authorizationServer.ts
|
|
725
|
+
(`isBackChannelTransportAllowed()`, `authorizationServer.ts#isBackChannelTransportAllowed`). L'opérateur a déjà choisi
|
|
709
726
|
cette adresse en l'écrivant ; un `http` vers une autre adresse est une adresse que personne n'a
|
|
710
727
|
écrite, refusée.
|
|
711
728
|
- **Les points du navigateur restent en `https`** (autorisation, déconnexion) : un navigateur
|
|
712
729
|
renvoyé vers une adresse interne n'aboutirait nulle part.
|
|
713
730
|
- **L'adresse est vérifiée au démarrage** : ni `http(s)`, ou porteuse d'identifiants ou d'un
|
|
714
|
-
fragment → démarrage refusé (`parseDiscoveryUrl()`, `authorizationServer.ts
|
|
731
|
+
fragment → démarrage refusé (`parseDiscoveryUrl()`, `authorizationServer.ts#parseDiscoveryUrl`).
|
|
715
732
|
|
|
716
733
|
> [!WARNING]
|
|
717
734
|
> Une adresse interne en `http` fait passer le **secret du client** et les jetons **en clair** sur
|
|
@@ -723,7 +740,7 @@ son adresse interne (constaté, pas déduit) :
|
|
|
723
740
|
|
|
724
741
|
- **`https` partout, même en développement.** L'ID token est lu sans vérifier sa signature, ce
|
|
725
742
|
qu'OIDC Core §3.1.3.7 n'admet que parce qu'il arrive du point de jeton par TLS. Un émetteur en
|
|
726
|
-
`http` est refusé au démarrage (`canonicalIssuer()`, `authorizationServer.ts
|
|
743
|
+
`http` est refusé au démarrage (`canonicalIssuer()`, `authorizationServer.ts#canonicalIssuer`).
|
|
727
744
|
- **Client confidentiel obligatoire** : le schéma exige un secret non vide. Un client public ne
|
|
728
745
|
conviendrait qu'à une application sans serveur, ce que n'est pas une application Nodefony.
|
|
729
746
|
- **Anti-mix-up** : le paramètre `iss` du retour et le claim `iss` de l'ID token doivent égaler
|
|
@@ -760,27 +777,27 @@ les mêmes échecs (famille « Fournisseurs OAuth »), l'administration les sert
|
|
|
760
777
|
`/nodefony/security/api/oauth/diagnosis`. Côté Keycloak, la sonde ouvre une session de connexion
|
|
761
778
|
vide, qui expire seule.
|
|
762
779
|
|
|
763
|
-
| Symptôme | Cause
|
|
764
|
-
| ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
765
|
-
| Démarrage refusé : `security.oauth2.providers.keycloak.issuer est requis` | Les trois variables ne sont pas posées ensemble, ou `issuer` manque à la main
|
|
766
|
-
| Démarrage refusé : `émetteur invalide … https` | Émetteur en `http`, ou avec `?…`/`#…`
|
|
767
|
-
| Pas de bouton + WARNING `oauth2 provider "keycloak" indisponible` | Keycloak injoignable, ou certificat non reconnu (`fetch failed (SELF_SIGNED_CERT_IN_CHAIN …)`)
|
|
768
|
-
| Démarrage refusé (78) : `http.certificates.extraCaFiles : « … » est illisible` | Le fichier de `NF_TLS_CA_FILE` n'existe pas à ce chemin (relatif = à la racine de l'application)
|
|
769
|
-
| Avec `discoveryUrl` : `le document déclare « http://keycloak.sso:8080/… » alors qu'il a été demandé à « https://sso… »` | `KC_HOSTNAME` absent : Keycloak prend le nom de l'adresse par laquelle on l'interroge
|
|
770
|
-
| Avec `discoveryUrl` : le bouton est là, mais le retour de connexion échoue en `fetch failed` vers l'adresse publique | `KC_HOSTNAME_BACKCHANNEL_DYNAMIC` absent : Keycloak publie son point de jeton sur l'adresse publique
|
|
771
|
-
| Avec `discoveryUrl` : `« token_endpoint » doit être en https (ou en http sur l'origine de discoveryUrl …)` | Keycloak publie une AUTRE origine que celle de `discoveryUrl` (hôte ou port réécrit par un intermédiaire)
|
|
772
|
-
| `Invalid parameter: redirect_uri` sur la page Keycloak | `redirectUri` ≠ les **Valid redirect URIs** du client
|
|
773
|
-
| Retour sur `failureRedirect` + WARNING `unauthorized_client` (secret faux) ou `invalid_client` (client inconnu) | Mauvais secret, client absent du realm, ou client public (_Client authentication OFF_)
|
|
774
|
-
| Retour sur `failureRedirect` + WARNING `OAuth issuer mismatch` | Keycloak joint par deux adresses différentes : l'émetteur varie
|
|
775
|
-
| Retour sur `failureRedirect` + WARNING `local account exists without a keycloak link` | Un compte local porte déjà cet identifiant (`UserService.ts
|
|
776
|
-
| API : `401` avec un jeton Keycloak valide | Audience absente du jeton : pas de mapper _Audience_
|
|
777
|
-
| Session de l'application vivante après une déconnexion chez Keycloak ; Keycloak journalise `Some clients have not been logged out` | _Front channel logout_ ON, ou **Backchannel logout URL** absente ou injoignable depuis Keycloak
|
|
780
|
+
| Symptôme | Cause | Correction |
|
|
781
|
+
| ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
782
|
+
| Démarrage refusé : `security.oauth2.providers.keycloak.issuer est requis` | Les trois variables ne sont pas posées ensemble, ou `issuer` manque à la main | Poser `NF_KEYCLOAK_ISSUER` (`checkProviderIssuer()`, `oauth2.ts#checkProviderIssuer`) |
|
|
783
|
+
| Démarrage refusé : `émetteur invalide … https` | Émetteur en `http`, ou avec `?…`/`#…` | Recopier l'`issuer` du document _OpenID Endpoint Configuration_ |
|
|
784
|
+
| Pas de bouton + WARNING `oauth2 provider "keycloak" indisponible` | Keycloak injoignable, ou certificat non reconnu (`fetch failed (SELF_SIGNED_CERT_IN_CHAIN …)`) | Démarrer Keycloak ; déclarer l'autorité d'entreprise par `NF_TLS_CA_FILE`. Retour sous 30 s |
|
|
785
|
+
| Démarrage refusé (78) : `http.certificates.extraCaFiles : « … » est illisible` | Le fichier de `NF_TLS_CA_FILE` n'existe pas à ce chemin (relatif = à la racine de l'application) | Corriger le chemin, ou monter le fichier dans le pod |
|
|
786
|
+
| Avec `discoveryUrl` : `le document déclare « http://keycloak.sso:8080/… » alors qu'il a été demandé à « https://sso… »` | `KC_HOSTNAME` absent : Keycloak prend le nom de l'adresse par laquelle on l'interroge | `KC_HOSTNAME` = l'URL publique COMPLÈTE |
|
|
787
|
+
| Avec `discoveryUrl` : le bouton est là, mais le retour de connexion échoue en `fetch failed` vers l'adresse publique | `KC_HOSTNAME_BACKCHANNEL_DYNAMIC` absent : Keycloak publie son point de jeton sur l'adresse publique | `KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true` |
|
|
788
|
+
| Avec `discoveryUrl` : `« token_endpoint » doit être en https (ou en http sur l'origine de discoveryUrl …)` | Keycloak publie une AUTRE origine que celle de `discoveryUrl` (hôte ou port réécrit par un intermédiaire) | Écrire dans `discoveryUrl` exactement l'origine que Keycloak publie |
|
|
789
|
+
| `Invalid parameter: redirect_uri` sur la page Keycloak | `redirectUri` ≠ les **Valid redirect URIs** du client | Aligner schéma, hôte (`localhost` ≠ `127.0.0.1`), port et chemin |
|
|
790
|
+
| Retour sur `failureRedirect` + WARNING `unauthorized_client` (secret faux) ou `invalid_client` (client inconnu) | Mauvais secret, client absent du realm, ou client public (_Client authentication OFF_) | Recopier le secret de l'onglet _Credentials_ ; passer le client en ON |
|
|
791
|
+
| Retour sur `failureRedirect` + WARNING `OAuth issuer mismatch` | Keycloak joint par deux adresses différentes : l'émetteur varie | Fixer `KC_HOSTNAME` à l'URL publique complète |
|
|
792
|
+
| Retour sur `failureRedirect` + WARNING `local account exists without a keycloak link` | Un compte local porte déjà cet identifiant (`UserService.ts#UserService.changePassword`) — typiquement un `sub` changé après réimport du realm | Rattacher le compte explicitement, ou fixer l'`id` des utilisateurs dans l'export du realm |
|
|
793
|
+
| API : `401` avec un jeton Keycloak valide | Audience absente du jeton : pas de mapper _Audience_ | `security:keycloak:realm --write` puis import partiel ; `aud` doit contenir la `resource` de la zone |
|
|
794
|
+
| Session de l'application vivante après une déconnexion chez Keycloak ; Keycloak journalise `Some clients have not been logged out` | _Front channel logout_ ON, ou **Backchannel logout URL** absente ou injoignable depuis Keycloak | _Front channel logout_ OFF ; une URL que Keycloak joint (en développement Linux : serveur à l'écoute au-delà de la boucle locale) |
|
|
778
795
|
|
|
779
796
|
| Démarrage refusé : `… est un rôle de PLATEFORME` | `roleMapping` traduit vers un `ROLE_NODEFONY_*` | Donner ce rôle à la main, ou écrire `allowPlatformRoles: true` en connaissance de cause |
|
|
780
797
|
| Le rôle Keycloak n'apparaît pas dans l'application | Rôle du REALM alors que la source est `client`, ou rôle d'un autre client, ou session ouverte avant le changement | Créer le rôle sur le client de l'application, ou `rolesSource: ["realm"]` ; se reconnecter |
|
|
781
798
|
|
|
782
799
|
La cause d'un retour sur `failureRedirect` n'est jamais montrée au navigateur ; elle est dans le
|
|
783
|
-
journal du serveur, préfixée `oauth2 callback "keycloak"` (`OAuth2Controller.ts
|
|
800
|
+
journal du serveur, préfixée `oauth2 callback "keycloak"` (`OAuth2Controller.ts#OAuth2Controller.authorize`).
|
|
784
801
|
|
|
785
802
|
## 🧪 Tests & couverture
|
|
786
803
|
|
package/docs/login-page.md
CHANGED
|
@@ -64,14 +64,14 @@ la personnaliser là où c'est sans risque : les textes, les couleurs, le conten
|
|
|
64
64
|
## La vision Nodefony
|
|
65
65
|
|
|
66
66
|
- **Configuration dans `@nodefony/security`, rendu dans `@nodefony/framework`** : le premier décrit
|
|
67
|
-
la page (`AuthFlow.describeLoginPage()`, `authFlow.ts
|
|
68
|
-
(`buildLoginPageView()`, `loginPage.ts
|
|
69
|
-
`LoginPageController.ts
|
|
70
|
-
(`authRoutes.ts
|
|
67
|
+
la page (`AuthFlow.describeLoginPage()`, `authFlow.ts#AuthFlow.describeLoginPage`), le second la rend
|
|
68
|
+
(`buildLoginPageView()`, `loginPage.ts#buildLoginPageView`) et la sert (`LoginPageController`,
|
|
69
|
+
`LoginPageController.ts#LoginPageController`). Le contrat entre les deux, `ILoginPageDescription`, vit au cœur
|
|
70
|
+
(`authRoutes.ts#ILoginPageDescription`).
|
|
71
71
|
- **Tout passe par du texte échappé** : aucune valeur de la configuration ni de la requête n'entre
|
|
72
72
|
dans le balisage autrement que par `<%= %>`. L'image du panneau passe par votre feuille de style,
|
|
73
73
|
jamais par une adresse injectée dans le HTML.
|
|
74
|
-
- **Un seul déroulé** : le script de la page (`mountLoginPage()`, `mountLoginPage.ts
|
|
74
|
+
- **Un seul déroulé** : le script de la page (`mountLoginPage()`, `mountLoginPage.ts#mountLoginPage`) s'appuie
|
|
75
75
|
sur `NodefonyLogin`, le même client que vos propres écrans peuvent utiliser.
|
|
76
76
|
|
|
77
77
|
## 🚀 Démarrage rapide
|
|
@@ -119,7 +119,7 @@ redéfinissent pour les deux thèmes (voir « Pièges », plus bas) :
|
|
|
119
119
|
### Ce qu'on observe
|
|
120
120
|
|
|
121
121
|
- `GET /login` rend la page, avec `Cache-Control: no-store` et une CSP
|
|
122
|
-
`frame-ancestors 'none'` (`LoginPageController.ts
|
|
122
|
+
`frame-ancestors 'none'` (`LoginPageController.ts#LoginPageController.constructor`) ;
|
|
123
123
|
- le titre de la carte vaut « Espace client », le panneau affiche votre accroche à la place de la
|
|
124
124
|
vitrine Nodefony ;
|
|
125
125
|
- votre feuille arrive en second `<link rel="stylesheet">`, après `login.css` : vos variables
|
|
@@ -136,7 +136,7 @@ et ne laisse que votre marque sur l'image du panneau.
|
|
|
136
136
|
|
|
137
137
|
`password: false` retire le formulaire : il ne reste que les boutons des fournisseurs. Chaque
|
|
138
138
|
bouton prend son image si vous la réglez (`oauth2.providers.<nom>.icon`, sinon la marque GitHub ou
|
|
139
|
-
une clé) — `listDisplayProviders()` (`oauth2.ts
|
|
139
|
+
une clé) — `listDisplayProviders()` (`oauth2.ts#OAuth2Service.listDisplayProviders`).
|
|
140
140
|
|
|
141
141
|
Le libellé suit ce que l'utilisateur connaît. Une marque grand public (GitHub, Google, Microsoft…)
|
|
142
142
|
garde son nom. Un serveur d'identité d'organisation (Keycloak, un OIDC d'entreprise) se lit
|
|
@@ -148,7 +148,7 @@ une page de connexion.
|
|
|
148
148
|
|
|
149
149
|
Par défaut, quand des fournisseurs sont proposés, leurs boutons passent avant le formulaire : la
|
|
150
150
|
plupart des comptes d'une entreprise sont ceux de son annuaire. `providersFirst: false` inverse
|
|
151
|
-
l'ordre (`loginPage.ts
|
|
151
|
+
l'ordre (`loginPage.ts#buildLoginPageView`).
|
|
152
152
|
|
|
153
153
|
### Situation 4 — votre propre page
|
|
154
154
|
|
|
@@ -217,7 +217,7 @@ export const acmeSkin = {
|
|
|
217
217
|
|
|
218
218
|
**Trois feuilles, dans cet ordre** : `login.css` (le socle), la feuille de l'habillage
|
|
219
219
|
(`/nodefony/security/login/skins/<skin>.css`, aucune pour `frontispiece`), puis la vôtre — vos
|
|
220
|
-
variables l'emportent toujours (`loginPage.ts
|
|
220
|
+
variables l'emportent toujours (`loginPage.ts#resolveSkin`).
|
|
221
221
|
|
|
222
222
|
**La photo vient de vous.** Aucun habillage n'en publie : une photo par défaut alourdirait chaque
|
|
223
223
|
installation et finirait à l'identique dans toutes les applications. Une seule variable suffit,
|
|
@@ -233,7 +233,7 @@ une couleur pleine.
|
|
|
233
233
|
```
|
|
234
234
|
|
|
235
235
|
**La mise en page suit l'habillage** : sans `layout`, c'est celle pour laquelle il est dessiné ;
|
|
236
|
-
un `layout` écrit gagne (`authFlow.ts
|
|
236
|
+
un `layout` écrit gagne (`authFlow.ts#AuthFlow.describeLoginPage`).
|
|
237
237
|
|
|
238
238
|
**Keycloak suit le même habillage** : chaque habillage a son thème de connexion Keycloak
|
|
239
239
|
(`nodefony-<habillage>`), que `security:keycloak:realm --write` pose dans le realm d'après
|
|
@@ -243,7 +243,7 @@ couleurs et leur mise en page.
|
|
|
243
243
|
|
|
244
244
|
**Comparer sans redémarrer** : en développement, `/login?skin=dots` sert la page sous un autre
|
|
245
245
|
habillage, et `&layout=card` force la mise en page — le temps d'une requête, sur vos vrais
|
|
246
|
-
réglages. En production ces paramètres sont ignorés (`loginPage.ts
|
|
246
|
+
réglages. En production ces paramètres sont ignorés (`loginPage.ts#resolveSkin`) : la page servie est
|
|
247
247
|
celle de la configuration, et rien d'autre.
|
|
248
248
|
|
|
249
249
|
## 🏗️ Architecture interne
|
|
@@ -262,11 +262,11 @@ sequenceDiagram
|
|
|
262
262
|
```
|
|
263
263
|
|
|
264
264
|
- **La route est montée au démarrage** par `mountLoginPage()` du framework, hors firewall
|
|
265
|
-
(`bypassFirewall`, `LoginPageController.ts
|
|
265
|
+
(`bypassFirewall`, `LoginPageController.ts#mountLoginPage`) — sinon une application qui protège `^/`
|
|
266
266
|
exigerait d'être connecté pour se connecter.
|
|
267
267
|
- **Les fournisseurs sont relus à chaque requête** : l'un d'eux peut tomber ou revenir pendant que
|
|
268
268
|
le serveur tourne, et un bouton qui mène à une erreur est retiré de l'offre.
|
|
269
|
-
- **La destination `?from=`** repasse par `safeRedirectPath` (`loginPage.ts
|
|
269
|
+
- **La destination `?from=`** repasse par `safeRedirectPath` (`loginPage.ts#buildLoginPageView`) : une adresse
|
|
270
270
|
hors de l'origine retombe sur `/`.
|
|
271
271
|
- **Les boutons de fournisseur portent `from` et `back`** : `back` désigne cette page, si bien que
|
|
272
272
|
le « Retour » du fournisseur et un échec y ramènent, destination comprise, quel que soit
|
|
@@ -275,7 +275,7 @@ sequenceDiagram
|
|
|
275
275
|
FIXE dans la zone de message ; une annulation n'en affiche aucun. La raison reçue n'est jamais
|
|
276
276
|
recopiée dans la page.
|
|
277
277
|
|
|
278
|
-
## ⚙️ Configuration (schéma Zod `loginPageSchema`, `config.ts
|
|
278
|
+
## ⚙️ Configuration (schéma Zod `loginPageSchema`, `config.ts#loginPageSchema`)
|
|
279
279
|
|
|
280
280
|
<!-- prettier-ignore -->
|
|
281
281
|
| Option | Type · défaut | Effet |
|
|
@@ -286,17 +286,17 @@ sequenceDiagram
|
|
|
286
286
|
| `logo` | string · logo Nodefony | Adresse du logo (chemin servi, ou URL autorisée par la CSP). |
|
|
287
287
|
| `heading` | string · « Se connecter » | Titre de la carte et de l'onglet. |
|
|
288
288
|
| `subtitle` | string · déduit | Ligne sous le titre ; par défaut, déduite des moyens proposés. |
|
|
289
|
-
| `stylesheet` | string · — | Feuille chargée après `login.css` et l'habillage (`config.ts
|
|
290
|
-
| `skin` | un des neuf · `frontispiece` | Habillage (`config.ts
|
|
291
|
-
| `layout` | `split` · `card` · `bare` · celle de l'habillage | Mise en page : panneau et formulaire, carte centrée, formulaire seul (`config.ts
|
|
292
|
-
| `hero` | `{ heading, text? }` · `false` · vitrine | Contenu du panneau (`config.ts
|
|
293
|
-
| `providersFirst` | boolean · `true` | Fournisseurs avant le formulaire (`config.ts
|
|
294
|
-
| `footer` | boolean · `true` | Protections de la session et « Propulsé par Nodefony » (`config.ts
|
|
289
|
+
| `stylesheet` | string · — | Feuille chargée après `login.css` et l'habillage (`config.ts#studioSchema`). |
|
|
290
|
+
| `skin` | un des neuf · `frontispiece` | Habillage (`config.ts#loginPageSchema`) — voir « Habillages ». |
|
|
291
|
+
| `layout` | `split` · `card` · `bare` · celle de l'habillage | Mise en page : panneau et formulaire, carte centrée, formulaire seul (`config.ts#loginPageSchema`). |
|
|
292
|
+
| `hero` | `{ heading, text? }` · `false` · vitrine | Contenu du panneau (`config.ts#loginPageSchema`) ; une clé inconnue est refusée au démarrage. |
|
|
293
|
+
| `providersFirst` | boolean · `true` | Fournisseurs avant le formulaire (`config.ts#loginPageSchema`). |
|
|
294
|
+
| `footer` | boolean · `true` | Protections de la session et « Propulsé par Nodefony » (`config.ts#loginPageSchema`). |
|
|
295
295
|
| `password` | boolean · `true` | `false` = fournisseurs seulement. |
|
|
296
296
|
| `previewPicker` | boolean · `false` | En **développement** seulement : un sélecteur d'habillage et de mise en page posé dans un coin, qui recharge avec `?skin=` / `?layout=`. Sans effet en production. |
|
|
297
297
|
| `previewHook` | chemin · — | Avec `previewPicker`, en **développement** : chemin de l'application prévenu en `POST` (`{ skin, layout }`, même origine) AVANT chaque rechargement de l'aperçu — pour qu'un tiers suive l'habillage choisi (le thème d'un Keycloak de développement). Un échec n'empêche pas le rechargement ; une autre origine est refusée au démarrage. |
|
|
298
298
|
| `template` | string · — | Gabarit `.eta` de remplacement — dernier recours. |
|
|
299
|
-
| `oauth2.providers.<nom>.icon` | string · icône du framework | Image du bouton du fournisseur (`config.ts
|
|
299
|
+
| `oauth2.providers.<nom>.icon` | string · icône du framework | Image du bouton du fournisseur (`config.ts#oauthProviderSchema`). |
|
|
300
300
|
|
|
301
301
|
Toute la section est en `z.strictObject` : une clé mal orthographiée interrompt le démarrage en la
|
|
302
302
|
nommant, au lieu d'être ignorée.
|
|
@@ -307,7 +307,7 @@ nommant, au lieu d'être ignorée.
|
|
|
307
307
|
le script porte le nonce CSP de la requête.
|
|
308
308
|
- **Mot de passe en clair refusé en production** : sur une requête qui n'arrive pas en HTTPS, la
|
|
309
309
|
connexion répond 403 avant toute vérification, et la page affiche « doit passer par HTTPS » au
|
|
310
|
-
lieu de « mot de passe incorrect » (`mountLoginPage.ts
|
|
310
|
+
lieu de « mot de passe incorrect » (`mountLoginPage.ts#mountLoginPage`). Derrière un proxy qui termine TLS,
|
|
311
311
|
déclarez-le dans `trustProxy` ([authenticators](authenticators.md#mot-de-passe-en-clair--refusé-en-production)).
|
|
312
312
|
- **Message d'échec uniforme** : un refus d'identifiants affiche un message fixe, jamais le texte du
|
|
313
313
|
serveur — il ne dit pas si le compte existe.
|
|
@@ -317,7 +317,7 @@ nommant, au lieu d'être ignorée.
|
|
|
317
317
|
## ⚡ Performance & mémoire
|
|
318
318
|
|
|
319
319
|
- Le gabarit est compilé **une fois par processus**, et un échec de chargement n'est pas mémorisé
|
|
320
|
-
(`keepSuccess()`, `loginPage.ts
|
|
320
|
+
(`keepSuccess()`, `loginPage.ts#keepSuccess`) : la page ne reste pas en 500 après une correction.
|
|
321
321
|
- Feuille et script sont servis en statique, versionnés par leur empreinte (`?v=`) : le navigateur
|
|
322
322
|
les garde en cache jusqu'au prochain changement. Le script pèse 4 Ko.
|
|
323
323
|
|