10.10. Login no painel de controlo¶
O painel de controlo web precisa de um formulário de login – pessoas aleatórias na LAN não devem ter acesso ao pátio. É isso que as sessões com suporte em cookies e o decorador de login fazem.
Uma sessão é um pequeno dicionário que a câmara escreve num cookie. O cookie é assinado com o segredo de assinatura JWT carregado anteriormente no capítulo, pelo que o browser pode transportá-lo mas não pode adulterar o conteúdo sem invalidar a assinatura.
10.10.1. Configurar os objetos de sessão e login¶
microdot.session.Session instala o mecanismo de sessão em app. microdot.login.Login adiciona o decorador no estilo login_required e os auxiliares 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 na página leia o cookie – uma defesa em camadas contra um atacante XSS que tente sequestrar a sessão. secure=False é um marcador de posição até o HTTPS estar em funcionamento; altere para True assim que o servidor funcionar sobre TLS, para que o cookie nunca viaje sobre HTTP simples.
Nota
Cross-site scripting (XSS) é a classe de ataques em que o atacante consegue executar JavaScript dentro da vista do utilizador de uma página de confiança – tipicamente através de um campo de formulário sem escape, um comentário renderizado em HTML, ou um widget de terceiros vulnerável. O sinalizador de cookie http_only não previne XSS; apenas mantém o cookie de sessão fora do alcance do script injetado, para que um XSS bem-sucedido não possa ser facilmente transformado em sequestro de sessão.
O user_loader é chamado em cada pedido protegido para converter o ID armazenado na sessão no registo de utilizador que o manipulador irá ver. Mantenha-o eficiente – é executado 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/ tal como o painel de controlo. O formulário faz POSTpara /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() escreve o cookie de sessão e devolve um redirecionamento 302 para a página que o cliente tentou aceder originalmente (argumento de consulta next=, com fallback para /). remember=True também escreve um cookie _remember de maior duração para que a sessão sobreviva ao reinício do browser.
microdot.login.Login.logout_user() limpa ambos os cookies e um redirecionamento de seguimento envia o browser de volta para o formulário.
10.10.3. Proteção do painel de controlo¶
Decore cada rota voltada para o painel de controlo com @login para que um pedido não autenticado receba um redirecionamento 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 da API baseados em token (/api/login, /api/ack) continuam a ser baseados em token – são para a aplicação do telemóvel, não para o browser. A autenticação por token e a autenticação por sessão coexistem sem conflitos no mesmo app.
10.10.4. Sessões recentes vs. sessões lembradas¶
O cookie _remember mantém o utilizador com sessão iniciada após reinícios do browser, mas é uma forma de autenticação mais fraca – o browser pode ter sido deixado num café. Para rotas que alteram palavras-passe, registam tokens de API, ou fazem qualquer outra coisa que valha a pena re-autenticar, decore com @login.fresh em vez de @login. Um login recente é aquele em que o utilizador digitou a sua palavra-passe nesta sessão; um login lembrado não o é. O painel de controlo não tem nada que justifique esse critério, mas o decorador está disponível quando necessário.
O painel de controlo exige agora um login antes de qualquer uma das suas rotas responder.