10.10. Hallintapaneelin kirjautuminen

Web-hallintapaneeli tarvitsee kirjautumislomakkeen – satunnaisten LAN-verkossa olevien ihmisten ei pitäisi nähdä takapihaa. Juuri tätä varten ovat evästepohjaiset istunnot ja kirjautumisdekoraattori.

Istunto on pieni sanakirja, jonka kamera kirjoittaa evästeeseen. Eväste allekirjoitetaan tämän luvun aiemmin lataamalla JWT-allekirjoitussalaisuudella, joten selain voi kuljettaa sitä mukanaan, mutta ei voi peukaloida sisältöä mitätöimättä allekirjoitusta.

10.10.1. Istunto- ja kirjautumisobjektien määrittäminen

microdot.session.Session asentaa istuntokoneiston objektiin app. microdot.login.Login lisää login_required-tyylisen dekoraattorin sekä login_user- / logout_user-apufunktiot:

# 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 estää sivulla olevaa JavaScriptiä lukemasta evästettä – kerroksellinen suoja XSS-hyökkääjää vastaan, joka yrittäisi kaapata istunnon. secure=False on paikanpitäjä, kunnes HTTPS on käytössä; vaihda se arvoon True, kun palvelin toimii TLS:n yli, jotta eväste ei koskaan kulje pelkän HTTP:n yli.

Muista

Cross-site scripting (XSS) on hyökkäysluokka, jossa hyökkääjä saa JavaScriptin suoritettua luotetun sivun käyttäjälle näkyvän osan sisällä – tyypillisesti escapettamattoman lomakekentän, HTML:nä renderöidyn kommentin tai haavoittuvan kolmannen osapuolen widgetin kautta. Evästeen http_only-lippu ei estä XSS:ää; se vain pitää istuntoevästeen poissa injektoidun skriptin ulottuvilta, joten onnistunutta XSS:ää ei voi triviaalisti muuttaa istunnon kaappaukseksi.

user_loader kutsutaan jokaisella suojatulla pyynnöllä, jotta istuntoon tallennettu tunnus muutetaan käsittelijän näkemäksi käyttäjätietueeksi. Pidä se kevyenä – se ajetaan kuumalla polulla.

10.10.2. Kirjautumislomake, kirjautumisen POST, uloskirjautuminen

Itse kirjautumislomake on staattinen sivu, joka tarjotaan kansiosta /sdcard/static/ aivan kuten hallintapaneelikin. Lomake tekee POSTin osoitteeseen /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() kirjoittaa istuntoevästeen ja palauttaa 302-uudelleenohjauksen siihen sivuun, jonne asiakas alun perin yritti päästä (next=-kyselyargumentti, oletuksena /). remember=True kirjoittaa myös pidempi-ikäisen _remember-evästeen, jotta istunto säilyy selaimen uudelleenkäynnistyksen yli.

microdot.login.Login.logout_user() tyhjentää molemmat evästeet, ja seuraava uudelleenohjaus lähettää selaimen takaisin lomakkeelle.

10.10.3. Hallintapaneelin suojaaminen

Dekoroi jokainen hallintapaneeliin liittyvä reitti dekoraattorilla @login, jotta todentamaton pyyntö saa 302-vastauksen osoitteeseen /login sisällön sijaan:

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

Token-pohjaiset API-päätepisteet (/api/login, /api/ack) pysyvät token-pohjaisina – ne ovat puhelinsovellusta varten, eivät selainta varten. Token-todennus ja istuntotodennus toimivat onnellisesti rinnakkain samassa objektissa app.

10.10.4. Tuoreet vs. muistetut istunnot

_remember-eväste pitää käyttäjän kirjautuneena selaimen uudelleenkäynnistysten yli, mutta se on heikompi todennusmuoto – selain on voitu jättää kahvilaan. Reitit, jotka vaihtavat salasanoja, rekisteröivät API-tokeneita tai tekevät mitä tahansa muuta uudelleentodennuksen arvoista, kannattaa dekoroida @login-dekoraattorin sijaan dekoraattorilla @login.fresh. Tuore kirjautuminen on sellainen, jossa käyttäjä on kirjoittanut salasanansa tämän istunnon aikana; muistettu kirjautuminen ei ole. Hallintapaneelissa ei ole mitään, mikä nousisi tälle tasolle, mutta dekoraattori on käytettävissä, kun sitä tarvitset.

Hallintapaneeli vaatii nyt kirjautumisen, ennen kuin mikään sen reiteistä vastaa.