@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/docs/lexique.md
CHANGED
|
@@ -126,21 +126,21 @@ source: "src/packages/@nodefony/security/docs/lexique.md"
|
|
|
126
126
|
> L'**autorisation** (authz) répond « as-tu le DROIT de faire ça ? » (les voters, `@IsGranted`).
|
|
127
127
|
> On peut être authentifié ET refusé (un `user` connecté sur une route `ROLE_ADMIN` → **403**, pas 401).
|
|
128
128
|
|
|
129
|
-
| Sigle / terme
|
|
130
|
-
|
|
|
131
|
-
| **RBAC**
|
|
132
|
-
| **ABAC**
|
|
133
|
-
| **Voter**
|
|
134
|
-
| **GRANT / DENY / ABSTAIN**
|
|
135
|
-
| **`
|
|
136
|
-
| **Stratégie affirmative**
|
|
137
|
-
| **Veto (DENY)**
|
|
138
|
-
| **Default DENY**
|
|
139
|
-
| **Fail-closed (voter)**
|
|
140
|
-
| **`voterRegistry`**
|
|
141
|
-
| **`RoleVoter`**
|
|
142
|
-
| **Attribute**
|
|
143
|
-
| **Subject**
|
|
129
|
+
| Sigle / terme | Développé | En clair |
|
|
130
|
+
| ---------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| **RBAC** | Role-Based Access Control | Droits par RÔLES (`ROLE_ADMIN` peut X). Simple, couvre 80 % des besoins. Niveau **A** chez Nodefony (`RoleVoter`). |
|
|
132
|
+
| **ABAC** | Attribute-Based Access Control | Droits par ATTRIBUTS contextuels (le **propriétaire** du document peut le modifier) → les voters reçoivent le `subject`. Niveau **C**. |
|
|
133
|
+
| **Voter** | « votant / juré » | Petit juge spécialisé : pour une décision donnée il rend **un** verdict parmi 3 (GRANT / DENY / ABSTAIN). On en empile autant qu'on veut (rôles, propriété, horaires…), chacun sur son domaine. |
|
|
134
|
+
| **GRANT / DENY / ABSTAIN** | accorde / refuse / s'abstient | Les 3 verdicts d'un voter. **ABSTAIN** = « pas mon rayon » (ne compte pas). C'est la combinaison des votes qui tranche, pas un seul. |
|
|
135
|
+
| **`Authorization.decide()`** | — | Le **juge en chef** (`authorization.ts`, J6) : interroge tous les voters et applique la stratégie de vote → un seul booléen GRANT/DENY. |
|
|
136
|
+
| **Stratégie affirmative** | — | « **un seul GRANT suffit** » (tant qu'aucun DENY) — la stratégie de `decide()`. Permissive sur l'accord, stricte sur le refus. |
|
|
137
|
+
| **Veto (DENY)** | — | « **un seul DENY refuse tout** », même si d'autres votent GRANT. Le refus l'emporte toujours (sécurité d'abord). |
|
|
138
|
+
| **Default DENY** | refus par défaut | Tous ABSTAIN, ou **zéro** voter, ou aucun GRANT → **refusé** (Zero Trust). Il faut un OUI explicite, le silence ne suffit pas. |
|
|
139
|
+
| **Fail-closed (voter)** | « échoue fermé » | Si un voter **plante** (throw) → compté comme DENY + log ERROR, jamais une 500 qui laisserait passer. L'erreur ne doit jamais ouvrir la porte (cf lexique général `fail-closed`). |
|
|
140
|
+
| **`voterRegistry`** | registre de voters | Fabrique pluggable (convention-frère de `authenticatorRegistry`) : on **ajoute** un voter sans toucher `decide()`. Le futur `PermissionVoter` (RBAC ORM, niveau B/J6b) s'y branchera. |
|
|
141
|
+
| **`RoleVoter`** | — | Le voter niveau **A** : GRANT si le token porte le rôle demandé (via `RoleHierarchyWalker`), ABSTAIN sinon — **jamais de veto** (il ne bloque pas les attributs qu'il ne connaît pas). |
|
|
142
|
+
| **Attribute** | attribut (de décision) | La chose demandée passée à `decide()` : un rôle (`ROLE_ADMIN`), une permission (`post:edit`)… Ne PAS confondre avec l'ABAC « attribut contextuel » (le `subject`). |
|
|
143
|
+
| **Subject** | sujet / ressource ciblée | L'objet **sur lequel** porte l'action (le document à éditer), passé au voter pour les règles ABAC (« est-il le propriétaire ? »). Optionnel — souvent un param de route via `@IsGranted(..., { subject })`. |
|
|
144
144
|
|
|
145
145
|
### Décorateurs sécurité (panoplie J7)
|
|
146
146
|
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Page de connexion — la page servie par le framework, et comment l'habiller"
|
|
3
|
+
navTitle: Page de connexion
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/security"
|
|
6
|
+
topic: login-page
|
|
7
|
+
section: "Sécurité"
|
|
8
|
+
audience: [developer]
|
|
9
|
+
tags: [security, login, page, theme, branding, sso, csp]
|
|
10
|
+
version: "doc"
|
|
11
|
+
status: stable
|
|
12
|
+
updated: 2026-10-11
|
|
13
|
+
source: "src/packages/@nodefony/security/docs/login-page.md"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Page de connexion — la page servie, et comment l'habiller
|
|
17
|
+
|
|
18
|
+
> Une application qui protège ses routes a besoin d'une page où l'on se connecte. Nodefony en sert
|
|
19
|
+
> une, prête à l'emploi, à l'adresse `/login` : identifiant, mot de passe, second facteur, boutons
|
|
20
|
+
> des fournisseurs (Keycloak, GitHub…), thème clair et sombre. Vous l'**habillez** par la
|
|
21
|
+
> configuration (`security.loginPage`) et une feuille de style — sans copier son balisage. Ancré sur
|
|
22
|
+
> `src/packages/@nodefony/security/nodefony/config/config.ts` (la configuration),
|
|
23
|
+
> `src/packages/@nodefony/framework/nodefony/src/loginPage.ts` (le rendu) et
|
|
24
|
+
> `src/nodefony/src/client/login/mountLoginPage.ts` (le script de la page).
|
|
25
|
+
|
|
26
|
+
📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **Page de connexion**
|
|
27
|
+
|
|
28
|
+
## 🧠 Le modèle mental — la configuration dit QUOI, la feuille dit COMMENT
|
|
29
|
+
|
|
30
|
+
```mermaid
|
|
31
|
+
flowchart LR
|
|
32
|
+
CFG["security.loginPage<br/>(textes, moyens, panneau)"] --> DESC["authFlow.describeLoginPage()"]
|
|
33
|
+
OAUTH["oauth2.providers<br/>(libellé, icône)"] --> DESC
|
|
34
|
+
DESC --> VIEW["buildLoginPageView()<br/>données filtrées"]
|
|
35
|
+
VIEW --> HTML["page HTML<br/>login.css + habillage + votre feuille"]
|
|
36
|
+
HTML --> JS["login.js<br/>déroulé de connexion"]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
La configuration décide de ce que la page **propose et dit** ; l'**habillage** choisi et votre
|
|
40
|
+
feuille de style décident de son **apparence** (couleurs, rayons, photo). Le balisage, lui, reste celui du framework :
|
|
41
|
+
c'est ce qui vous laisse profiter de ses corrections sans rien recopier.
|
|
42
|
+
|
|
43
|
+
## 📖 Lexique
|
|
44
|
+
|
|
45
|
+
| Terme | Signification |
|
|
46
|
+
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| **SSO** | _Single Sign-On_ : se connecter avec le compte d'une organisation (Keycloak, Google…) plutôt qu'un mot de passe local. |
|
|
48
|
+
| **Fournisseur** | Un service d'identité configuré dans `oauth2.providers` ; il devient un bouton sur la page. |
|
|
49
|
+
| **Panneau (`hero`)** | La moitié illustrée de la mise en page `split`, à côté du formulaire. |
|
|
50
|
+
| **Vitrine** | Le contenu par défaut du panneau : la présentation de Nodefony. |
|
|
51
|
+
| **CSP** | _Content Security Policy_ : liste des sources que le navigateur accepte de charger pour la page. |
|
|
52
|
+
| **Redirection ouverte** | Faille où une adresse de retour fournie par l'attaquant renvoie la victime vers un autre site (CWE-601). |
|
|
53
|
+
| **`no-store`** | En-tête qui interdit toute mise en cache de la réponse, y compris par un proxy partagé. |
|
|
54
|
+
|
|
55
|
+
## Qu'est-ce que c'est ?
|
|
56
|
+
|
|
57
|
+
C'est la porte d'entrée de votre application : l'écran que voit quiconque arrive sur une route
|
|
58
|
+
protégée sans session. L'écrire soi-même paraît simple, et c'est là que les failles se logent — une
|
|
59
|
+
adresse de retour `?from=` qui renvoie vers un autre site, une page affichable dans le cadre d'un
|
|
60
|
+
site tiers (détournement de clic), un mot de passe qui part en clair, un message d'erreur qui dit
|
|
61
|
+
si le compte existe. Nodefony sert une page qui ferme ces portes par construction, et vous laisse
|
|
62
|
+
la personnaliser là où c'est sans risque : les textes, les couleurs, le contenu du panneau.
|
|
63
|
+
|
|
64
|
+
## La vision Nodefony
|
|
65
|
+
|
|
66
|
+
- **Configuration dans `@nodefony/security`, rendu dans `@nodefony/framework`** : le premier décrit
|
|
67
|
+
la page (`AuthFlow.describeLoginPage()`, `authFlow.ts:470`), le second la rend
|
|
68
|
+
(`buildLoginPageView()`, `loginPage.ts:227`) et la sert (`LoginPageController`,
|
|
69
|
+
`LoginPageController.ts:102`). Le contrat entre les deux, `ILoginPageDescription`, vit au cœur
|
|
70
|
+
(`authRoutes.ts:262`).
|
|
71
|
+
- **Tout passe par du texte échappé** : aucune valeur de la configuration ni de la requête n'entre
|
|
72
|
+
dans le balisage autrement que par `<%= %>`. L'image du panneau passe par votre feuille de style,
|
|
73
|
+
jamais par une adresse injectée dans le HTML.
|
|
74
|
+
- **Un seul déroulé** : le script de la page (`mountLoginPage()`, `mountLoginPage.ts:85`) s'appuie
|
|
75
|
+
sur `NodefonyLogin`, le même client que vos propres écrans peuvent utiliser.
|
|
76
|
+
|
|
77
|
+
## 🚀 Démarrage rapide
|
|
78
|
+
|
|
79
|
+
### Dans une app `nodefony create app`, la page est DÉJÀ servie
|
|
80
|
+
|
|
81
|
+
Rien à écrire : ouvrez `/login`. Le chemin se change par `loginPage.path`, et toutes les
|
|
82
|
+
redirections le suivent (échec d'un fournisseur, retour après déconnexion).
|
|
83
|
+
|
|
84
|
+
### L'habiller : textes, panneau, feuille de style
|
|
85
|
+
|
|
86
|
+
Dans le fragment de configuration de la sécurité (`nodefony/config/security.ts`, importé par
|
|
87
|
+
`nodefony.config.ts` dans `use("@nodefony/security", …)`) :
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// nodefony/config/security.ts — extrait, compile tel quel
|
|
91
|
+
import type { ISecurityConfigInput } from "@nodefony/security";
|
|
92
|
+
|
|
93
|
+
export const acmeLogin = {
|
|
94
|
+
loginPage: {
|
|
95
|
+
title: "Acme", // nom de l'application, en tête et dans l'onglet
|
|
96
|
+
heading: "Espace client", // titre de la carte
|
|
97
|
+
subtitle: "Votre compte Acme",
|
|
98
|
+
stylesheet: "/assets/acme-login.css", // chargée APRÈS celle du framework
|
|
99
|
+
hero: {
|
|
100
|
+
heading: "Bienvenue chez Acme",
|
|
101
|
+
text: "Vos commandes, vos factures, votre support.",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
} satisfies ISecurityConfigInput;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
La feuille redéfinit les variables `--nf-login-*`. Les couleurs qui changent avec le thème se
|
|
108
|
+
redéfinissent pour les deux thèmes (voir « Pièges », plus bas) :
|
|
109
|
+
|
|
110
|
+
```css
|
|
111
|
+
/* /assets/acme-login.css */
|
|
112
|
+
:root {
|
|
113
|
+
--nf-login-brand: #b4235a; /* même valeur dans les deux thèmes */
|
|
114
|
+
--nf-login-card-radius: 4px;
|
|
115
|
+
--nf-login-hero-image: url("/assets/acme-hero.jpg");
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Ce qu'on observe
|
|
120
|
+
|
|
121
|
+
- `GET /login` rend la page, avec `Cache-Control: no-store` et une CSP
|
|
122
|
+
`frame-ancestors 'none'` (`LoginPageController.ts:112`) ;
|
|
123
|
+
- le titre de la carte vaut « Espace client », le panneau affiche votre accroche à la place de la
|
|
124
|
+
vitrine Nodefony ;
|
|
125
|
+
- votre feuille arrive en second `<link rel="stylesheet">`, après `login.css` : vos variables
|
|
126
|
+
l'emportent.
|
|
127
|
+
|
|
128
|
+
## ⚙️ Choisir sa page — quatre situations
|
|
129
|
+
|
|
130
|
+
### Situation 1 — la page par défaut, à votre nom
|
|
131
|
+
|
|
132
|
+
`title` et `logo` suffisent. La vitrine Nodefony reste dans le panneau ; `hero: false` la retire
|
|
133
|
+
et ne laisse que votre marque sur l'image du panneau.
|
|
134
|
+
|
|
135
|
+
### Situation 2 — connexion par l'organisation seulement (SSO)
|
|
136
|
+
|
|
137
|
+
`password: false` retire le formulaire : il ne reste que les boutons des fournisseurs. Chaque
|
|
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:422`).
|
|
140
|
+
|
|
141
|
+
Le libellé suit ce que l'utilisateur connaît. Une marque grand public (GitHub, Google, Microsoft…)
|
|
142
|
+
garde son nom. Un serveur d'identité d'organisation (Keycloak, un OIDC d'entreprise) se lit
|
|
143
|
+
**« Compte de l'organisation »**, son nom passant en petit dessous, `SSO · Keycloak` (votre
|
|
144
|
+
`oauth2.providers.<nom>.label` y figure) : personne ne sait ce qu'est « Keycloak » en arrivant sur
|
|
145
|
+
une page de connexion.
|
|
146
|
+
|
|
147
|
+
### Situation 3 — le formulaire d'abord
|
|
148
|
+
|
|
149
|
+
Par défaut, quand des fournisseurs sont proposés, leurs boutons passent avant le formulaire : la
|
|
150
|
+
plupart des comptes d'une entreprise sont ceux de son annuaire. `providersFirst: false` inverse
|
|
151
|
+
l'ordre (`loginPage.ts:255`).
|
|
152
|
+
|
|
153
|
+
### Situation 4 — votre propre page
|
|
154
|
+
|
|
155
|
+
`enabled: false` : le framework ne sert plus rien à ce chemin, et votre application y sert sa
|
|
156
|
+
page, construite sur `NodefonyLogin` (voir la doc du client du cœur). Les redirections continuent
|
|
157
|
+
d'y mener. `template` (un gabarit `.eta` qui remplace celui du framework) existe aussi, en dernier
|
|
158
|
+
recours : un gabarit copié ne suit plus les évolutions du balisage.
|
|
159
|
+
|
|
160
|
+
### Le thème clair ou sombre
|
|
161
|
+
|
|
162
|
+
Le système décide par défaut (`prefers-color-scheme`). Un bouton en haut à droite bascule entre les
|
|
163
|
+
deux, et le choix est retenu dans le navigateur (`LOGIN_PAGE_THEME_KEY`), relu avant le premier
|
|
164
|
+
affichage — la page ne clignote pas. Un `?theme=light|dark` dans l'adresse l'emporte : c'est ainsi
|
|
165
|
+
que Studio passe son thème à la page.
|
|
166
|
+
|
|
167
|
+
### Ce que la carte propose — tous les cas
|
|
168
|
+
|
|
169
|
+
La carte se compose de trois moyens, chacun décidé par un réglage différent : le formulaire
|
|
170
|
+
(`loginPage.password`), la passkey (`passkeys.enabled`, service WebAuthn prêt) et les
|
|
171
|
+
fournisseurs (ceux qui répondent). `describeLoginPage()` les relit à chaque affichage.
|
|
172
|
+
|
|
173
|
+
| Moyens proposés | La carte montre |
|
|
174
|
+
| -------------------------- | ---------------------------------------------------------------------------- |
|
|
175
|
+
| mot de passe | le formulaire |
|
|
176
|
+
| mot de passe + passkey | le formulaire, « ou », le bouton « Passkey / empreinte digitale » |
|
|
177
|
+
| mot de passe + fournisseur | le formulaire, « ou », les boutons des fournisseurs |
|
|
178
|
+
| passkey seule | le bouton passkey ; si le navigateur ne sait pas la faire, un message le dit |
|
|
179
|
+
| fournisseurs (± passkey) | les boutons, sans formulaire |
|
|
180
|
+
| **aucun** | un message « aucun moyen de connexion », et un WARNING au démarrage |
|
|
181
|
+
|
|
182
|
+
Le bouton passkey est rendu MASQUÉ : c'est le script de la page qui le montre, après avoir
|
|
183
|
+
constaté que le navigateur sait faire WebAuthn. Un écran de connexion à vous lit le même état par
|
|
184
|
+
`GET /nodefony/security/api/webauthn/status` (`WEBAUTHN_STATUS_PATH`, `{ enabled }`), pour ne
|
|
185
|
+
pas proposer un bouton dont la cérémonie répondrait `503`.
|
|
186
|
+
|
|
187
|
+
## 🎨 Habillages — neuf apparences, un seul balisage
|
|
188
|
+
|
|
189
|
+
Le framework livre neuf habillages. Chacun est une feuille qui redéfinit les variables
|
|
190
|
+
`--nf-login-*` et, au plus, la disposition de sa mise en page : le balisage ne change jamais, donc
|
|
191
|
+
vos réglages (`heading`, `hero`, `providersFirst`…) et votre feuille valent pour les neuf.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// nodefony/config/security.ts — extrait, compile tel quel
|
|
195
|
+
import type { ISecurityConfigInput } from "@nodefony/security";
|
|
196
|
+
|
|
197
|
+
export const acmeSkin = {
|
|
198
|
+
loginPage: {
|
|
199
|
+
skin: "horizon", // un des neuf ; un nom inconnu arrête le démarrage
|
|
200
|
+
stylesheet: "/brand/login.css", // votre photo et vos couleurs, chargée en dernier
|
|
201
|
+
},
|
|
202
|
+
} satisfies ISecurityConfigInput;
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
<!-- prettier-ignore -->
|
|
206
|
+
| `skin` | Mise en page | Fond de page | Pour quoi |
|
|
207
|
+
| --- | --- | --- | --- |
|
|
208
|
+
| `frontispiece` (défaut) | `split` | — | Intranet, application d'entreprise : panneau de marque à bord courbe, les comptes de l'organisation avant le mot de passe local. `login.css` seule. |
|
|
209
|
+
| `ledger` | `bare` | — | Outil interne, back-office : feuille en deux colonnes, étapes numérotées, formulaire à droite. |
|
|
210
|
+
| `horizon` | `card` | — | Grand public, SaaS : carte postale, la photo en bandeau dans la carte. |
|
|
211
|
+
| `console` | `card` | — | Outils, consoles : la signature de la barre de debug, en carte centrée. |
|
|
212
|
+
| `blueprint` | `card` | grille de plan | Produits techniques, API, données : fond bleu nuit. |
|
|
213
|
+
| `dots` | `card` | trame de points | Applications métier : discret, clair d'abord, ombre légère. |
|
|
214
|
+
| `minimal` | `bare` | — | Produit qui veut s'effacer : pas de carte, beaucoup d'air. |
|
|
215
|
+
| `enterprise` | `card` | courbes de niveau | Intranet derrière un fournisseur d'identité, palette sarcelle ; à marier avec `providersFirst: true`. |
|
|
216
|
+
| `photo-card` | `card` | photo voilée | La photo de l'application en fond de page, la carte reste pleine au-dessus. |
|
|
217
|
+
|
|
218
|
+
**Trois feuilles, dans cet ordre** : `login.css` (le socle), la feuille de l'habillage
|
|
219
|
+
(`/nodefony/security/login/skins/<skin>.css`, aucune pour `frontispiece`), puis la vôtre — vos
|
|
220
|
+
variables l'emportent toujours (`loginPage.ts:210`).
|
|
221
|
+
|
|
222
|
+
**La photo vient de vous.** Aucun habillage n'en publie : une photo par défaut alourdirait chaque
|
|
223
|
+
installation et finirait à l'identique dans toutes les applications. Une seule variable suffit,
|
|
224
|
+
chaque habillage la place où il veut — le panneau (`frontispiece`), le bandeau (`horizon`), le fond
|
|
225
|
+
de page (`photo-card`) — et changer d'habillage la conserve. Sans elle, ces emplacements prennent
|
|
226
|
+
une couleur pleine.
|
|
227
|
+
|
|
228
|
+
```css
|
|
229
|
+
/* /brand/login.css — servie par votre application (public/brand/) */
|
|
230
|
+
:root {
|
|
231
|
+
--nf-login-hero-image: url("/brand/login-hero.webp");
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
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:495`).
|
|
237
|
+
|
|
238
|
+
**Keycloak suit le même habillage** : chaque habillage a son thème de connexion Keycloak
|
|
239
|
+
(`nodefony-<habillage>`), que `security:keycloak:realm --write` pose dans le realm d'après
|
|
240
|
+
`loginPage.skin` — voir [Keycloak](keycloak.md). Seuls `ledger` et `horizon` y perdent leur
|
|
241
|
+
structure propre (les formulaires de Keycloak n'ont pas nos classes) et n'y gardent que leurs
|
|
242
|
+
couleurs et leur mise en page.
|
|
243
|
+
|
|
244
|
+
**Comparer sans redémarrer** : en développement, `/login?skin=dots` sert la page sous un autre
|
|
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:207`) : la page servie est
|
|
247
|
+
celle de la configuration, et rien d'autre.
|
|
248
|
+
|
|
249
|
+
## 🏗️ Architecture interne
|
|
250
|
+
|
|
251
|
+
```mermaid
|
|
252
|
+
sequenceDiagram
|
|
253
|
+
participant N as Navigateur
|
|
254
|
+
participant C as LoginPageController
|
|
255
|
+
participant A as authFlow
|
|
256
|
+
N->>C: GET /login?from=/admin
|
|
257
|
+
C->>A: describeLoginPage()
|
|
258
|
+
A-->>C: description (null = page désactivée)
|
|
259
|
+
C->>C: buildLoginPageView() — from filtré, thème filtré
|
|
260
|
+
C-->>N: HTML (no-store, CSP + nonce)
|
|
261
|
+
N->>N: login.js — identifiant, mot de passe, second facteur
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
- **La route est montée au démarrage** par `mountLoginPage()` du framework, hors firewall
|
|
265
|
+
(`bypassFirewall`, `LoginPageController.ts:156`) — sinon une application qui protège `^/`
|
|
266
|
+
exigerait d'être connecté pour se connecter.
|
|
267
|
+
- **Les fournisseurs sont relus à chaque requête** : l'un d'eux peut tomber ou revenir pendant que
|
|
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:231`) : une adresse
|
|
270
|
+
hors de l'origine retombe sur `/`.
|
|
271
|
+
- **Les boutons de fournisseur portent `from` et `back`** : `back` désigne cette page, si bien que
|
|
272
|
+
le « Retour » du fournisseur et un échec y ramènent, destination comprise, quel que soit
|
|
273
|
+
`failureRedirect` (cf `oauth2.md`, § Les retours).
|
|
274
|
+
- **Un retour sans connexion s'annonce** : `?reason=` autre qu'`access_denied` affiche un message
|
|
275
|
+
FIXE dans la zone de message ; une annulation n'en affiche aucun. La raison reçue n'est jamais
|
|
276
|
+
recopiée dans la page.
|
|
277
|
+
|
|
278
|
+
## ⚙️ Configuration (schéma Zod `loginPageSchema`, `config.ts:1040`)
|
|
279
|
+
|
|
280
|
+
<!-- prettier-ignore -->
|
|
281
|
+
| Option | Type · défaut | Effet |
|
|
282
|
+
| --- | --- | --- |
|
|
283
|
+
| `enabled` | boolean · `true` | `false` = le framework ne sert pas de page ; la vôtre prend le chemin. |
|
|
284
|
+
| `path` | string · `/login` | Chemin local de la page, sans requête ni fragment. |
|
|
285
|
+
| `title` | string · nom de l'application | Nom affiché en tête et dans l'onglet. |
|
|
286
|
+
| `logo` | string · logo Nodefony | Adresse du logo (chemin servi, ou URL autorisée par la CSP). |
|
|
287
|
+
| `heading` | string · « Se connecter » | Titre de la carte et de l'onglet. |
|
|
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:1024`). |
|
|
290
|
+
| `skin` | un des neuf · `frontispiece` | Habillage (`config.ts:1133`) — voir « Habillages ». |
|
|
291
|
+
| `layout` | `split` · `card` · `bare` · celle de l'habillage | Mise en page : panneau et formulaire, carte centrée, formulaire seul (`config.ts:1139`). |
|
|
292
|
+
| `hero` | `{ heading, text? }` · `false` · vitrine | Contenu du panneau (`config.ts:1105`) ; une clé inconnue est refusée au démarrage. |
|
|
293
|
+
| `providersFirst` | boolean · `true` | Fournisseurs avant le formulaire (`config.ts:1099`). |
|
|
294
|
+
| `footer` | boolean · `true` | Protections de la session et « Propulsé par Nodefony » (`config.ts:1121`). |
|
|
295
|
+
| `password` | boolean · `true` | `false` = fournisseurs seulement. |
|
|
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
|
+
| `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
|
+
| `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:1296`). |
|
|
300
|
+
|
|
301
|
+
Toute la section est en `z.strictObject` : une clé mal orthographiée interrompt le démarrage en la
|
|
302
|
+
nommant, au lieu d'être ignorée.
|
|
303
|
+
|
|
304
|
+
## 🔒 Sécurité
|
|
305
|
+
|
|
306
|
+
- **Pas de cache partagé** (`no-store`) ni d'affichage dans un cadre (`frame-ancestors 'none'`) ;
|
|
307
|
+
le script porte le nonce CSP de la requête.
|
|
308
|
+
- **Mot de passe en clair refusé en production** : sur une requête qui n'arrive pas en HTTPS, la
|
|
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:150`). Derrière un proxy qui termine TLS,
|
|
311
|
+
déclarez-le dans `trustProxy` ([authenticators](authenticators.md#mot-de-passe-en-clair--refusé-en-production)).
|
|
312
|
+
- **Message d'échec uniforme** : un refus d'identifiants affiche un message fixe, jamais le texte du
|
|
313
|
+
serveur — il ne dit pas si le compte existe.
|
|
314
|
+
- **Vos adresses** (`logo`, `stylesheet`, `icon`) sont soumises à la CSP de la page : servez-les
|
|
315
|
+
depuis l'application, ou autorisez explicitement leur origine.
|
|
316
|
+
|
|
317
|
+
## ⚡ Performance & mémoire
|
|
318
|
+
|
|
319
|
+
- Le gabarit est compilé **une fois par processus**, et un échec de chargement n'est pas mémorisé
|
|
320
|
+
(`keepSuccess()`, `loginPage.ts:128`) : la page ne reste pas en 500 après une correction.
|
|
321
|
+
- Feuille et script sont servis en statique, versionnés par leur empreinte (`?v=`) : le navigateur
|
|
322
|
+
les garde en cache jusqu'au prochain changement. Le script pèse 4 Ko.
|
|
323
|
+
|
|
324
|
+
## ⚠️ Pièges (symptôme → cause → correction)
|
|
325
|
+
|
|
326
|
+
| Symptôme | Cause | Correction |
|
|
327
|
+
| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
328
|
+
| Ma couleur d'accent ne s'applique qu'en thème sombre | Les couleurs qui dépendent du thème (`--nf-login-accent`, `--nf-login-bg`…) sont redéfinies pour le thème clair, avec un sélecteur plus précis que `:root` | Les redéfinir aussi sous `@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) { … } }` et `:root[data-theme="light"]` |
|
|
329
|
+
| Ma feuille ou mon logo ne se charge pas | Adresse d'un autre site, refusée par la CSP | Servir le fichier depuis l'application |
|
|
330
|
+
| `hero: { image: … }` refuse de démarrer | Le panneau n'accepte que du texte | Poser l'image dans la feuille : `--nf-login-hero-image: url(…)` |
|
|
331
|
+
| Le panneau n'apparaît pas | `layout: "card"` ou `"bare"` : seul `split` a un panneau | `layout: "split"` |
|
|
332
|
+
| Toutes les connexions répondent 403 « doit passer par HTTPS » en production | Pod en HTTP derrière un proxy TLS non déclaré | Déclarer le proxy dans `trustProxy` |
|
|
333
|
+
| La page répond 404 | `enabled: false`, ou configuration de sécurité invalide (journal CRITIC au démarrage) | Réactiver, ou corriger la configuration |
|
|
334
|
+
|
|
335
|
+
## 🧪 Tests & couverture
|
|
336
|
+
|
|
337
|
+
- **unit (framework)** : `tests/unit/loginPage.test.ts` — données du gabarit filtrées, gabarit
|
|
338
|
+
échappé (titres, sous-titre, accroche et adresses hostiles rendus en texte), chaque réglage de la
|
|
339
|
+
page, et le **vrai** script monté sur le HTML rendu jusqu'à l'ouverture de la session ;
|
|
340
|
+
- **unit (security)** : `tests/unit/loginPage.test.ts` — la section `loginPage` et sa lecture par
|
|
341
|
+
`describeLoginPage()` ; `oauth2Service.test.ts` — libellé et image des boutons ;
|
|
342
|
+
- **unit (cœur)** : `loginSkins.test.ts` — une feuille par habillage et aucune hors catalogue,
|
|
343
|
+
jeu clair écrit deux fois à l'identique, aucune variable lue sans définition ni posée sans
|
|
344
|
+
lecteur, aucune image publiée ; `clientLoginPage.test.ts` — le script de la page : déroulé complet, destination
|
|
345
|
+
hors origine refusée, message fixe, refus du clair reconnu ;
|
|
346
|
+
- **intégration live** : `tests/integration/login-page.test.ts` chez `@nodefony/framework` (serveur
|
|
347
|
+
réel : en-têtes, CSP, fichiers servis) ;
|
|
348
|
+
- **attaque** : `securedArea.attack.test.ts` — une zone protégée couvre la casse et la barre finale
|
|
349
|
+
que le routeur sert.
|
|
350
|
+
|
|
351
|
+
Ce qui manque : aucun test de charge dédié à la page (elle est servie hors du chemin chaud).
|
|
352
|
+
Couverture : `npm run coverage` dans chaque paquet.
|
|
353
|
+
|
|
354
|
+
## 🔗 Pour aller plus loin
|
|
355
|
+
|
|
356
|
+
- ⬆️ **Retour au hub** : [Sécurité — vue d'ensemble](index.md) · [Toute la documentation](../../../../../docs/index.md)
|
|
357
|
+
- 🧭 **Pages sœurs** : [Authenticators](authenticators.md) · [OAuth2 et fournisseurs](oauth2.md) · [Keycloak](keycloak.md)
|
|
358
|
+
|
|
359
|
+
- La décision d'architecture (page servie par le framework) → ADR-0015 dans `docs/adr/`
|
|
360
|
+
- Le client de connexion sans interface (`NodefonyLogin`) → doc du client du cœur
|
|
361
|
+
- Les en-têtes de sécurité et la CSP → [headers](./headers.md)
|