Skip to content

Authentification

rocambille edited this page Apr 26, 2026 · 16 revisions

Résumé : StartER propose une implémentation complète et sécurisée d'authentification sans mot de passe (Magic Link), basée sur des jetons opaques stockés en base de données et un JWT de session stocké dans un cookie sécurisé.

Le fonctionnement de l'authentification

Le processus dans StartER repose sur un lien de connexion envoyé par email :

  1. L'utilisateur saisit son email.
  2. Le serveur génère un jeton opaque (random bytes), le hache, le stocke en base et envoie le lien par email via SMTP (ou l'affiche dans le terminal en développement).
  3. L'utilisateur clique sur le lien, arrivant sur une page de vérification.
  4. Le serveur vérifie la validité du jeton (non expiré, non consommé).
  5. Un token JWT de session est généré et stocké dans un cookie HTTP-only.
  6. Le frontend React est informé de la connexion via le AuthContext.

Implémentation dans StartER

Côté serveur : Express

Les fonctionnalités d'authentification sont regroupées dans le module auth :

src/express/modules/auth/
├── authActions.ts    (Logique métier et middleware)
├── authRepository.ts (Accès aux jetons en base)
└── authRoutes.ts     (Définition des endpoints)

Endpoints principaux

Méthode Route Action Description
POST /api/auth/magic-link sendMagicLink Envoie le lien de connexion par email
POST /api/auth/verify verifyMagicLink Vérifie le jeton opaque et pose le cookie
DELETE /api/auth/logout destroyAccessToken Déconnexion (supprime le cookie)
GET /api/me readMe Renvoie l'utilisateur connecté (req.me)

Envoi du lien (sendMagicLink) et stratégie d'email hybride

Lorsqu'un utilisateur demande un lien, le serveur cherche ou crée l'utilisateur, génère un jeton aléatoire sécurisé, puis stocke son hachage SHA-256 en base.

Afin de faciliter le développement local sans imposer la configuration immédiate d'un outil comme Docker ou Mailpit, StartER adopte une approche d'email hybride :

  • En développement : si la variable SMTP_URL n'est pas définie dans votre .env, le lien magique de connexion ne sera pas envoyé par email, mais sera affiché dans la console de votre terminal. Vous pourrez cliquer dessus pour vous connecter.
  • En production : la variable SMTP_URL devient obligatoire. Le serveur refusera de démarrer sans elles pour s'assurer que l'application est bien capable de relayer les emails aux véritables utilisateurs.
const magicLink = `${trustedBaseUrl}/verify?token=${rawToken}`;

if (transporter) {
  // Le transporteur a pu être configuré avec la variable SMTP_URL du .env
  await transporter.sendMail({
    from: "starter@mail.com",
    to: email,
    subject: "Lien de connexion",
    html: `<a href="${magicLink}">Cliquez ici pour vous connecter</a>`,
  });
} else {
  // Fallback de développement : affiche le lien dans la console
  console.info(`Magic Link for ${email}:`);
  console.info(magicLink);
}

Vérification et session (verifyMagicLink)

Lorsque l'utilisateur clique sur le lien :

  1. Le client envoie le jeton au serveur.
  2. Le serveur compare le hachage reçu avec celui en base.
  3. Si valide, il génère un JWT de session et le place dans un cookie sécurisé :
const cookieOptions: CookieOptions = {
  httpOnly: true,
  secure: true,
  sameSite: "strict",
  maxAge: 30 * 24 * 60 * 60 * 1000, // 30 jours
};

res.cookie("__Host-auth", sessionToken, cookieOptions);

Middleware verifyAccessToken et req.me

Certaines routes nécessitent d'identifier l'utilisateur. StartER injecte l'entité User complète dans req.me.

const verifyAccessToken: RequestHandler = async (req, res, next) => {
  const token = req.cookies["__Host-auth"];
  const payload = auth.verify(token); // Vérifie la signature du JWT

  // Récupère l'utilisateur depuis la base de données
  const me = await userRepository.find(Number(payload.sub));

  if (me == null) throw new Error("User not found");

  req.me = me; // Injecte l'utilisateur dans la requête

  next();
};

Cela sécurise l'application : si un utilisateur est supprimé de la base, son JWT devient instantanément invalide même s'il n'a pas expiré.

Côté client : React

La logique est centralisée dans src/react/components/auth/AuthContext.tsx.

Flux de connexion

  1. MagicLinkForm : Saisie de l'email et appel à /api/auth/magic-link.
  2. VerifyPage : Route /verify qui récupère le token dans l'URL et l'envoie à /api/auth/verify.

Persistance de la session

Au chargement de l'application, le routeur React (via la fonction loader de la route racine) appelle /api/me. Si le cookie est présent et valide, l'utilisateur est récupéré et automatiquement injecté dans le composant AuthProvider.

// src/react/routes.tsx
export const routes = [
  {
    loader: async ({ request }) => {
      // Sur le serveur : transfert explicite des cookies
      const response = await fetch("/api/me", {
        headers: { cookie: request.headers.get("cookie") ?? "" },
      });
      const me = response.ok ? await response.json() : null;
      return { me };
    },
    Component: () => {
      const { me } = useLoaderData<{ me: User | null }>();
      return <AuthProvider initialUser={me}>...</AuthProvider>;
    }
  }
];

Bonnes pratiques et sécurité

  1. Jetons à usage unique : une fois vérifié, le jeton de Magic Link est marqué comme "consommé" en base de données.
  2. Hachage des jetons : les jetons opaques ne sont jamais stockés en clair (SHA-256).
  3. Cookies HTTP-only : le JWT de session est invisible pour le JavaScript, protégeant contre les vols de session via XSS.
  4. Trusted Identity (req.me) : utiliser l'objet req.me dans vos controllers garantit que vous manipulez un utilisateur existant et à jour.

Voir aussi

Clone this wiki locally