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.