10.10. Login para o painel

O painel web precisa de um formulário de login – pessoas aleatórias na LAN não deveriam ver o quintal. É exatamente isso que as sessões baseadas em cookies e o decorador de login fazem.

Uma sessão é um pequeno dict que a câmera grava em um cookie. O cookie é assinado com o segredo de assinatura JWT carregado anteriormente neste capítulo, de modo que o navegador pode carregá-lo por aí, mas não pode adulterar o conteúdo sem invalidar a assinatura.

10.10.1. Configure os objetos de sessão e login

microdot.session.Session instala a infraestrutura de sessão no app. microdot.login.Login adiciona o decorador no estilo login_required e os utilitários 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 impede que o JavaScript da página leia o cookie – uma defesa em camadas contra um atacante de XSS sequestrar a sessão. secure=False é um marcador temporário até que o HTTPS esteja implementado; mude-o para True assim que o servidor rodar sobre TLS, para que o cookie nunca trafegue por HTTP puro.

Nota

Cross-site scripting (XSS) é a classe de ataques em que o atacante consegue executar JavaScript dentro da visão que o usuário tem de uma página confiável – normalmente por meio de um campo de formulário não escapado, um comentário renderizado como HTML ou um widget de terceiros vulnerável. A flag de cookie http_only não impede o XSS; ela apenas mantém o cookie de sessão fora do alcance do script injetado, de modo que um XSS bem-sucedido não pode ser trivialmente convertido em sequestro de sessão.

O user_loader é chamado em cada requisição protegida para transformar o ID armazenado na sessão no registro de usuário que o handler verá. Mantenha-o barato – ele roda no caminho crítico.

10.10.2. Formulário de login, post de login, logout

O próprio formulário de login é uma página estática servida a partir de /sdcard/static/, assim como o painel. O formulário faz um POST para /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() grava o cookie de sessão e retorna um redirecionamento 302 para qualquer página que o cliente originalmente tentou acessar (argumento de consulta next=, recorrendo a /). remember=True também grava um cookie _remember de vida mais longa, para que a sessão sobreviva a uma reinicialização do navegador.

microdot.login.Login.logout_user() limpa ambos os cookies e um redirecionamento subsequente envia o navegador de volta ao formulário.

10.10.3. Protegendo o painel

Decore cada rota voltada ao painel com @login para que uma requisição não autenticada receba um 302 para /login em vez do conteúdo:

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

Os endpoints de API baseados em token (/api/login, /api/ack) permanecem baseados em token – eles são para o aplicativo do celular, não para o navegador. A autenticação por token e a autenticação por sessão coexistem tranquilamente no mesmo app.

10.10.4. Sessões novas vs. lembradas

O cookie _remember mantém o usuário logado entre reinicializações do navegador, mas é uma forma mais fraca de autenticação – o navegador pode ter sido deixado em um café. Para rotas que alteram senhas, registram tokens de API ou fazem qualquer outra coisa que valha a pena reautenticar, decore com @login.fresh em vez de @login. Um login novo é aquele em que o usuário digitou a senha nesta sessão; um login lembrado não é. O painel não tem nada que se eleve a esse nível, mas o decorador está lá quando você precisar.

O painel agora exige um login antes que qualquer uma de suas rotas responda.