10.10. Connexion au tableau de bord

Le tableau de bord web a besoin d’un formulaire de connexion – des inconnus sur le LAN ne devraient pas pouvoir voir le jardin. C’est précisément le rôle des sessions adossées aux cookies et du décorateur de connexion.

Une session est un petit dictionnaire que la caméra écrit dans un cookie. Le cookie est signé avec le secret de signature JWT chargé plus tôt dans le chapitre, ce qui permet au navigateur de le transporter sans pour autant pouvoir en modifier le contenu sans invalider la signature.

10.10.1. Configurer les objets de session et de connexion

microdot.session.Session installe le mécanisme de session sur app. microdot.login.Login ajoute le décorateur de type login_required ainsi que les utilitaires 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 empêche le JavaScript de la page de lire le cookie – une défense en profondeur contre un attaquant XSS qui tenterait de détourner la session. secure=False est une valeur provisoire en attendant la mise en place de HTTPS ; passez-la à True une fois que le serveur fonctionne via TLS afin que le cookie ne transite jamais en HTTP en clair.

Note

Le cross-site scripting (XSS) désigne la catégorie d’attaques où l’attaquant parvient à faire exécuter du JavaScript dans la vue qu’a l’utilisateur d’une page de confiance – généralement via un champ de formulaire non échappé, un commentaire rendu en HTML ou un widget tiers vulnérable. L’indicateur de cookie http_only n’empêche pas le XSS ; il maintient simplement le cookie de session hors de portée du script injecté, de sorte qu’un XSS réussi ne puisse pas être trivialement transformé en détournement de session.

Le user_loader est appelé à chaque requête protégée pour convertir l’identifiant stocké dans la session en l’enregistrement utilisateur que verra le gestionnaire. Gardez-le léger – il s’exécute sur le chemin critique.

10.10.2. Formulaire de connexion, envoi de la connexion, déconnexion

Le formulaire de connexion lui-même est une page statique servie depuis /sdcard/static/ exactement comme le tableau de bord. Le formulaire envoie un POSTvers /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() écrit le cookie de session et renvoie une redirection 302 vers la page que le client tentait initialement d’atteindre (argument de requête next=, avec repli sur /). remember=True écrit en outre un cookie _remember à durée de vie plus longue afin que la session survive à un redémarrage du navigateur.

microdot.login.Login.logout_user() efface les deux cookies, et une redirection consécutive renvoie le navigateur vers le formulaire.

10.10.3. Protéger le tableau de bord

Décorez chaque route destinée au tableau de bord avec @login afin qu’une requête non authentifiée reçoive une redirection 302 vers /login plutôt que le contenu :

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

Les points de terminaison de l’API basés sur des jetons (/api/login, /api/ack) restent basés sur des jetons – ils sont destinés à l’application mobile, pas au navigateur. L’authentification par jeton et l’authentification par session coexistent sans problème sur le même app.

10.10.4. Sessions fraîches contre sessions mémorisées

Le cookie _remember maintient l’utilisateur connecté entre les redémarrages du navigateur, mais c’est une forme d’authentification plus faible – le navigateur pourrait avoir été laissé dans un café. Pour les routes qui changent les mots de passe, enregistrent des jetons d’API ou effectuent toute autre opération méritant une réauthentification, décorez avec @login.fresh plutôt qu’avec @login. Une connexion fraîche est une connexion où l’utilisateur a tapé son mot de passe durant cette session ; une connexion mémorisée ne l’est pas. Le tableau de bord n’a rien qui atteigne ce niveau d’exigence, mais le décorateur est là si vous en avez besoin.

Le tableau de bord exige désormais une connexion avant que l’une quelconque de ses routes ne réponde.