Aller au contenu
5 min de lecture Moustakime KIFIA

Le BFF qui échange un JWT API contre un cookie httpOnly

Pourquoi un BFF bien placé sert à la fois de frontière de sécurité web et de couche d'adaptation de données pour le frontend.

  • Auth
  • Engineering
  • Integrations

Le sujet n’est pas “comment poser un cookie”. Le sujet, c’est où placer la vraie frontière de sécurité dans une application web qui parle à un BFF.

Quand le navigateur appelle directement un backend avec un token qu’il manipule lui-même, l’auth finit par diffuser dans trop d’endroits : code client, stockage local, helpers réseau, garde-fous UI, et parfois tests e2e.

Le pattern qui tient bien ici est plus simple à maintenir : le navigateur parle à des routes Next.js côté serveur, ces routes parlent au BFF, et le token d’API redevient un cookie httpOnly géré côté serveur.

Le problème à résoudre

Le problème n’est pas seulement de connecter un utilisateur. Le problème est de garder en même temps :

  • des frontends relativement simples
  • des tokens absents du JavaScript applicatif
  • une couche d’intégration claire entre app web, BFF et API métier
  • une session relisible de manière homogène sur les routes /api/*

Sans cette frontière, la complexité revient vite :

  • token dans localStorage ou en mémoire côté client
  • appels directs au BFF depuis le navigateur
  • duplication de la logique d’auth dans plusieurs pages ou hooks
  • surface XSS plus coûteuse à raisonner

La solution retenue

La solution retenue est de faire porter la connexion et le proxying par des routes Next.js server-side.

Le flux est volontairement simple :

  1. le navigateur poste ses identifiants à une route Next.js
  2. cette route appelle le BFF
  3. le BFF dialogue avec l’API métier
  4. la route Next.js écrit un cookie httpOnly
  5. les autres routes /api/* relisent ce cookie puis proxifient vers le BFF

Le point important n’est pas le cookie lui-même. Le point important, c’est que le navigateur n’a plus besoin de porter seul le contrat d’auth avec l’API.

Pourquoi cette solution tient bien dans ce type de plateforme

Ce pattern tient bien parce qu’il donne une frontière nette :

  • le navigateur porte l’UI et les formulaires
  • les routes Next.js portent la session web
  • le BFF porte l’adaptation entre frontend et API
  • l’API métier reste derrière une couche déjà authentifiée

Mais cette frontière ne suffit pas à elle seule à justifier un BFF.

Le deuxième rôle important est la mise en forme de la donnée pour le frontend.

Dans un environnement à plusieurs services, le front ne devrait pas avoir à connaître :

  • les différences de formats entre APIs
  • les conventions de nommage propres à chaque service
  • les appels multiples nécessaires pour construire une seule vue utile

Le BFF peut alors :

  • harmoniser les contrats
  • restructurer une réponse pour l’usage réel de l’UI
  • agréger plusieurs services
  • enrichir la donnée avant de la renvoyer

Autrement dit, le frontend ne consomme plus plusieurs services bruts. Il consomme une interface déjà adaptée à son besoin.

Dans ce modèle, un BFF ne vaut le coup que s’il absorbe une vraie différence de contrat ou de composition. Ici, c’est le cas : il absorbe la différence entre navigateur, routes Next.js et API métier, mais aussi une partie du travail d’adaptation que le frontend ne devrait pas porter.

Comment on l’implémente

Le premier point d’appui se trouve dans la route de login Platform :

const response = NextResponse.json({ status: "ok" }, { status });

response.cookies.set("authToken", data.token, {
  httpOnly: true,
  sameSite: "strict",
  secure: process.env.NODE_ENV === "production",
  path: "/",
  maxAge: 60 * 60 * 24 * 7
});

L’idée n’est pas de montrer beaucoup de code. L’idée est de rendre visible la responsabilité :

  • la route reçoit les credentials
  • elle appelle le BFF via fetchFromBff("/auth/login")
  • elle valide la réponse
  • elle transforme le JWT en cookie web

Le deuxième point d’appui intéressant est la variante passwordless dans apps/member/src/app/api/auth/login-with-code/route.ts.

On y retrouve le même geste architectural :

  • appel server-side au BFF
  • lecture de token ou access_token
  • écriture du cookie authToken

Cette variante est utile parce qu’elle montre que le pattern n’est pas lié à email/password. Il s’applique aussi à d’autres modes d’entrée, tant que la frontière reste la même.

Ce que la solution retire de la base

Le gain le plus important est souvent négatif : ce qu’on n’a plus besoin d’écrire.

Ce pattern retire :

  • des appels directs au backend depuis le client
  • des branches de code qui manipulent explicitement le JWT dans l’UI
  • une partie du risque lié au stockage du token dans le navigateur
  • des contrats d’auth répétés dans chaque client API
  • une partie de l’orchestration légère qui finit sinon dans le frontend

Il simplifie aussi la lecture de la session sur les routes applicatives. Une route /api/* peut relire request.cookies.get("authToken") et reconstruire un Authorization propre vers le BFF.

Et quand le BFF compose déjà la bonne réponse, le frontend évite aussi de multiplier les appels, les mappings et les états intermédiaires juste pour afficher une vue métier cohérente.

Le détail qui compte en staging

Ce type de solution paraît petit, mais ses détails d’exécution comptent.

Le bon exemple est le flag secure. En production, il est normal de vouloir un cookie secure. Mais dans des déploiements où TLS est terminé en amont, le raisonnement doit rester cohérent avec la chaîne réelle de transport.

On retrouve ce sujet dans les routes d’auth et dans les middlewares qui lisent x-forwarded-proto côté portail. Ce n’est pas un détail d’infra isolé : c’est une partie du design d’auth web.

Ce que je veux montrer à d’autres devs

Je ne veux pas montrer “un cookie de plus”. Je veux montrer un critère de décision.

Un BFF et des routes Next.js valent le coup quand ils :

  • retirent le token du JavaScript applicatif
  • rendent la session web plus lisible
  • centralisent le point de conversion entre auth API et auth web
  • laissent le frontend consommer une frontière plus simple
  • harmonisent les contrats de plusieurs services
  • renvoient une donnée déjà plus utile pour l’UI

S’ils n’absorbent aucune complexité réelle, ils ajoutent juste une couche.

Ici, ils absorbent deux responsabilités utiles : sécuriser la frontière web et présenter au frontend une donnée mieux structurée. C’est pour ça que le pattern mérite sa place.

Continuer la lecture

Quelques articles relies pour renforcer le maillage interne et prolonger les sujets techniques voisins.