Ga naar hoofdinhoud

Wachtwoorden: inloggen met verify

Hier bouw je op verder: Try en except

Weet je dit niet meer zeker? Lees het terug voordat je verdergaat.

Na les 7 staan er alleen nog Argon2-hashes in je database. Bij het inloggen kun je niet meer vergelijken met ==: de hash van hetzelfde wachtwoord is elke keer anders. Daarvoor is ph.verify. Die haalt het zout en de instellingen uit de opgeslagen hash, rekent de hash van het ingetypte wachtwoord uit, en vergelijkt.

/inloggen met ph.verify​

from argon2.exceptions import VerifyMismatchError

NEP = ph.hash("niemand")

@app.post("/inloggen")
async def inloggen(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
opgeslagen = db.get(naam, NEP)
try:
ph.verify(opgeslagen, wachtwoord)
except VerifyMismatchError:
return {"bericht": "Naam of wachtwoord klopt niet"}
if opgeslagen == NEP:
return {"bericht": "Naam of wachtwoord klopt niet"}
return {"bericht": "Ingelogd"}
  1. Regel 1:

    VerifyMismatchError is de fout die ph.verify gooit als het wachtwoord niet klopt.

  2. Regel 3:

    Een nep-hash: de Argon2-hash van een wachtwoord dat bij geen enkel account hoort. Je rekent hem één keer uit, als de server start.

  3. Regel 8:

    Bestaat de naam niet, dan geeft db.get de nep-hash terug in plaats van None.

  4. Regel 9-12:

    Klopt het wachtwoord, dan gaat ph.verify zonder fout verder. Klopt het niet, dan vang je de fout af en meld je dat inloggen niet lukt.

  5. Regel 13-14:

    Was het de nep-hash, dan bestaat de naam niet. Ook wie toevallig niemand intypt, komt dus niet binnen. De melding is dezelfde als bij een fout wachtwoord, zodat je niet verraadt welke namen wél een account hebben.

Zet de import bij je andere imports, NEP direct onder ph = PasswordHasher(), en vervang je oude /inloggen door deze, in hetzelfde main.py als je /registreer uit les 7. import hashlib mag weg: niets gebruikt hem meer.

Waarom die nep-hash? ph.verify rekent een Argon2-hash uit, en die is expres traag, zoals je in les 6 mat. Zou je bij een onbekende naam meteen antwoorden, dan komt dat antwoord vele malen sneller terug dan bij een bestaande naam met een fout wachtwoord. De melding is gelijk, maar de tijd verraadt dan alsnog welke namen een account hebben. Met de nep-hash doet de server bij een onbekende naam hetzelfde rekenwerk, en duren beide antwoorden even lang.

Waar de controle zit​

Registreren en inloggen naast elkaar: de server maakt en controleert de hash, de browser ziet hem nooit.

Waar de controle zit: wachtwoorden
  1. Browser: Je registreert. Naam en wachtwoord gaan via het formulier naar de server.

  2. Hier zit de controleServer: ph.hash maakt de hash. Met een eigen zout, en expres traag. Alleen de hash gaat de database in. · Registreren met Argon2

  3. Browser: Je logt in. Weer naam en wachtwoord via het formulier.

  4. Hier zit de controleServer: ph.verify vergelijkt. Met het zout en de instellingen uit de opgeslagen hash. · Inloggen met verify

  5. Browser: Ingelogd, of één melding. Een foute naam en een fout wachtwoord geven hetzelfde antwoord.

Doe de test​

Draai inloggen.py uit les 4 opnieuw. Het doet drie pogingen: Sara met haar wachtwoord, Sara met een letter ernaast, en Bob, die geen account heeft.

Zo ziet je main.py er nu uit
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
from fastapi import FastAPI, Form, HTTPException
from sqlitedict import SqliteDict

app = FastAPI()
ph = PasswordHasher()
NEP = ph.hash("niemand")

@app.post("/registreer")
async def registreer(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
if naam in db:
raise HTTPException(status_code=400, detail="Deze naam is al bezet")
db[naam] = ph.hash(wachtwoord)
db.commit()
return {"bericht": f"Welkom, {naam}"}

@app.post("/inloggen")
async def inloggen(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
opgeslagen = db.get(naam, NEP)
try:
ph.verify(opgeslagen, wachtwoord)
except VerifyMismatchError:
return {"bericht": "Naam of wachtwoord klopt niet"}
if opgeslagen == NEP:
return {"bericht": "Naam of wachtwoord klopt niet"}
return {"bericht": "Ingelogd"}

Registreren en inloggen werken nu allebei met Argon2.

Controlevraag
Bob heeft geen account. Waarom rekent de server bij zijn poging toch een Argon2-hash uit, tegen NEP?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.

Er gaat iets mis​

Zelf vergelijken​

Je vergelijkt zelf, in plaats van ph.verify te gebruiken:

# FOUT
if ph.hash(wachtwoord) == opgeslagen:
return {"bericht": "Ingelogd"}

# GOED
try:
ph.verify(opgeslagen, wachtwoord)
except VerifyMismatchError:
return {"bericht": "Naam of wachtwoord klopt niet"}
return {"bericht": "Ingelogd"}

Er komt geen foutmelding, maar niemand kan meer inloggen, ook niet met het goede wachtwoord.

Oorzaak: ph.hash maakt elke keer een nieuw zout, dus elke keer een andere hash. Twee hashes van hetzelfde wachtwoord zijn nooit gelijk.

Oplossing: vergelijk nooit zelf. ph.verify haalt het zout uit de opgeslagen hash en rekent daarmee.

Een oude hash in de database​

Je draait inloggen.py, en het script stopt met een lange foutmelding die zo eindigt:

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

In de terminal van je server staat bij dezelfde poging "POST /inloggen HTTP/1.1" 500, en daaronder een foutmelding die eindigt met argon2.exceptions.InvalidHashError.

Oorzaak: in de database staat nog een sha256-hash of een wachtwoord als gewone tekst, uit de lessen hiervoor. ph.verify herkent die code niet als Argon2-hash en gooit een fout, en de server stuurt dan een 500 met als inhoud de platte tekst Internal Server Error. Daar zit geen JSON in, dus antwoord.json() in je script loopt vast.

Oplossing: gooi gebruikers.db weg, en draai registreer.py opnieuw met je server van nu.

Opdrachten​

Opdracht 1: Predict - Wat zie je?​

Noor heeft welkom123. Je stuurt een inlogpoging met naam noor en wachtwoord Welkom123, met een hoofdletter.

Vraag: wat antwoordt de server?

Antwoord

Naam of wachtwoord klopt niet. Een hoofdletter is een ander teken, dus een ander wachtwoord, en ph.verify gooit een VerifyMismatchError.

Opdracht 2: Investigate - Wachten in plaats van rekenen​

Een klasgenoot stelt voor om de nep-hash weg te laten, en bij een onbekende naam de server even te laten wachten met time.sleep(0.05). Dan duurt dat antwoord toch ook lang. Waarom is de nep-hash beter?

Tip

Hoe lang duurt één Argon2-hash? Kijk naar je meting in les 6, en vergelijk die met de meting van een klasgenoot.

Antwoord

Hoe lang ph.verify duurt, verschilt per computer en per moment: in les 6 kreeg elke computer andere getallen, en een server die het druk heeft, rekent langzamer. Een vaste 0.05 past dus bijna nooit precies, en wie de tijd meet, ziet het verschil alsnog. Worden de instellingen van Argon2 later strenger, dan klopt het getal helemaal niet meer.

Met de nep-hash doet de server bij een onbekende naam precies hetzelfde rekenwerk als bij een bestaande naam. Daardoor duren beide antwoorden even lang, op elke computer.

Door naar les 9: wachtwoord wijzigen.