10.10. התחברות ללוח הבקרה

לוח הבקרה ברשת זקוק לטופס התחברות – אנשים אקראיים ברשת המקומית (LAN) לא אמורים לראות את החצר. בדיוק לשם כך נועדו ה-sessions המגובים בעוגיות ומעטף ה-login.

session הוא dict קטן שהמצלמה כותבת לתוך עוגייה. העוגייה חתומה במפתח החתימה של JWT שנטען מוקדם יותר בפרק, כך שהדפדפן יכול לשאת אותה אך אינו יכול לשנות את תוכנה בלי לפסול את החתימה.

10.10.1. הגדרת אובייקטי ה-session וה-login

microdot.session.Session מתקין את מנגנון ה-session על app. microdot.login.Login מוסיף את המעטף בסגנון login_required ואת פונקציות העזר 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 מונע מ-JavaScript שבדף לקרוא את העוגייה – הגנה רב-שכבתית מפני תוקף XSS שמנסה לחטוף את ה-session. secure=False הוא ערך זמני עד שמיושם HTTPS; שנו אותו ל-True ברגע שהשרת רץ מעל TLS כדי שהעוגייה לעולם לא תועבר ב-HTTP רגיל.

הערה

Cross-site scripting (XSS) הוא סוג המתקפות שבהן התוקף גורם ל-JavaScript לרוץ בתוך התצוגה של המשתמש לדף מהימן – בדרך כלל דרך שדה טופס שלא עבר escaping, תגובה שמוצגת כ-HTML, או widget צד-שלישי פגיע. הדגל http_only של העוגייה אינו מונע XSS; הוא רק מרחיק את עוגיית ה-session מהישג ידו של הסקריפט המוזרק, כך ש-XSS מוצלח אינו ניתן להפיכה קלה לחטיפת session.

ה-user_loader נקרא בכל בקשה מוגנת כדי להמיר את המזהה השמור ב-session לרשומת המשתמש שה-handler יראה. שמרו אותו זול – הוא רץ בנתיב החם.

10.10.2. טופס התחברות, שליחת התחברות, התנתקות

טופס ההתחברות עצמו הוא דף סטטי המוגש מ-/sdcard/static/ בדיוק כמו לוח הבקרה. הטופס שולח POSTל-/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() כותב את עוגיית ה-session ומחזיר הפניית 302 לדף שאליו הלקוח ניסה להגיע במקור (ארגומנט השאילתה next=, עם נפילה חזרה ל-/). remember=True גם כותב עוגיית _remember ארוכת-טווח כך שה-session שורד הפעלה מחדש של הדפדפן.

microdot.login.Login.logout_user() מנקה את שתי העוגיות, והפניה עוקבת מחזירה את הדפדפן אל הטופס.

10.10.3. הגנה על לוח הבקרה

עטפו כל route הפונה אל לוח הבקרה ב-@login כך שבקשה לא מאומתת תקבל 302 ל-/login במקום את התוכן:

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

נקודות הקצה של ה-API מבוססות הטוקן (/api/login, /api/ack) נשארות מבוססות טוקן – הן מיועדות לאפליקציית הטלפון, לא לדפדפן. אימות טוקן ואימות session מתקיימים יחד בשמחה על אותו app.

10.10.4. sessions טריים מול sessions זכורים

עוגיית ה-_remember שומרת את המשתמש מחובר בין הפעלות מחדש של הדפדפן, אך זו צורת אימות חלשה יותר – ייתכן שהדפדפן נשאר פתוח בבית קפה. עבור routes שמשנים סיסמאות, רושמים טוקני API, או עושים כל דבר אחר שכדאי לאמת מחדש עבורו, עטפו עם @login.fresh במקום @login. התחברות טרייה היא כזו שבה המשתמש הקליד את סיסמתו בהפעלה הנוכחית; התחברות זכורה אינה כזו. בלוח הבקרה אין דבר שמגיע לרף הזה, אך המעטף קיים כשתזדקקו לו.

לוח הבקרה דורש כעת התחברות לפני שאחד מה-routes שלו מגיב.