10.10. ダッシュボードへのログイン

Web ダッシュボードにはログインフォームが必要です。LAN 上の無関係な人が庭を覗けてはなりません。それを実現するのが、Cookie に裏付けられたセッションとログインデコレータです。

セッションとは、カメラが Cookie に書き込む小さな辞書(dict)です。この Cookie は本章で先ほど読み込んだ JWT 署名用シークレットで署名されるため、ブラウザは持ち回ることはできても、署名を無効化せずに中身を改ざんすることはできません。

10.10.1. セッションオブジェクトとログインオブジェクトをセットアップする

microdot.session.Sessionapp にセッション機構を組み込みます。microdot.login.Loginlogin_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 が Cookie を読み取れなくなります。これは XSS 攻撃者によるセッション乗っ取りに対する多層防御です。secure=False は HTTPS が整うまでのプレースホルダーです。サーバーが TLS 上で動作するようになったら True に切り替え、Cookie が平文の HTTP を通って流れないようにしてください。

注釈

クロスサイトスクリプティング(XSS)とは、信頼されたページのユーザー表示内で攻撃者が JavaScript を実行させる種類の攻撃です。典型的にはエスケープされていないフォームフィールド、HTML としてレンダリングされるコメント、脆弱なサードパーティ製ウィジェットなどを通じて発生します。http_only Cookie フラグは XSS そのものを防ぐわけではありません。ただ、注入されたスクリプトの手の届かないところにセッション Cookie を置いておくだけです。そのため XSS が成功しても、それを安直にセッション乗っ取りへつなげることはできなくなります。

user_loader は保護されたリクエストごとに呼び出され、セッションに保存された ID を、ハンドラが参照するユーザーレコードへと変換します。これはホットパス上で実行されるため、軽量に保ってください。

10.10.2. ログインフォーム、ログインの POST、ログアウト

ログインフォーム自体は、ダッシュボードと同じく /sdcard/static/ から配信される静的ページです。このフォームは /login に対して POST を行います:

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() はセッション Cookie を書き込み、クライアントが当初到達しようとしていたページ(next= クエリ引数、なければ / にフォールバック)への 302 リダイレクトを返します。remember=True を指定すると、より長寿命の _remember Cookie も書き込まれ、ブラウザを再起動してもセッションが維持されます。

microdot.login.Login.logout_user() は両方の Cookie をクリアし、続くリダイレクトでブラウザをフォームへ戻します。

10.10.3. ダッシュボードを保護する

ダッシュボードに面するすべてのルートを @login でデコレートし、認証されていないリクエストにはコンテンツの代わりに /login への 302 を返すようにします:

@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)はトークンベースのまま残します。これらはブラウザではなくスマホアプリ向けだからです。トークン認証とセッション認証は、同じ app 上で問題なく共存できます。

10.10.4. 新規セッションと記憶されたセッション

_remember Cookie はブラウザを再起動してもユーザーをログイン状態に保ちますが、これはより弱い認証形式です。ブラウザはカフェに置き忘れられていたかもしれません。パスワードを変更したり、API トークンを登録したり、その他再認証する価値のある処理を行うルートには、@login の代わりに @login.fresh でデコレートしてください。フレッシュなログインとは、ユーザーが今回のセッションでパスワードを入力したものを指し、記憶されたログインはそうではありません。ダッシュボードにはそこまでの基準に達するものはありませんが、必要になったときのためにデコレータは用意されています。

これでダッシュボードは、いずれのルートが応答する前にもログインを要求するようになりました。