10.10. Inloggen voor het dashboard¶
Het webdashboard heeft een inlogformulier nodig – willekeurige mensen op het LAN zouden de tuin niet moeten kunnen zien. Daar zorgen cookie-gebaseerde sessies en de login-decorator voor.
Een sessie is een kleine dict die de cam in een cookie schrijft. De cookie wordt ondertekend met het JWT-ondertekeningsgeheim dat eerder in dit hoofdstuk werd geladen, zodat de browser hem kan meedragen maar de inhoud niet kan manipuleren zonder de handtekening ongeldig te maken.
10.10.1. De session- en login-objecten opzetten¶
microdot.session.Session installeert de sessiemachinerie op app. microdot.login.Login voegt de decorator in login_required-stijl toe en de hulpfuncties login_user / logout_user:
# auth/login.py
from microdot.session import Session
from microdot.login import Login
Session(app, secret_key=SECRET,
cookie_options={'http_only': True, 'secure': False})
login = Login()
USERS = {
'owner': {'id': 'owner', 'password_hash': load_password_hash()},
}
@login.user_loader
async def load_user(user_id):
return USERS.get(user_id)
http_only=True voorkomt dat JavaScript op de pagina de cookie kan lezen – een gelaagde verdediging tegen een XSS-aanvaller die de sessie kaapt. secure=False is een tijdelijke waarde totdat HTTPS in gebruik is; zet het op True zodra de server over TLS draait, zodat de cookie nooit over gewoon HTTP reist.
Notitie
Cross-site scripting (XSS) is de klasse van aanvallen waarbij de aanvaller JavaScript laat uitvoeren binnen de weergave van de gebruiker op een vertrouwde pagina – doorgaans via een niet-ontsnapt formulierveld, een als HTML weergegeven reactie of een kwetsbare widget van een derde partij. De cookievlag http_only voorkomt XSS niet; het houdt alleen de sessiecookie buiten bereik van het geïnjecteerde script, zodat een geslaagde XSS niet eenvoudig kan worden omgezet in sessiekaping.
De user_loader wordt bij elk beveiligd verzoek aangeroepen om de in de sessie opgeslagen ID om te zetten in het gebruikersrecord dat de handler te zien krijgt. Houd hem goedkoop – hij draait op het hot path.
10.10.2. Inlogformulier, login-post, uitloggen¶
Het inlogformulier zelf is een statische pagina die net als het dashboard vanuit /sdcard/static/ wordt geserveerd. Het formulier doet een POST naar /login:
from microdot import redirect
@app.get('/login')
async def login_form(request):
return Response.send_file('/sdcard/static/login.html')
@app.post('/login')
async def do_login(request):
form = request.form
user = USERS.get(form.get('user'))
if not user or not check_password(user, form.get('pass')):
return redirect('/login?error=1')
return await login.login_user(request, user, remember=True)
@app.post('/logout')
async def logout(request):
await login.logout_user(request)
return redirect('/login')
microdot.login.Login.login_user() schrijft de sessiecookie en geeft een 302-redirect terug naar de pagina die de client oorspronkelijk probeerde te bereiken (queryargument next=, met / als terugval). remember=True schrijft ook een langer geldige _remember-cookie zodat de sessie een browserherstart overleeft.
microdot.login.Login.logout_user() wist beide cookies en een vervolgredirect stuurt de browser terug naar het formulier.
10.10.3. Het dashboard beveiligen¶
Voorzie elke dashboardroute van @login zodat een niet-geauthenticeerd verzoek een 302 naar /login krijgt in plaats van de inhoud:
@app.get('/<path:filename>')
@login
async def static(request, filename):
...
@app.get('/config')
@login
async def get_config(request):
...
@app.post('/config')
@login
async def set_config(request):
...
@app.get('/events')
@login
@with_sse
async def events(request, sse):
...
@app.get('/control')
@login
@with_websocket
async def control(request, ws):
...
De token-gebaseerde API-eindpunten (/api/login, /api/ack) blijven token-gebaseerd – ze zijn voor de telefoon-app, niet voor de browser. Tokenauthenticatie en sessieauthenticatie bestaan probleemloos naast elkaar op dezelfde app.
10.10.4. Verse versus onthouden sessies¶
De _remember-cookie houdt de gebruiker ingelogd na browserherstarts, maar het is een zwakkere vorm van authenticatie – de browser had in een café kunnen zijn achtergelaten. Voor routes die wachtwoorden wijzigen, API-tokens registreren of iets anders doen waarvoor herauthenticatie de moeite waard is, gebruik je @login.fresh in plaats van @login. Een verse login is er een waarbij de gebruiker zijn wachtwoord deze sessie heeft ingetypt; een onthouden login niet. Het dashboard heeft niets dat aan die lat voldoet, maar de decorator is er wanneer je hem nodig hebt.
Het dashboard vereist nu een login voordat een van zijn routes reageert.