Ga naar hoofdinhoud

Cookies afschermen: uitloggen

Een sessie hoort niet eeuwig te duren. Hij eindigt op twee manieren: vanzelf, als de cookie verloopt na max_age seconden, of als de bezoeker uitlogt. Bij uitloggen doet je server twee dingen: hij gooit de sessie weg in sessies.db, en hij haalt de cookie weg bij de bezoeker.

Een endpoint om uit te loggen​

Zet dit endpoint onder de andere in main.py:

@app.post("/uitloggen")
async def uitloggen(sessie_id: str = Cookie(default="")):
with SqliteDict("sessies.db") as sessies:
if sessie_id in sessies:
del sessies[sessie_id]
sessies.commit()
antwoord = JSONResponse({"bericht": "Uitgelogd"})
antwoord.delete_cookie("sessie_id")
return antwoord
  1. Regel 2:

    Het sessie-id uit de cookie, net als bij verwijderen.

  2. Regel 3-6:

    De sessie gaat weg op de server. Daarna hoort dit sessie-id bij niemand meer.

  3. Regel 8:

    delete_cookie zegt tegen de browser: gooi deze cookie weg.

Doe de test​

Een script uitloggen.py plaatst als Sara een bericht, logt uit, en probeert daarna haar bericht te verwijderen:

import httpx

adres = "http://127.0.0.1:8000"

sara = httpx.Client(base_url=adres)
sleutel = sara.post("/bericht", data={"naam": "Sara", "bericht": "Hoi"}).json()["sleutel"]
print("Cookie voor uitloggen:", sara.cookies.get("sessie_id") is not None)
print(sara.post("/uitloggen").json())
print("Cookie na uitloggen:", sara.cookies.get("sessie_id"))
print("Verwijderen:", sara.delete(f"/bericht/{sleutel}").status_code)

Uitvoer:

Cookie voor uitloggen: True
{'bericht': 'Uitgelogd'}
Cookie na uitloggen: None
Verwijderen: 403

Na het uitloggen heeft Sara geen cookie meer, en de server kent haar sessie niet meer. Haar bericht verwijderen lukt niet: voor de server is ze een nieuwe bezoeker.

Er gaat iets mis​

Na het uitloggen is de cookie weg, maar de sessie staat nog in sessies.db.

Oorzaak: het endpoint doet alleen delete_cookie. De browser gooit de cookie weg, maar voor de server is de sessie nog geldig. Wie dat sessie-id nog heeft, bijvoorbeeld omdat het was uitgelezen voordat je httponly gebruikte, komt er nog steeds mee binnen.

Oplossing: gooi altijd ook de sessie op de server weg, zoals in regel 3 tot en met 6.

Opdrachten​

Stuur een verzoek naar /uitloggen en kijk in het tabblad Netwerk naar de regel set-cookie in het antwoord. Hoe vertelt de server de browser dat de cookie weg moet?

Antwoord
sessie_id=""; expires=…; Max-Age=0; Path=/; SameSite=lax

De server zet de cookie opnieuw, met een lege waarde en Max-Age=0: houdbaar voor nul seconden. De browser gooit hem dan meteen weg. Een apart commando om een cookie te verwijderen bestaat niet.

Opdracht 2: Predict - Uitloggen zonder in te loggen​

Een nieuwe bezoeker, zonder cookie, stuurt POST /uitloggen.

Vraag: wat antwoordt de server?

Antwoord

{'bericht': 'Uitgelogd'}. Het sessie-id is leeg, dus if sessie_id in sessies is niet waar en er gaat niets weg. Daarna haalt delete_cookie een cookie weg die er niet was. Er gaat niets mis: uitloggen als je niet bent ingelogd, doet niets.

Zo ziet je main.py er nu uit
import secrets
import time

from fastapi import Cookie, FastAPI, Form, HTTPException
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from sqlitedict import SqliteDict

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")

@app.post("/bericht")
async def bericht_plaatsen(
naam: str = Form(...),
bericht: str = Form(...),
sessie_id: str = Cookie(default=""),
):
if not sessie_id:
sessie_id = secrets.token_hex(16)

sleutel = f"bericht_{time.time_ns()}"
with SqliteDict("gastenboek.db") as db:
db[sleutel] = {"naam": naam, "bericht": bericht}
db.commit()

with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {"naam": naam, "berichten": []})
mijn["naam"] = naam
mijn["berichten"].append(sleutel)
sessies[sessie_id] = mijn
sessies.commit()

antwoord = JSONResponse({"sleutel": sleutel})
antwoord.set_cookie(
key="sessie_id",
value=sessie_id,
max_age=60 * 60 * 24 * 30,
httponly=True,
)
return antwoord

@app.get("/berichten")
async def berichten():
with SqliteDict("gastenboek.db") as db:
return dict(db)

@app.delete("/bericht/{sleutel}")
async def bericht_verwijderen(sleutel: str, sessie_id: str = Cookie(default="")):
with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {})
if sleutel not in mijn.get("berichten", []):
raise HTTPException(status_code=403, detail="Dit is niet jouw bericht")

with SqliteDict("gastenboek.db") as db:
if sleutel in db:
del db[sleutel]
db.commit()
return {"bericht": "Verwijderd"}

@app.post("/uitloggen")
async def uitloggen(sessie_id: str = Cookie(default="")):
with SqliteDict("sessies.db") as sessies:
if sessie_id in sessies:
del sessies[sessie_id]
sessies.commit()
antwoord = JSONResponse({"bericht": "Uitgelogd"})
antwoord.delete_cookie("sessie_id")
return antwoord

Je gastenboek kan nu inloggen via de sessie, en weer uitloggen.

Door naar les 5: in je eigen project.