Guide Senior pour découpler Symfony (API) et Svelte (SPA) : Apprenez l'authentification par Cookies HttpOnly, le traitement CSRF, et la configuration des endpoints /api/* avec des snippets Vite, Nginx et curl.
Développement Back-end Développement Web

Symfony API & Svelte : Auth Cookies HttpOnly et Endpoints

Le front (Svelte/React/Vue) découplé du back (Symfony) est un standard. Le sujet n’est plus « comment appeler l’API », mais comment le faire proprement, en prod, sans surfaces d’attaque inutiles.

Ce guide donne une convention d’URL simple, deux stratégies d’auth claires (session vs JWT en cookie), le CSRF correctement traité, et des snippets prêts à coller (Symfony, Vite, Nginx, curl).

1 – Convention des endpoints : /api/* ou rien

  • Tous les endpoints de données commencent par /api/ (ex. /api/users, /api/analyse).
  • Avantages : sécurité (firewall dédié), cache/ETag séparés, logs distincts, règles CORS ciblées, reverse‑proxy plus simple.

Règle nginx/Apache : toutes les requêtes hors /api/* servent le index.html de la SPA. Les /api/* vont vers public/index.php (Symfony).

2 – Cookies HttpOnly : le bon choix en même domaine

Quand front et back partagent le même sous‑domaine, préférez un cookie HttpOnly plutôt que du stockage côté client (localStorage).

Attributs recommandés :

  • HttpOnly + Secure + Path=/api
  • SameSite=Lax (pratique) et CSRF token pour les méthodes mutantes (POST/PUT/PATCH/DELETE).
  • TTL court (ex. 15 min) + refresh (rotation) → voir §6.

SameSite=Strict casse trop de flows légitimes (deeplinks, retours). Lax + vrai CSRF = combo robuste.

3 – Dev : CORS ciblé + proxy de dev

En dev, origines différentes (ex. Vite :5173 / Symfony :8000).

3.1 CORS côté Symfony (Nelmio)


nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET','POST','PUT','PATCH','DELETE','OPTIONS']
allow_headers: ['Content-Type','Authorization',
                'X-CSRF-Token','X-Requested-With']
expose_headers: ['Link']
max_age: 3600
paths:
'^/api/':
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET','POST','PUT','PATCH','DELETE','OPTIONS']
allow_headers: ['Content-Type','Authorization',
                'X-CSRF-Token','X-Requested-With']
allow_credentials: true # indispensable pour envoyer les cookies

# .env.local (dev)
CORS_ALLOW_ORIGIN=^https?://(localhost|127\.0\.0\.1):(3000|5173)$

3.2 Proxy côté Vite/Svelte


// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:8000',
        changeOrigin: true,
        secure: false,
      },
    },
  },
}
            

4 – Client Svelte : wrapper fetch (cookies + CSRF + 401


// src/lib/api.js
let csrfToken = null;

async function ensureCsrf() {
  if (!csrfToken) {
    const r = await fetch('/api/auth/csrf', { credentials: 'include' });
    if (!r.ok) throw new Error('CSRF fetch failed');
    const { token } = await r.json();
    csrfToken = token;
  }
  return csrfToken;
}

export async function api(path, { method = 'GET', body, headers = {} } = {}) {
  const opts = {
    method,
    credentials: 'include', // envoie le cookie HttpOnly
    headers: { 'Accept': 'application/json', ...headers }
  };
if (body !== undefined) {
  opts.headers['Content-Type'] = 'application/json';
  // token CSRF uniquement sur méthodes mutantes
  if (['POST','PUT','PATCH','DELETE'].includes(method.toUpperCase())) {
    const t = await ensureCsrf();
    opts.headers['X-CSRF-Token'] = t;
  }
  opts.body = JSON.stringify(body);
}
const res = await fetch(path.startsWith('/api') ? path : `/api${path}`, opts);
if (res.status === 401) {
  // TODO: rediriger vers /login, rafraîchir le token, etc.
}
if (!res.ok) {
  const problem = await res.json().catch(() => ({}));
  throw Object.assign(new Error(problem.detail || 'API error'), { problem, status: res.status });
}
if (res.status === 204) return null;
  return res.json();
}
            

5) Security.yaml : choisis l’une des deux options

Option A — Session en cookie (étatful, simple, efficace)

  • stateless: false
  • CSRF via CsrfTokenManager ou token custom X-CSRF-Token.

# config/packages/security.yaml
security:
  firewalls:
    api:
      pattern: ^/api/
      stateless: false
      lazy: true
      user_checker: App\Security\UserChecker
      # Exemple avec authenticator JSON
      custom_authenticators:
        - App\Security\JsonLoginAuthenticator
      logout:
        path: /api/auth/logout
        invalidate_session: true
  access_control:
    - { path: ^/api/auth/(login|csrf)$, roles: PUBLIC_ACCESS }
    - { path: ^/api/, roles: ROLE_USER }
    

Option B — JWT en cookie (stateless)

  • stateless: true
  • CSRF obligatoire sur méthodes mutantes
  • Couple access (court) / refresh (plus long) + rotation

# config/packages/security.yaml
security:
  firewalls:
    api:
      pattern: ^/api/
      stateless: true
      custom_authenticators:
        - App\Security\JwtCookieAuthenticator
  access_control:
    - { path: ^/api/auth/(login|refresh|csrf)$, 
        roles: PUBLIC_ACCESS }
    - { path: ^/api/, roles: ROLE_USER }
    

Dans les deux cas, impose Accept: application/json et retourne des erreurs RFC 7807 (Problem Details).

Exemple de Problem Details :


{
  "type": "about:blank",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Authentication required",
  "instance": "/api/users/me"
}
    

6 – Endpoints d’auth clairs (login / refresh / logout / csrf)

  • POST /api/auth/login → set cookie HttpOnly (Path=/api, Secure, SameSite=Lax) + renvoie le profil minimal.
  • POST /api/auth/refresh → rotation des tokens (JWT) ou prolongation de session (rolling) ; protège contre le vol de refresh (stockage côté serveur, liste de révocation courte).
  • POST /api/auth/logoutinvalidate_session + suppression cookie (Max-Age=0).
  • GET /api/auth/csrf → renvoie un token CSRF (JSON) consommable par le header X-CSRF-Token.

7 – Reverse‑proxy (Nginx) : séparation SPA / API


# /api -> Symfony
location ^~ /api/ {
  proxy_pass http://127.0.0.1:9000;   # PHP-FPM via fastcgi_pass si configuré, ou backend
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

# tout le reste -> SPA (SvelteKit static/adapter)
location / {
  try_files $uri $uri/ /index.html;
}

Pensez à bloquer l’accès aux dossiers sensibles (/var, /config, backups…).

8 – Cache & ETag

  • Sur /api/* GET : ETag/Last-Modified + Cache-Control: private, must-revalidate.
  • Sur POST/PUT/PATCH/DELETE : Cache-Control: no-store.
  • Sur assets SPA (hors /api) : fingerprint + Cache-Control: public, max-age=31536000, immutable.

9 – Tests manuels rapides (sans Postman)


# 1) login et garde les cookies
curl -i -c /tmp/c.jar -X POST \
  -H 'Accept: application/json' -H 'Content-Type: application/json' \
  --data '{"email":"user@example.com","password":"secret"}' \
  https://example.com/api/auth/login

# 2) récupère le CSRF
curl -b /tmp/c.jar -c /tmp/c.jar -H 'Accept: application/json' \
  https://example.com/api/auth/csrf
# -> { "token": "..." }

# 3) appel protégé avec CSRF (méthode mutante)
curl -b /tmp/c.jar -H 'Accept: application/json' -H 'Content-Type: application/json' \
  -H 'X-CSRF-Token: __TOKEN__' \
  --data '{"title":"Nouvelle ressource"}' \
  https://example.com/api/items

# 4) refresh (si JWT)
curl -b /tmp/c.jar -X POST -H 'Accept: application/json' \
  https://example.com/api/auth/refresh

# 5) logout
curl -b /tmp/c.jar -X POST -H 'Accept: application/json' \
  https://example.com/api/auth/logout

10) Check‑list mise en prod (copy/paste)

  1. HTTPS + HSTS obligatoires.
  2. Cookies: HttpOnly + Secure + SameSite=Lax + Path=/api.
  3. CSRF sur POST/PUT/PATCH/DELETE (/api/auth/csrf → header X-CSRF-Token).
  4. security.yaml:

    • Session → stateless: false
    • JWT cookie → stateless: true + CSRF obligatoire.
  5. Rate limiter sur /api/auth/login (+ Retry-After).
  6. CORS OFF en prod (même domaine). En dev: origin regex + allow_credentials: true.
  7. Nginx split : /api/* → Symfony, le reste → SPA (index.html).
  8. Cache: GET /api = ETag ; mutations = no-store ; assets = immutable.
  9. Erreurs: Problem Details (RFC7807) ; 401403.
  10.  
  11. Versioning: /api/v1 si public/multi-clients.

Smoke test (3 commandes)


# login (cookies)
curl -i -c c.jar -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' \
  --data '{"email":"u@x.com","password":"secret"}' https://app.tld/api/auth/login
# csrf
curl -b c.jar -H 'Accept: application/json' https://app.tld/api/auth/csrf
# POST protégé
curl -b c.jar -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' \
  -H 'X-CSRF-Token: __TOKEN__' --data '{"title":"ok"}' https://app.tld/api/items


Conclusion

Pas de mode : du contexte. Même domaine → cookies HttpOnly.
Session (statefull) si tu veux du simple/robuste, JWT (stateless) si tu as plusieurs clients hétérogènes (mobile, extensions, autres domaines) — mais avec CSRF et refresh propres.

Avec :

  • /api/* isolé,
  • cookies bien configurés,
  • CSRF traité sérieusement,
  • proxy propre,
  • et snippets prod,

Voilà une base maintenable, sécurisée, et prête pour l’échelle.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *