10.10. Přihlášení k dashboardu

Webový dashboard potřebuje přihlašovací formulář – náhodní lidé v LAN by neměli vidět dvorek. Přesně to zajišťují relace založené na cookies a přihlašovací dekorátor.

Relace (session) je malý slovník, který kamera zapíše do cookie. Cookie je podepsaná tajným klíčem pro podepisování JWT načteným dříve v této kapitole, takže ji prohlížeč může nosit s sebou, ale nemůže měnit její obsah, aniž by zneplatnil podpis.

10.10.1. Nastavení objektů session a login

microdot.session.Session nainstaluje mechanismus relací do app. microdot.login.Login přidává dekorátor ve stylu login_required a pomocné funkce 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 zabrání JavaScriptu na stránce ve čtení cookie – vrstvená obrana proti útočníkovi využívajícímu XSS k únosu relace. secure=False je zástupný symbol, dokud není zavedeno HTTPS; jakmile server poběží přes TLS, přepněte jej na True, aby cookie nikdy neputovala přes nešifrované HTTP.

Poznámka

Cross-site scripting (XSS) je třída útoků, při nichž útočník dosáhne spuštění JavaScriptu uvnitř uživatelova zobrazení důvěryhodné stránky – typicky prostřednictvím neošetřeného pole formuláře, komentáře vykresleného jako HTML nebo zranitelného widgetu třetí strany. Příznak cookie http_only nezabraňuje XSS; pouze drží cookie relace mimo dosah vloženého skriptu, takže úspěšné XSS nelze triviálně přeměnit na únos relace.

user_loader se volá při každém chráněném požadavku, aby z ID uloženého v relaci vytvořil záznam uživatele, který handler uvidí. Udržujte jej levný – běží na horké cestě.

10.10.2. Přihlašovací formulář, odeslání přihlášení, odhlášení

Samotný přihlašovací formulář je statická stránka servírovaná z /sdcard/static/ stejně jako dashboard. Formulář provádí POST na /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() zapíše cookie relace a vrátí přesměrování 302 na stránku, kterou se klient původně pokusil navštívit (argument dotazu next=, s návratem na /). remember=True navíc zapíše déle žijící cookie _remember, takže relace přežije restart prohlížeče.

microdot.login.Login.logout_user() vymaže obě cookies a následné přesměrování pošle prohlížeč zpět na formulář.

10.10.3. Ochrana dashboardu

Dekorujte každou cestu obsluhující dashboard pomocí @login, aby neautentizovaný požadavek dostal 302 na /login namísto obsahu:

@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):
    ...

Koncové body API založené na tokenech (/api/login, /api/ack) zůstávají založené na tokenech – jsou určeny pro mobilní aplikaci, ne pro prohlížeč. Autentizace tokenem a autentizace relací spolu vesele koexistují na téže app.

10.10.4. Čerstvé vs. zapamatované relace

Cookie _remember udržuje uživatele přihlášeného i po restartu prohlížeče, ale je to slabší forma autentizace – prohlížeč mohl zůstat zapnutý v kavárně. Pro cesty, které mění hesla, registrují API tokeny nebo dělají cokoli jiného, co stojí za opětovnou autentizaci, použijte místo @login dekorátor @login.fresh. Čerstvé přihlášení je takové, při kterém uživatel zadal své heslo v této relaci; zapamatované přihlášení nikoli. Dashboard nemá nic, co by dosahovalo této úrovně, ale dekorátor je k dispozici, když jej budete potřebovat.

Dashboard nyní před odpovědí kterékoli ze svých cest vyžaduje přihlášení.