Nodefony 10.0.0-beta.5
Framework Node.js fullstack — HTTP & WebSocket, même contexte

Page de connexion — la page servie par le framework, et comment l'habiller

stable@nodefony/securitymis à jour 2026-10-11

Une application qui protège ses routes a besoin d’une page où l’on se connecte. Nodefony en sert une, prête à l’emploi, à l’adresse /login : identifiant, mot de passe, second facteur, boutons des fournisseurs (Keycloak, GitHub…), thème clair et sombre. Vous l’habillez par la configuration (security.loginPage) et une feuille de style — sans copier son balisage. Ancré sur src/packages/@nodefony/security/nodefony/config/config.ts (la configuration), src/packages/@nodefony/framework/nodefony/src/loginPage.ts (le rendu) et src/nodefony/src/client/login/mountLoginPage.ts (le script de la page).

📍 Documentation › Sécurité › Page de connexion

🧠 Le modèle mental — la configuration dit QUOI, la feuille dit COMMENT#

La configuration décide de ce que la page propose et dit ; l’habillage choisi et votre feuille de style décident de son apparence (couleurs, rayons, photo). Le balisage, lui, reste celui du framework : c’est ce qui vous laisse profiter de ses corrections sans rien recopier.

📖 Lexique#

Terme Signification
SSO Single Sign-On : se connecter avec le compte d’une organisation (Keycloak, Google…) plutôt qu’un mot de passe local.
Fournisseur Un service d’identité configuré dans oauth2.providers ; il devient un bouton sur la page.
Panneau (hero) La moitié illustrée de la mise en page split, à côté du formulaire.
Vitrine Le contenu par défaut du panneau : la présentation de Nodefony.
CSP Content Security Policy : liste des sources que le navigateur accepte de charger pour la page.
Redirection ouverte Faille où une adresse de retour fournie par l’attaquant renvoie la victime vers un autre site (CWE-601).
no-store En-tête qui interdit toute mise en cache de la réponse, y compris par un proxy partagé.

Qu’est-ce que c’est ?#

C’est la porte d’entrée de votre application : l’écran que voit quiconque arrive sur une route protégée sans session. L’écrire soi-même paraît simple, et c’est là que les failles se logent — une adresse de retour ?from= qui renvoie vers un autre site, une page affichable dans le cadre d’un site tiers (détournement de clic), un mot de passe qui part en clair, un message d’erreur qui dit si le compte existe. Nodefony sert une page qui ferme ces portes par construction, et vous laisse la personnaliser là où c’est sans risque : les textes, les couleurs, le contenu du panneau.

La vision Nodefony#

🚀 Démarrage rapide#

Dans une app nodefony create app, la page est DÉJÀ servie#

Rien à écrire : ouvrez /login. Le chemin se change par loginPage.path, et toutes les redirections le suivent (échec d’un fournisseur, retour après déconnexion).

L’habiller : textes, panneau, feuille de style#

Dans le fragment de configuration de la sécurité (nodefony/config/security.ts, importé par nodefony.config.ts dans use("@nodefony/security", …)) :

// nodefony/config/security.ts — extrait, compile tel quel
import type { ISecurityConfigInput } from "@nodefony/security";

export const acmeLogin = {
  loginPage: {
    title: "Acme", // nom de l'application, en tête et dans l'onglet
    heading: "Espace client", // titre de la carte
    subtitle: "Votre compte Acme",
    stylesheet: "/assets/acme-login.css", // chargée APRÈS celle du framework
    hero: {
      heading: "Bienvenue chez Acme",
      text: "Vos commandes, vos factures, votre support.",
    },
  },
} satisfies ISecurityConfigInput;

La feuille redéfinit les variables --nf-login-*. Les couleurs qui changent avec le thème se redéfinissent pour les deux thèmes (voir « Pièges », plus bas) :

/* /assets/acme-login.css */
:root {
  --nf-login-brand: #b4235a; /* même valeur dans les deux thèmes */
  --nf-login-card-radius: 4px;
  --nf-login-hero-image: url("/assets/acme-hero.jpg");
}

Ce qu’on observe#

⚙️ Choisir sa page — quatre situations#

Situation 1 — la page par défaut, à votre nom#

title et logo suffisent. La vitrine Nodefony reste dans le panneau ; hero: false la retire et ne laisse que votre marque sur l’image du panneau.

Situation 2 — connexion par l’organisation seulement (SSO)#

password: false retire le formulaire : il ne reste que les boutons des fournisseurs. Chaque bouton prend son image si vous la réglez (oauth2.providers.<nom>.icon, sinon la marque GitHub ou une clé) — listDisplayProviders() (oauth2.ts#OAuth2Service.listDisplayProviders).

Le libellé suit ce que l’utilisateur connaît. Une marque grand public (GitHub, Google, Microsoft…) garde son nom. Un serveur d’identité d’organisation (Keycloak, un OIDC d’entreprise) se lit « Compte de l’organisation », son nom passant en petit dessous, SSO · Keycloak (votre oauth2.providers.<nom>.label y figure) : personne ne sait ce qu’est « Keycloak » en arrivant sur une page de connexion.

Situation 3 — le formulaire d’abord#

Par défaut, quand des fournisseurs sont proposés, leurs boutons passent avant le formulaire : la plupart des comptes d’une entreprise sont ceux de son annuaire. providersFirst: false inverse l’ordre (loginPage.ts#buildLoginPageView).

Situation 4 — votre propre page#

enabled: false : le framework ne sert plus rien à ce chemin, et votre application y sert sa page, construite sur NodefonyLogin (voir la doc du client du cœur). Les redirections continuent d’y mener. template (un gabarit .eta qui remplace celui du framework) existe aussi, en dernier recours : un gabarit copié ne suit plus les évolutions du balisage.

Le thème clair ou sombre#

Le système décide par défaut (prefers-color-scheme). Un bouton en haut à droite bascule entre les deux, et le choix est retenu dans le navigateur (LOGIN_PAGE_THEME_KEY), relu avant le premier affichage — la page ne clignote pas. Un ?theme=light|dark dans l’adresse l’emporte : c’est ainsi que Studio passe son thème à la page.

Ce que la carte propose — tous les cas#

La carte se compose de trois moyens, chacun décidé par un réglage différent : le formulaire (loginPage.password), la passkey (passkeys.enabled, service WebAuthn prêt) et les fournisseurs (ceux qui répondent). describeLoginPage() les relit à chaque affichage.

Moyens proposés La carte montre
mot de passe le formulaire
mot de passe + passkey le formulaire, « ou », le bouton « Passkey / empreinte digitale »
mot de passe + fournisseur le formulaire, « ou », les boutons des fournisseurs
passkey seule le bouton passkey ; si le navigateur ne sait pas la faire, un message le dit
fournisseurs (± passkey) les boutons, sans formulaire
aucun un message « aucun moyen de connexion », et un WARNING au démarrage

Le bouton passkey est rendu MASQUÉ : c’est le script de la page qui le montre, après avoir constaté que le navigateur sait faire WebAuthn. Un écran de connexion à vous lit le même état par GET /nodefony/security/api/webauthn/status (WEBAUTHN_STATUS_PATH, { enabled }), pour ne pas proposer un bouton dont la cérémonie répondrait 503.

🎨 Habillages — neuf apparences, un seul balisage#

Le framework livre neuf habillages. Chacun est une feuille qui redéfinit les variables --nf-login-* et, au plus, la disposition de sa mise en page : le balisage ne change jamais, donc vos réglages (heading, hero, providersFirst…) et votre feuille valent pour les neuf.

// nodefony/config/security.ts — extrait, compile tel quel
import type { ISecurityConfigInput } from "@nodefony/security";

export const acmeSkin = {
  loginPage: {
    skin: "horizon", // un des neuf ; un nom inconnu arrête le démarrage
    stylesheet: "/brand/login.css", // votre photo et vos couleurs, chargée en dernier
  },
} satisfies ISecurityConfigInput;
skin Mise en page Fond de page Pour quoi
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.
ledger bare — Outil interne, back-office : feuille en deux colonnes, étapes numérotées, formulaire à droite.
horizon card — Grand public, SaaS : carte postale, la photo en bandeau dans la carte.
console card — Outils, consoles : la signature de la barre de debug, en carte centrée.
blueprint card grille de plan Produits techniques, API, données : fond bleu nuit.
dots card trame de points Applications métier : discret, clair d’abord, ombre légère.
minimal bare — Produit qui veut s’effacer : pas de carte, beaucoup d’air.
enterprise card courbes de niveau Intranet derrière un fournisseur d’identité, palette sarcelle ; à marier avec providersFirst: true.
photo-card card photo voilée La photo de l’application en fond de page, la carte reste pleine au-dessus.

Trois feuilles, dans cet ordre : login.css (le socle), la feuille de l’habillage (/nodefony/security/login/skins/<skin>.css, aucune pour frontispiece), puis la vôtre — vos variables l’emportent toujours (loginPage.ts#resolveSkin).

La photo vient de vous. Aucun habillage n’en publie : une photo par défaut alourdirait chaque installation et finirait à l’identique dans toutes les applications. Une seule variable suffit, chaque habillage la place où il veut — le panneau (frontispiece), le bandeau (horizon), le fond de page (photo-card) — et changer d’habillage la conserve. Sans elle, ces emplacements prennent une couleur pleine.

/* /brand/login.css — servie par votre application (public/brand/) */
:root {
  --nf-login-hero-image: url("/brand/login-hero.webp");
}

La mise en page suit l’habillage : sans layout, c’est celle pour laquelle il est dessiné ; un layout écrit gagne (authFlow.ts#AuthFlow.describeLoginPage).

Keycloak suit le même habillage : chaque habillage a son thème de connexion Keycloak (nodefony-<habillage>), que security:keycloak:realm --write pose dans le realm d’après loginPage.skin — voir Keycloak. Seuls ledger et horizon y perdent leur structure propre (les formulaires de Keycloak n’ont pas nos classes) et n’y gardent que leurs couleurs et leur mise en page.

Comparer sans redémarrer : en développement, /login?skin=dots sert la page sous un autre habillage, et &layout=card force la mise en page — le temps d’une requête, sur vos vrais réglages. En production ces paramètres sont ignorés (loginPage.ts#resolveSkin) : la page servie est celle de la configuration, et rien d’autre.

🏗️ Architecture interne#

⚙️ Configuration (schéma Zod loginPageSchema, config.ts#loginPageSchema)#

Option Type · défaut Effet
enabled boolean · true false = le framework ne sert pas de page ; la vôtre prend le chemin.
path string · /login Chemin local de la page, sans requête ni fragment.
title string · nom de l’application Nom affiché en tête et dans l’onglet.
logo string · logo Nodefony Adresse du logo (chemin servi, ou URL autorisée par la CSP).
heading string · « Se connecter » Titre de la carte et de l’onglet.
subtitle string · déduit Ligne sous le titre ; par défaut, déduite des moyens proposés.
stylesheet string · — Feuille chargée après login.css et l’habillage (config.ts#studioSchema).
skin un des neuf · frontispiece Habillage (config.ts#loginPageSchema) — voir « Habillages ».
layout split · card · bare · celle de l’habillage Mise en page : panneau et formulaire, carte centrée, formulaire seul (config.ts#loginPageSchema).
hero { heading, text? } · false · vitrine Contenu du panneau (config.ts#loginPageSchema) ; une clé inconnue est refusée au démarrage.
providersFirst boolean · true Fournisseurs avant le formulaire (config.ts#loginPageSchema).
footer boolean · true Protections de la session et « Propulsé par Nodefony » (config.ts#loginPageSchema).
password boolean · true false = fournisseurs seulement.
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.
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.
template string · — Gabarit .eta de remplacement — dernier recours.
oauth2.providers.<nom>.icon string · icône du framework Image du bouton du fournisseur (config.ts#oauthProviderSchema).

Toute la section est en z.strictObject : une clé mal orthographiée interrompt le démarrage en la nommant, au lieu d’être ignorée.

🔒 Sécurité#

⚡ Performance & mémoire#

⚠️ Pièges (symptôme → cause → correction)#

Symptôme Cause Correction
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"]
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
hero: { image: … } refuse de démarrer Le panneau n’accepte que du texte Poser l’image dans la feuille : --nf-login-hero-image: url(…)
Le panneau n’apparaît pas layout: "card" ou "bare" : seul split a un panneau layout: "split"
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
La page répond 404 enabled: false, ou configuration de sécurité invalide (journal CRITIC au démarrage) Réactiver, ou corriger la configuration

🧪 Tests & couverture#

Ce qui manque : aucun test de charge dédié à la page (elle est servie hors du chemin chaud). Couverture : npm run coverage dans chaque paquet.

🔗 Pour aller plus loin#