10.10. Anmeldung für das Dashboard¶
Das Web-Dashboard benötigt ein Anmeldeformular – nicht jeder im LAN sollte den Garten sehen können. Genau dafür sorgen cookie-gestützte Sitzungen und der Login-Decorator.
Eine Sitzung ist ein kleines dict, das die Kamera in ein Cookie schreibt. Das Cookie wird mit dem zuvor in diesem Kapitel geladenen JWT-Signaturschlüssel signiert, sodass der Browser es mit sich führen kann, ohne den Inhalt manipulieren zu können, ohne die Signatur ungültig zu machen.
10.10.1. Einrichten der Sitzungs- und Login-Objekte¶
microdot.session.Session installiert die Sitzungsmechanik auf app. microdot.login.Login ergänzt den Decorator im Stil von login_required sowie die Hilfsfunktionen 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 verhindert, dass JavaScript auf der Seite das Cookie lesen kann – eine zusätzliche Verteidigungsschicht gegen einen XSS-Angreifer, der die Sitzung kapern will. secure=False ist ein Platzhalter, bis HTTPS eingerichtet ist; setzen Sie es auf True, sobald der Server über TLS läuft, damit das Cookie niemals über unverschlüsseltes HTTP übertragen wird.
Bemerkung
Cross-Site-Scripting (XSS) ist die Klasse von Angriffen, bei denen der Angreifer JavaScript innerhalb der Ansicht einer vertrauenswürdigen Seite zur Ausführung bringt – typischerweise über ein nicht maskiertes Formularfeld, einen als HTML gerenderten Kommentar oder ein verwundbares Drittanbieter-Widget. Das Cookie-Flag http_only verhindert XSS nicht; es hält das Sitzungs-Cookie lediglich außerhalb der Reichweite des eingeschleusten Skripts, sodass ein erfolgreicher XSS-Angriff nicht ohne Weiteres in eine Sitzungsübernahme verwandelt werden kann.
Der user_loader wird bei jeder geschützten Anfrage aufgerufen, um die in der Sitzung gespeicherte ID in den Benutzerdatensatz umzuwandeln, den der Handler sieht. Halten Sie ihn günstig – er läuft auf dem heißen Pfad.
10.10.2. Anmeldeformular, Login-POST, Logout¶
Das Anmeldeformular selbst ist eine statische Seite, die genau wie das Dashboard aus /sdcard/static/ ausgeliefert wird. Das Formular sendet einen POST an /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() schreibt das Sitzungs-Cookie und gibt eine 302-Weiterleitung zu der Seite zurück, die der Client ursprünglich erreichen wollte (Query-Argument next=, mit Rückfall auf /). remember=True schreibt zusätzlich ein langlebigeres _remember-Cookie, sodass die Sitzung einen Neustart des Browsers übersteht.
microdot.login.Login.logout_user() löscht beide Cookies, und eine anschließende Weiterleitung schickt den Browser zurück zum Formular.
10.10.3. Das Dashboard schützen¶
Versehen Sie jede zum Dashboard gehörende Route mit @login, damit eine nicht authentifizierte Anfrage statt des Inhalts eine 302 zu /login erhält:
@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):
...
Die token-basierten API-Endpunkte (/api/login, /api/ack) bleiben token-basiert – sie sind für die Telefon-App gedacht, nicht für den Browser. Token-Authentifizierung und Sitzungs-Authentifizierung existieren problemlos gemeinsam auf derselben app.
10.10.4. Frische vs. erinnerte Sitzungen¶
Das _remember-Cookie hält den Benutzer über Browser-Neustarts hinweg angemeldet, ist aber eine schwächere Form der Authentifizierung – der Browser könnte in einem Café zurückgelassen worden sein. Für Routen, die Passwörter ändern, API-Token registrieren oder irgendetwas anderes tun, das eine erneute Authentifizierung wert ist, verwenden Sie statt @login den Decorator @login.fresh. Eine frische Anmeldung ist eine, bei der der Benutzer sein Passwort in dieser Sitzung eingegeben hat; eine erinnerte Anmeldung ist das nicht. Das Dashboard enthält nichts, was diese Schwelle erreicht, aber der Decorator steht bereit, wenn Sie ihn brauchen.
Das Dashboard verlangt nun eine Anmeldung, bevor irgendeine seiner Routen antwortet.