Choix de session dans le navigateur
- Leçon 8
- 50 min
- Module 3 : Sessions et intégration à l'application
Leçon 8 sur 19 · Authentification, autorisations et applications multilocatairesChoix de session dans le navigateur
1. Bases de l'identité et des autorisations
2. Comprendre la connexion et les jetons
3. Sessions et intégration à l'application
4. Contrôler l'accès aux ressources
5. Travail en arrière-plan et administration
6. Valider et documenter la conception
Ce que vous saurez faire
Permettez aux gens de se connecter au service de soutien dans un navigateur, sans lui donner un jeton qu’un script pourrait voler. L’API garde les jetons sur le serveur et donne au navigateur seulement un témoin de session. Protégez ensuite ce témoin contre les requêtes lancées par d’autres sites.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Le flux de code d’autorisation avec PKCE et les trois jetons qu’il retourne, dans Flux d’application.
- Ce qu’est un jeton signé, et que quiconque le possède peut l’utiliser, dans Lire un jeton : JWT, signatures et clés.
- Les vérifications que l’API effectue sur chaque jeton d’accès, dans Ce que signifie un jeton.
Ce qu’il vous faut sur votre ordinateur
- Le projet après le module 2, dans
~/projects/ticket-api:python -m pytestaffiche181 passed(181 tests réussis). - PostgreSQL et Azurite en marche, avec les données des deux boutiques chargées, et le fournisseur d’exercice configuré avec
python -m idp init. - Le dépôt des exercices à côté du projet, dans
~/projects/nextia-learning-labs. Exécutez-ygit pullpour obtenir l’instantanéend-of-m03. - Un navigateur. Les étapes utilisent les outils de développement de Chrome; Firefox et Safari offrent les mêmes panneaux sous d’autres noms.
Vous n’avez besoin ni de compte ni d’argent. Testé sur macOS (Apple silicon) avec Python 3.12 et Chromium 156; les commandes Windows sont fournies, mais non testées.
Un bracelet à un festival
Section intitulée « Un bracelet à un festival »À un festival de musique, vous montrez votre billet une seule fois, à l’entrée. Le personnel le vérifie et fixe un bracelet à votre bras. Pendant le reste de la fin de semaine, vous montrez le bracelet au bar et devant la scène. Personne ne vous redemande le billet.
Certaines personnes font autrement. Elles gardent le billet papier dans une poche arrière et le montrent partout. Un voleur qui le prend peut l’utiliser à une autre entrée, car le billet fonctionne pour quiconque le possède. Le bracelet est plus sûr : il est fixé à votre bras, et personne ne peut l’enlever sans le couper.
Le bracelet a un point faible. Il fonctionne partout où va votre bras. Un inconnu peut tirer votre bras au-dessus du bar et commander une boisson. Le barman voit un bracelet valide et ajoute la boisson à votre facture. Un bar prudent demande donc aussi un mot de passe que le personnel vous a donné à l’entrée. L’inconnu qui tire votre bras ne le connaît pas.
Une preuve qu’on ne peut pas prendre, son point faible et un mot de passe : voilà les trois parties de cette leçon.
Le problème : un jeton est un billet papier
Section intitulée « Le problème : un jeton est un billet papier »À la fin du module 2, scripts.login a connecté Sam et affiché un jeton d’accès. Un programme en ligne de commande peut garder ce jeton en mémoire et l’envoyer dans l’en-tête Authorization. Le service de soutien a maintenant besoin d’une page dans un navigateur. Où cette page garde-t-elle les jetons?
Une réponse courante est le stockage local du navigateur (localStorage), un petit espace que le JavaScript de chaque site peut lire et modifier. La page y lit le jeton d’accès avant chaque requête. Elle y garde aussi le jeton de renouvellement pour obtenir de nouveaux jetons sans nouvelle connexion.
Cela fonctionne jusqu’à ce que le JavaScript de quelqu’un d’autre s’exécute dans la page. C’est le script intersite (XSS) : du texte provenant d’un attaquant arrive dans la page sous forme de code. Par exemple, un client écrit <script>…</script> dans le message d’un billet de soutien, et une page affiche le message sans l’échapper. Le script s’exécute alors avec les mêmes droits que le code de la page. Il peut lire tout ce que la page peut lire.
Voici un cas construit qui montre ce que cela signifie. L’auteur a connecté Sam avec scripts.login et placé son jeton d’accès dans localStorage sur la page de soutien dans Chromium. Puis l’auteur a exécuté une ligne, comme le ferait un script injecté :
localStorage.getItem("access_token") // 732 characters: Sam's access tokenEnsuite, l’auteur a utilisé la chaîne volée depuis un terminal, comme le ferait un attaquant depuis un autre ordinateur :
curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer <the 732 characters>" http://127.0.0.1:8000/v1/me200L’API a répondu comme prévu : le jeton est valide, donc l’appelant est Sam. L’API ne peut pas savoir que le jeton a quitté le navigateur. C’est un billet papier, et il fonctionne pour quiconque le possède. Le jeton d’accès dure 10 minutes, mais le jeton de renouvellement dans le même stockage crée de nouveaux jetons pendant 8 h.
L’idée : le serveur garde les jetons, le navigateur garde un bracelet
Section intitulée « L’idée : le serveur garde les jetons, le navigateur garde un bracelet »La solution consiste à garder les jetons complètement hors du navigateur. L’API de billets effectue elle-même la connexion, comme serveur de sa propre page. Elle échange le code contre les jetons, les garde sur le serveur et donne au navigateur seulement un identifiant de session aléatoire dans un témoin.
Un témoin (cookie) est une petite valeur qu’un site demande au navigateur de garder. Le navigateur la renvoie avec chaque requête à ce site, sans JavaScript. Une session est l’enregistrement d’une connexion sur le serveur : qui s’est connecté, quand et jusqu’à quand. Le témoin désigne la session. Cette architecture porte un nom : un backend for frontend (BFF), un serveur qui gère la sécurité de sa propre interface.
L’attribut HttpOnly du témoin joue le rôle du bracelet. Il dit au navigateur : envoyez ce témoin avec les requêtes, mais ne le montrez jamais à JavaScript. Un script injecté qui lit document.cookie reçoit une chaîne vide. Il n’y a aucun jeton à envoyer ailleurs, car la page n’en a jamais eu.
Description textuelle du diagramme
Dans la conception A, le jeton se trouve là où n’importe quel script de la page peut le lire. Dans la conception B, le navigateur détient seulement un témoin que JavaScript ne peut pas lire. Regardez maintenant la flèche pointillée en B. Un script injecté peut encore appeler l’API depuis la page ouverte, car le navigateur ajoute le témoin. Ainsi, HttpOnly limite les dommages à la page ouverte; il ne rend pas le XSS sans danger. Vous devez toujours échapper chaque valeur que vous affichez dans une page.
Un petit exemple : ce que chaque côté garde
Section intitulée « Un petit exemple : ce que chaque côté garde »Prenez une session fictive. La valeur du témoin est une chaîne aléatoire, par exemple larkfield-practice-cookie-0001 (une vraie valeur contient 43 caractères aléatoires). L’API ne stocke pas cette valeur. Elle stocke son SHA-256, le même type d’empreinte que dans PKCE :
| Qui garde quoi | Valeur |
|---|---|
Le navigateur : témoin ticket_session |
larkfield-practice-cookie-0001 |
L’API : sessions.session_sha256 |
4915b043c9618e3f1de7985beec1ad89f6dba7dc527a4fb816f423b6d03bc527 |
L’API : sessions.refresh_token_encrypted |
octets que seule SESSION_KEY peut déchiffrer |
Un script dans la page : document.cookie |
"" |
Pourquoi l’empreinte? Une copie de la table sessions, provenant par exemple d’une sauvegarde, donne 64 caractères hexadécimaux par session. On ne peut pas les reconvertir en témoin. L’API calcule le SHA-256 du témoin de chaque requête et cherche cette valeur.
Le point faible : le navigateur envoie le témoin tout seul
Section intitulée « Le point faible : le navigateur envoie le témoin tout seul »Le navigateur ajoute le témoin à chaque requête vers l’API, peu importe qui a lancé la requête. C’est le point faible du bracelet. Une page d’un autre site peut contenir un formulaire qui envoie POST http://127.0.0.1:8000/auth/logout dès qu’elle s’ouvre. Si le navigateur ajoutait le témoin de Sam, l’API déconnecterait Sam, et Sam ne saurait pas pourquoi. Avec les routes de billets du module 4, le même procédé pourrait ajouter un message au nom de Sam.
Cette attaque est la falsification de requête intersite (CSRF) : un autre site fait envoyer à votre navigateur une requête que votre témoin rend valide. L’attaquant ne voit jamais le témoin. Il profite seulement de l’habitude du navigateur de l’envoyer.
Deux verrous la bloquent, un dans le navigateur et un dans l’API :
SameSite=Laxsur le témoin. Le navigateur n’envoie pas le témoin avec unPOSTou unfetchlancé par un autre site. Il l’envoie quand même quand vous suivez un lien d’un autre site vers l’API, car c’est une visite normale. UnGETne doit donc jamais rien changer.- Un jeton CSRF, le mot de passe. À la connexion, l’API crée une deuxième valeur aléatoire pour la session. La page la lit depuis
GET /auth/sessionet l’envoie dans l’en-têteX-CSRF-Tokenavec chaque requête qui change quelque chose. Un autre site ne peut pas obtenir cette valeur, car le navigateur empêche un site de lire les réponses d’un autre. Il ne peut donc pas envoyer l’en-tête.
L’auteur a testé les quatre cas dans Chromium avec l’API d’exercice. L’autre site était une page construite sur http://localhost:8099 :
Description textuelle du diagramme
Les deux premières lignes montrent la décision du navigateur : cette requête reçoit-elle le témoin? Les deux dernières montrent la décision de l’API : le jeton CSRF est-il là aussi? La troisième ligne simule un navigateur qui a quand même envoyé le témoin, par exemple un vieux navigateur qui ignore SameSite. Le jeton CSRF le bloque quand même. Un fetch depuis l’autre site a aussi échoué (Failed to fetch, échec de la récupération), car l’API n’autorise aucun autre site dans ses paramètres CORS.
Larkfield : une page de connexion servie par l’API
Section intitulée « Larkfield : une page de connexion servie par l’API »Kwame, l’architecte, pose encore ses trois questions : qui fait l’appel, que peut cette personne faire, dans quelle organisation? Pour un navigateur, il en ajoute une quatrième : « Où conserve-t-on la preuve de la première réponse? » La réponse d’Amira est la conception présentée plus haut. L’API sert une petite page à /app/, à la même adresse que l’API. La page et l’API sont donc sur le même site. Les jetons ne quittent jamais le serveur.
Tomás demande : « Pourquoi ne pas garder le jeton dans une variable JavaScript? » Amira répond : « Un script injecté s’exécute dans la même mémoire. Il peut donc aussi lire la variable. Avec le témoin, il n’y a aucun jeton dans le navigateur. »
Exercice guidé : ajouter la connexion dans le navigateur
Section intitulée « Exercice guidé : ajouter la connexion dans le navigateur »Le code se trouve dans l’instantané end-of-m03. Copiez-le, puis lisez les parties importantes ci-dessous.
-
Dans
~/projects/ticket-api, avec l’environnement virtuel actif, copiez le code de session, la migration et les tests :Fenêtre de terminal S=../nextia-learning-labs/C23/snapshots/end-of-m03mkdir -p ticket_api/staticfor f in migrations/015_sessions.sql ticket_api/sessions.py ticket_api/login.py \ticket_api/static/app.html ticket_api/auth.py ticket_api/config.py \ticket_api/errors.py ticket_api/main.py ticket_api/repository.py \ticket_api/security.py tests/test_sessions.py tests/conftest.py \contract/openapi.json .env.example; do cp "$S/$f" "$f"; doneFenêtre de terminal $S = "..\nextia-learning-labs\C23\snapshots\end-of-m03"New-Item -ItemType Directory -Force ticket_api\static | Out-Nullforeach ($f in 'migrations\015_sessions.sql', 'ticket_api\sessions.py', 'ticket_api\login.py','ticket_api\static\app.html', 'ticket_api\auth.py', 'ticket_api\config.py','ticket_api\errors.py', 'ticket_api\main.py', 'ticket_api\repository.py','ticket_api\security.py', 'tests\test_sessions.py', 'tests\conftest.py','contract\openapi.json', '.env.example') { Copy-Item "$S\$f" $f }Vous avez copié 9 fichiers modifiés et 5 nouveaux fichiers. Le nouveau code ne demande aucun nouveau paquet : le chiffrement vient du paquet
cryptography, que PyJWT a déjà installé. -
Ajoutez quatre lignes à
.env. Ouvrez le fichier dans votre éditeur et ajoutez-les à la fin :Fenêtre de terminal OIDC_CLIENT_ID=help-desk-webOIDC_REDIRECT_URI=http://127.0.0.1:8000/auth/callbackPOST_LOGOUT_REDIRECT_URI=http://127.0.0.1:8000/app/SESSION_COOKIE_SECURE=falseLes trois premières décrivent l’inscription de l’API chez le fournisseur; Configuration du fournisseur explique chacune d’elles. La dernière ligne sert seulement sur votre ordinateur. Le cas d’échec ci-dessous montre pourquoi.
OIDC_CLIENT_SECRETetSESSION_KEYsont déjà dans.env:python -m idp initles a écrites au module 2. -
Appliquez la nouvelle migration. Elle ajoute deux tables,
login_requestsetsessions:Fenêtre de terminal python -m ticket_api.migrateapplied 015_sessionsDatabase is up to date (15 migrations). -
Exécutez les tests :
Fenêtre de terminal python -m pytest193 passed, 1 deselectedLes 12 nouveaux tests dans
tests/test_sessions.pyjouent le rôle du navigateur et suivent chaque redirection manuellement. -
Démarrez le fournisseur dans un terminal et l’API dans un deuxième, avec l’environnement virtuel actif dans les deux. Si le fournisseur fonctionne encore depuis le module 2, gardez-le. Si l’API fonctionne, arrêtez-la avec Ctrl+C et redémarrez-la pour qu’elle lise les nouvelles lignes de
.env:Fenêtre de terminal python -m idpFenêtre de terminal fastapi dev -
Ouvrez http://127.0.0.1:8000/app/ et choisissez Sign in (se connecter). La page du fournisseur nomme l’application « Help desk (web) » (service de soutien web) et indique ses portées. Choisissez Sam. Vous revenez à
/app/:
La page du service de soutien après la connexion de Sam, dans Chromium. La page a appelé /v1/me avec seulement le témoin; elle n'a aucun jeton à envoyer. L’image est en anglais. -
Ouvrez les outils de développement (F12 sur Windows, Cmd+Option+I sur macOS). Choisissez Application, puis Cookies (témoins) et
http://127.0.0.1:8000. Trouvezticket_session. La colonne HttpOnly est cochée, et SameSite indiqueLax. -
Dans la Console des mêmes outils, tapez
document.cookieet appuyez sur Entrée. La réponse est'', une chaîne vide. Tapez ensuiteawait (await fetch('/v1/me')).json(). Vous obtenez la réponse de Sam : la page peut utiliser la session, mais ne peut pas la voir.
Voir chaque étape avec curl
Section intitulée « Voir chaque étape avec curl »Le navigateur cache les redirections, tandis que curl les montre. Gardez les deux programmes actifs et utilisez un fichier de témoins : curl y garde les témoins, comme un navigateur. Chaque commande affiche une valeur intermédiaire.
TO=$(curl -s -o /dev/null -w '%{redirect_url}' -c jar.txt "http://127.0.0.1:8000/auth/login?return_to=/app/")echo "$TO"BACK=$(curl -s -o /dev/null -w '%{redirect_url}' -b jar.txt -c jar.txt -d user=usr-sam "$TO")echo "$BACK"curl -s -i -b jar.txt -c jar.txt "$BACK"curl -s -i -b jar.txt http://127.0.0.1:8000/v1/me$to = curl.exe -s -o NUL -w '%{redirect_url}' -c jar.txt "http://127.0.0.1:8000/auth/login?return_to=/app/"$to$back = curl.exe -s -o NUL -w '%{redirect_url}' -b jar.txt -c jar.txt -d user=usr-sam "$to"$backcurl.exe -s -i -b jar.txt -c jar.txt "$back"curl.exe -s -i -b jar.txt http://127.0.0.1:8000/v1/meLa sortie, avec les longues valeurs aléatoires de la première ligne abrégées (…) et les en-têtes date et server omis :
http://localhost:8400/authorize?response_type=code&client_id=help-desk-web&redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2Fauth%2Fcallback&scope=openid+profile+email+offline_access+tickets%3Aread+tickets%3Awrite+members%3Amanage&state=-NISkOf_…&nonce=AyxDqtfT…&code_challenge=U-CpPRKr…&code_challenge_method=S256http://127.0.0.1:8000/auth/callback?code=tpddqllbFgemr2MCH2PyyLqTNGbuH-x5nN0sbpESlE4&state=hlsGqhUFC8usBnSZEoIoYTg1nkXNDBe4HTTP/1.1 302 Foundlocation: /app/set-cookie: ticket_session=p2nTgBlu45F5TGrc7vJBRszoLlKCf0BjfNNhbs_ntvk; HttpOnly; Max-Age=28800; Path=/; SameSite=laxHTTP/1.1 200 OKcontent-type: application/jsoncache-control: no-store
{"user_id":"usr-sam","kind":"user","name":"Sam","platform_role":null,"client_id":"help-desk-web","scopes":["members:manage","tickets:read","tickets:write"],"token_expires_at":"2026-10-10T14:31:02.535232Z","memberships":[{"tenant_id":"larkfield","tenant_name":"Larkfield","role":"staff"}]}La première ligne est l’adresse que l’API a construite, avec state, nonce et le défi PKCE de Flux d’application. La deuxième ligne est la réponse du fournisseur à -d user=usr-sam, le clic sur Sam : seulement un code à usage unique. Le rappel a échangé le code sur le serveur. Le navigateur reçoit donc seulement Set-Cookie. Max-Age=28800 correspond à 8 heures; Secure est absent à cause de SESSION_COOKIE_SECURE=false. La dernière réponse vient de /v1/me, avec le témoin et sans jeton. Pour une session, token_expires_at indique la fin de la session. cache-control: no-store demande au navigateur de ne pas garder une copie de la réponse d’une personne.
Ouvrez jar.txt dans votre éditeur. Il contient deux lignes qui commencent par #HttpOnly_ : une pour 127.0.0.1 (ticket_session) et une pour localhost (idp_session, le témoin du fournisseur). La prochaine leçon montre pourquoi ce deuxième témoin est important.
Testez maintenant le mot de passe convenu. Déconnectez-vous sans lui, puis avec lui :
curl -s -X POST -b jar.txt http://127.0.0.1:8000/auth/logoutCSRF=$(curl -s -b jar.txt http://127.0.0.1:8000/auth/session | python -c "import sys, json; print(json.load(sys.stdin)['csrf_token'])")curl -s -X POST -b jar.txt -c jar.txt -H "X-CSRF-Token: $CSRF" http://127.0.0.1:8000/auth/logoutcurl.exe -s -X POST -b jar.txt http://127.0.0.1:8000/auth/logout$csrf = (curl.exe -s -b jar.txt http://127.0.0.1:8000/auth/session | ConvertFrom-Json).csrf_tokencurl.exe -s -X POST -b jar.txt -c jar.txt -H "X-CSRF-Token: $csrf" http://127.0.0.1:8000/auth/logout{"error":{"code":"csrf_failed","message":"This request needs the X-CSRF-Token header of your session.","request_id":"36f61abb4c6a","fields":[]}}{"signed_out":true,"provider_logout_url":"http://localhost:8400/logout?post_logout_redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2Fapp%2F"}La première requête avait un témoin valide, mais aucun mot de passe convenu : 403 csrf_failed (échec de la vérification CSRF). La deuxième avait les deux. À la fin du cours, la même vérification protège chaque route qui modifie un billet. Un nouveau message avec le témoin, mais sans jeton CSRF, reçoit alors 403; avec le jeton, il reçoit 201.
Lire le code qui fait ce travail
Section intitulée « Lire le code qui fait ce travail »Trois endroits du code copié mettent en œuvre la conception. Ouvrez chacun d’eux.
ticket_api/login.py,set_cookie():httponly=True,secure=vient deSESSION_COOKIE_SECURE,samesite="lax". Un commentaire sur chaque ligne indique ce qu’elle bloque.ticket_api/sessions.py,create(): une nouvelle valeur aléatoire pour chaque connexion; la table garde seulement son empreinte SHA-256. Supposons que l’API réutilise une valeur fournie par le navigateur. Un attaquant pourrait alors placer une valeur et attendre que la victime se connecte (fixation de session).ticket_api/security.py,current_identity(): l’API vérifie un jeton porteur comme au module 2. Sans jeton, l’API utilise le témoin. Toute méthode autre queGET,HEADouOPTIONSdoit aussi fournirX-CSRF-Token.
Vous pouvez voir le côté serveur dans la base de données. Dans un terminal, dans le dossier du projet :
docker compose exec db psql -U tickets -c "SELECT left(session_sha256, 16) AS sha256_start, user_id, refresh_token_encrypted IS NOT NULL AS refresh_kept, end_reason FROM sessions ORDER BY created_at DESC LIMIT 1;" sha256_start | user_id | refresh_kept | end_reason------------------+---------+--------------+------------ f1410dbe80b1701d | usr-sam | t | logout(1 row)La ligne garde la déconnexion et sa raison, mais pas le témoin.
À vous de jouer : envoyer Sam ailleurs
Section intitulée « À vous de jouer : envoyer Sam ailleurs »La connexion commence à /auth/login?return_to=/app/, et le rappel envoie le navigateur à return_to. Un attaquant pourrait envoyer à Sam un lien vers la vraie connexion avec return_to=https://evil.example/. Après une vraie connexion, Sam arriverait sur une page qui ressemble seulement au service de soutien. Cette erreur est une redirection ouverte.
Prédisez où le rappel envoie le navigateur. Puis exécutez encore la séquence curl avec return_to=https://evil.example/ dans la première commande. Lisez la ligne location (destination) du rappel.
Afficher la réponse
location: /app/safe_return_to() dans login.py accepte seulement un chemin sur ce site. Il doit commencer par /, et non par //, et ne contenir aucun \. Toute autre valeur devient /app/. Le test test_return_to_cannot_send_you_to_another_site le vérifie.
Exécutez aussi la commande de rappel (la troisième) une deuxième fois. La réponse est 400 login_failed (échec de la connexion) : « Cette connexion est inconnue, a déjà été utilisée ou est trop ancienne. Recommencez. » Chaque state fonctionne une seule fois, pendant 10 minutes.
Quand utiliser une session côté serveur, quand l’éviter et ce qu’elle coûte
Section intitulée « Quand utiliser une session côté serveur, quand l’éviter et ce qu’elle coûte »Utilisez-la quand votre propre site sert les pages et l’API, comme ici : un service de soutien, un outil interne. Chaque jeton reste sur le serveur, et vous contrôlez la déconnexion et l’expiration. Ne l’utilisez pas pour un programme qui n’est pas un navigateur. Le client en ligne de commande et une application mobile gardent eux-mêmes leurs jetons et envoient Authorization: Bearer. L’API accepte les deux.
Les coûts : le serveur garde un état (une ligne par connexion, une lecture et une écriture en base par requête). Chaque route qui change quelque chose demande aussi la vérification CSRF. Une interface sur un autre site demande CORS et SameSite=None, ce qui retire le premier verrou. Cette conception dépasse le cadre de ce cours.
Erreur courante
La connexion fonctionne dans Chrome et échoue dans Safari
Symptôme : vous choisissez Sam sur la page du fournisseur et revenez à /app/, mais la page indique « You are not signed in. » (vous n’êtes pas connecté). Dans Chrome et Firefox, les mêmes étapes fonctionnent.
Cause : la ligne SESSION_COOKIE_SECURE=false manque dans .env, alors l’API utilise sa valeur par défaut, true. Un témoin Secure (sécurisé) peut seulement circuler par HTTPS. Chrome et Firefox font une exception pour 127.0.0.1 et localhost; WebKit, le moteur de Safari, n’en fait pas. L’auteur a fait le test avec SESSION_COOKIE_SECURE=true sur http://127.0.0.1. Chromium 156 et Firefox 157 ont permis la connexion. WebKit 27.2 a répondu {"signed_in":false}.
Diagnostic : ouvrez le rappel dans le panneau Network (réseau) des outils de développement. Son en-tête set-cookie se termine par ; Secure, et le navigateur indique que le témoin est bloqué. python -m scripts.check_identity, de Configuration du fournisseur, affiche PROBLEM cookie Secure=True, redirect URI over HTTP (problème : témoin Secure=True, URI de redirection sur HTTP).
Correction :
- Ajoutez
SESSION_COOKIE_SECURE=falseà.env. - Arrêtez
fastapi devavec Ctrl+C et redémarrez-le pour qu’il lise.env. - Connectez-vous de nouveau.
Sur un serveur, faites l’inverse : servez l’API par HTTPS et gardez la valeur par défaut true. Toute personne sur le même réseau peut lire un témoin de session qui circule par HTTP non chiffré.
Vérifiez votre compréhension
Un script injecté s'exécute dans la page /app/ pendant que Sam est connecté. Que peut-il emporter, et que peut-il encore faire? Comparez avec une page qui garde les jetons dans localStorage.
Afficher la réponse
Avec la session côté serveur, il ne peut rien emporter : le témoin est HttpOnly, donc document.cookie est vide. La page n’a aucun jeton. Mais il peut encore envoyer des requêtes depuis la page ouverte avec le témoin de Sam, jusqu’à sa fermeture ou à la fin de la session. Avec des jetons dans localStorage, le script envoie les jetons d’accès et de renouvellement ailleurs. L’attaquant les utilise ensuite depuis n’importe quel ordinateur. Dans le test construit, curl a reçu 200 comme Sam avec le jeton volé.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Prédisez : une page d'un autre site envoie un formulaire avec POST /auth/logout à l'API de billets pendant que Sam est connecté. Que répond l'API, et pourquoi? Que répondrait-elle si le navigateur ignorait SameSite?
Afficher la réponse
401 not_signed_in (non connecté), et Sam reste connecté. Le témoin a SameSite=Lax, donc le navigateur ne l’envoie pas avec un POST lancé par un autre site. L’API ne voit donc aucune session. Si le navigateur envoyait quand même le témoin, l’API répondrait 403 csrf_failed, car la requête n’a pas d’en-tête X-CSRF-Token. L’autre site ne peut pas obtenir le jeton CSRF : il se trouve seulement dans la réponse de GET /auth/session. Le navigateur empêche un autre site de lire cette réponse.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Exercice pratique : montrez que le navigateur a le témoin de session, que le JavaScript de la page ne peut pas le lire et que la base de données ne le contient pas.
Afficher la réponse
Dans les outils de développement, Application → Cookies → http://127.0.0.1:8000 affiche ticket_session avec HttpOnly coché et SameSite à Lax. Dans la Console, document.cookie renvoie '', tandis que fetch('/v1/me') renvoie encore la réponse de Sam. Dans psql, SELECT left(session_sha256, 16), user_id FROM sessions montre une empreinte hexadécimale, pas la valeur du témoin. Le témoin a 43 caractères; session_sha256 a 64 caractères hexadécimaux qui ne permettent pas de retrouver le témoin.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Les jetons dans le stockage du navigateur sont des billets en papier : un script injecté peut les envoyer ailleurs. Un jeton d’accès volé a obtenu 200 comme Sam depuis curl.
- Un serveur pour l’interface garde les jetons sur le serveur. Le navigateur reçoit le témoin
ticket_session: HttpOnly, SameSite=Lax, 8 heures, Secure sur un serveur. La table garde son empreinte SHA-256. - Les témoins circulent automatiquement, donc les changements demandent une protection CSRF : SameSite=Lax et l’en-tête
X-CSRF-Token(403 csrf_failedsans lui). return_toaccepte seulement un chemin local, et chaquestatefonctionne une seule fois.- Sur votre ordinateur,
SESSION_COOKIE_SECURE=false: le moteur de Safari refuse un témoin Secure sur HTTP non chiffré.
La session démarre maintenant correctement. La prochaine leçon la suit jusqu’à sa fin : expiration, renouvellement, déconnexion, compte désactivé et deuxième personne dans le même navigateur.
Leçon suivante : Cycle de vie d'une session