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.