10.10. Login per la dashboard

La dashboard web ha bisogno di un modulo di login: persone a caso sulla LAN non dovrebbero poter vedere il giardino. È esattamente ciò che fanno le sessioni basate su cookie e il decoratore di login.

Una sessione è un piccolo dict che la camera scrive in un cookie. Il cookie è firmato con il segreto di firma JWT caricato in precedenza nel capitolo, così il browser può portarselo dietro ma non può manometterne il contenuto senza invalidare la firma.

10.10.1. Configurazione degli oggetti session e login

microdot.session.Session installa il meccanismo delle sessioni su app. microdot.login.Login aggiunge il decoratore in stile login_required e gli helper 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 impedisce al JavaScript della pagina di leggere il cookie, una difesa a più livelli contro un attaccante XSS che voglia dirottare la sessione. secure=False è un segnaposto finché non è in uso HTTPS; portalo a True una volta che il server gira su TLS, così il cookie non viaggia mai su HTTP in chiaro.

Nota

Il cross-site scripting (XSS) è la classe di attacchi in cui l’attaccante riesce a far eseguire JavaScript all’interno della vista che l’utente ha di una pagina fidata, tipicamente tramite un campo di modulo non sottoposto a escape, un commento renderizzato come HTML o un widget di terze parti vulnerabile. Il flag http_only del cookie non previene l’XSS; si limita a tenere il cookie di sessione fuori dalla portata dello script iniettato, così un XSS riuscito non può essere banalmente trasformato in un dirottamento di sessione.

Lo user_loader viene chiamato a ogni richiesta protetta per trasformare l’ID memorizzato nella sessione nel record utente che vedrà l’handler. Mantienilo leggero: viene eseguito sul percorso critico.

10.10.2. Modulo di login, POST di login, logout

Il modulo di login stesso è una pagina statica servita da /sdcard/static/ proprio come la dashboard. Il modulo invia un POST a /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() scrive il cookie di sessione e restituisce un redirect 302 verso qualunque pagina il client avesse originariamente cercato di raggiungere (argomento di query next=, con fallback a /). remember=True scrive anche un cookie _remember di durata maggiore, così la sessione sopravvive a un riavvio del browser.

microdot.login.Login.logout_user() cancella entrambi i cookie e un redirect successivo riporta il browser al modulo.

10.10.3. Protezione della dashboard

Decora ogni rotta rivolta alla dashboard con @login così che una richiesta non autenticata riceva un 302 verso /login invece del contenuto:

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

Gli endpoint dell’API basati su token (/api/login, /api/ack) rimangono basati su token: sono pensati per l’app del telefono, non per il browser. L’autenticazione a token e quella a sessione convivono felicemente sullo stesso app.

10.10.4. Sessioni nuove e sessioni ricordate

Il cookie _remember mantiene l’utente connesso anche tra un riavvio del browser e l’altro, ma è una forma di autenticazione più debole: il browser potrebbe essere stato lasciato in un bar. Per le rotte che cambiano le password, registrano token API o fanno qualsiasi altra cosa per cui valga la pena riautenticarsi, decora con @login.fresh invece di @login. Un login fresco è quello in cui l’utente ha digitato la propria password in questa sessione; un login ricordato no. La dashboard non ha nulla che arrivi a quel livello, ma il decoratore è lì per quando ne avrai bisogno.

La dashboard ora richiede un login prima che una qualsiasi delle sue rotte risponda.