Wachtwoorden: wachtwoord wijzigen
Met registreren en inloggen heb je de basis. Twee dingen horen er nog bij: een gebruiker moet zijn wachtwoord kunnen veranderen, en een hash met oude instellingen moet je kunnen bijwerken.
Het oude wachtwoord eerst
Een nieuw wachtwoord zet je pas in de database als het oude klopt. Anders kan
iemand die jouw laptop even gebruikt, je wachtwoord veranderen en je buitensluiten.
Het begin is daarom hetzelfde als bij /inloggen:
@app.post("/wijzig")
async def wijzig(naam: str = Form(...), oud: str = Form(...), nieuw: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
opgeslagen = db.get(naam)
if opgeslagen is None:
return {"bericht": "Naam of wachtwoord klopt niet"}
try:
ph.verify(opgeslagen, oud)
except VerifyMismatchError:
return {"bericht": "Naam of wachtwoord klopt niet"}
with SqliteDict("gebruikers.db") as db:
db[naam] = ph.hash(nieuw)
db.commit()
return {"bericht": "Wachtwoord gewijzigd"}
- Regel 3-10:
Net als bij inloggen: opzoeken, en
ph.verifymet het oude wachtwoord. - Regel 11-13:
Pas als het oude wachtwoord klopt, open je de database opnieuw en bewaar je de hash van het nieuwe.
Zet dit endpoint in hetzelfde main.py als /registreer en /inloggen: het
gebruikt dezelfde imports en dezelfde ph.
Oude hashes bijwerken
Computers worden elk jaar sneller, dus wat vandaag traag genoeg is, is dat over een paar jaar niet meer. Daarom worden de standaardinstellingen van Argon2 af en toe strenger. De oude hashes in je database blijven werken, want hun instellingen staan erin. Maar je wilt ze wel bijwerken.
Dat kan alleen als iemand inlogt: dan heb je het wachtwoord even in handen.
ph.check_needs_rehash zegt of een hash met andere instellingen is gemaakt dan
je PasswordHasher nu gebruikt:
from argon2 import PasswordHasher
ph = PasswordHasher()
nieuw = ph.hash("welkom123")
oud = PasswordHasher(time_cost=1).hash("welkom123")
print(ph.check_needs_rehash(nieuw))
print(ph.check_needs_rehash(oud))
Uitvoer:
False
True
PasswordHasher(time_cost=1) doet hier alsof hij een hash van jaren geleden
maakt, met lichtere instellingen: time_cost is de t= uit de tabel in
les 5.
In /inloggen komt het bijwerken ná het try/except-blok en vóór de laatste
return. Daar weet je zeker dat het wachtwoord klopt:
@app.post("/inloggen")
async def inloggen(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
opgeslagen = db.get(naam)
if opgeslagen is None:
return {"bericht": "Naam of wachtwoord klopt niet"}
try:
ph.verify(opgeslagen, wachtwoord)
except VerifyMismatchError:
return {"bericht": "Naam of wachtwoord klopt niet"}
if ph.check_needs_rehash(opgeslagen):
with SqliteDict("gebruikers.db") as db:
db[naam] = ph.hash(wachtwoord)
db.commit()
return {"bericht": "Ingelogd"}
- Regel 7-10:
Dit ken je uit les 6. Klopt het wachtwoord niet, dan stopt de functie hier.
- Regel 11-14:
Nieuw: is de opgeslagen hash met oude instellingen gemaakt, dan maak je een nieuwe van het wachtwoord dat je nu even in handen hebt, en bewaar je die.
Zo worden de hashes vanzelf bijgewerkt, zonder dat iemand zijn wachtwoord hoeft te veranderen.
Opdrachten
Opdracht 1: Run - Wijzigen
Zet /wijzig in je server. Schrijf een scriptje dat eerst probeert het
wachtwoord van Sara te wijzigen met een fout oud wachtwoord, en daarna met het
goede. Probeer daarna in te loggen met het oude en met het nieuwe wachtwoord.
Tip
Het lijkt op inloggen.py, maar met drie velden in data: naam, oud en
nieuw.
Antwoord
import httpx
adres = "http://127.0.0.1:8000"
wijzig = {"naam": "sara", "oud": "fout", "nieuw": "Lange-Nieuwe-Zin-7"}
print(httpx.post(adres + "/wijzig", data=wijzig).json())
wijzig["oud"] = "welkom123"
print(httpx.post(adres + "/wijzig", data=wijzig).json())
for wachtwoord in ["welkom123", "Lange-Nieuwe-Zin-7"]:
inloggen = {"naam": "sara", "wachtwoord": wachtwoord}
print(httpx.post(adres + "/inloggen", data=inloggen).json())
Uitvoer:
{'bericht': 'Naam of wachtwoord klopt niet'}
{'bericht': 'Wachtwoord gewijzigd'}
{'bericht': 'Naam of wachtwoord klopt niet'}
{'bericht': 'Ingelogd'}
Met het foute oude wachtwoord verandert er niets. Na de wijziging werkt alleen het nieuwe wachtwoord nog.
Opdracht 2: Investigate - Wanneer bijwerken?
Waarom werk je een oude hash bij in /inloggen, en niet bijvoorbeeld in een
apart script dat alle hashes in de database in één keer bijwerkt?
Antwoord
Voor een nieuwe hash heb je het wachtwoord nodig, en dat staat nergens: in de database staat alleen de oude hash. Het enige moment waarop je het wachtwoord even hebt, is als iemand inlogt.
Opdracht 3: Run - Zie het bijwerken gebeuren
Zet het bijwerken in je /inloggen. Dit script zet voor een nieuwe gebruiker
Tim een hash met oude instellingen in de database, logt in als Tim, en laat de
hash ervoor en erna zien. Voorspel eerst: verandert er iets aan de t=?
import httpx
from argon2 import PasswordHasher
from sqlitedict import SqliteDict
oud = PasswordHasher(time_cost=1).hash("zolder-regen-7")
with SqliteDict("gebruikers.db") as db:
db["tim"] = oud
db.commit()
print(db["tim"][:31])
antwoord = httpx.post("http://127.0.0.1:8000/inloggen", data={"naam": "tim", "wachtwoord": "zolder-regen-7"})
print(antwoord.json())
with SqliteDict("gebruikers.db") as db:
print(db["tim"][:31])
Tip
[:31] laat alleen het begin van de hash zien: het algoritme en de
instellingen, zonder het zout. Let op het getal achter t=.
Antwoord
Uitvoer:
$argon2id$v=19$m=65536,t=1,p=4$
{'bericht': 'Ingelogd'}
$argon2id$v=19$m=65536,t=3,p=4$
Tim komt binnen met de oude hash, want de instellingen staan erin. Daarna ziet
/inloggen dat die hash lichter is dan de instellingen van nu, en bewaart hij
een nieuwe: t=1 is t=3 geworden. Staat er bij jou een ander getal dan 3,
dan heeft jouw versie van argon2-cffi strengere instellingen.
Zo ziet je main.py er nu uit
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
from fastapi import FastAPI, Form
from sqlitedict import SqliteDict
app = FastAPI()
ph = PasswordHasher()
@app.post("/registreer")
async def registreer(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
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)
if opgeslagen is None:
return {"bericht": "Naam of wachtwoord klopt niet"}
try:
ph.verify(opgeslagen, wachtwoord)
except VerifyMismatchError:
return {"bericht": "Naam of wachtwoord klopt niet"}
if ph.check_needs_rehash(opgeslagen):
with SqliteDict("gebruikers.db") as db:
db[naam] = ph.hash(wachtwoord)
db.commit()
return {"bericht": "Ingelogd"}
@app.post("/wijzig")
async def wijzig(naam: str = Form(...), oud: str = Form(...), nieuw: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
opgeslagen = db.get(naam)
if opgeslagen is None:
return {"bericht": "Naam of wachtwoord klopt niet"}
try:
ph.verify(opgeslagen, oud)
except VerifyMismatchError:
return {"bericht": "Naam of wachtwoord klopt niet"}
with SqliteDict("gebruikers.db") as db:
db[naam] = ph.hash(nieuw)
db.commit()
return {"bericht": "Wachtwoord gewijzigd"}
Registreren, inloggen met bijwerken, en wijzigen: de hele reeks in één server.
Door naar les 8: in de praktijk.