10.10. Inicio de sesión para el panel de control¶
El panel web necesita un formulario de inicio de sesión: cualquier persona en la LAN no debería poder ver el jardín. Para eso sirven las sesiones respaldadas por cookies y el decorador de inicio de sesión.
Una sesión es un pequeño diccionario que la cámara escribe en una cookie. La cookie se firma con el secreto de firma JWT cargado anteriormente en el capítulo, de modo que el navegador puede transportarla pero no puede alterar su contenido sin invalidar la firma.
10.10.1. Configurar los objetos de sesión e inicio de sesión¶
microdot.session.Session instala la maquinaria de sesiones en app. microdot.login.Login añade el decorador de estilo login_required y los ayudantes 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 impide que el JavaScript de la página lea la cookie, una defensa en capas contra un atacante de XSS que secuestre la sesión. secure=False es un marcador temporal hasta que se implemente HTTPS; cámbialo a True una vez que el servidor funcione sobre TLS para que la cookie nunca viaje por HTTP sin cifrar.
Nota
El cross-site scripting (XSS) es la clase de ataques en los que el atacante consigue ejecutar JavaScript dentro de la vista que el usuario tiene de una página de confianza, normalmente a través de un campo de formulario sin escapar, un comentario renderizado como HTML o un widget de terceros vulnerable. El indicador de cookie http_only no previene el XSS; simplemente mantiene la cookie de sesión fuera del alcance del script inyectado, de modo que un XSS exitoso no pueda convertirse trivialmente en un secuestro de sesión.
El user_loader se llama en cada solicitud protegida para convertir el ID almacenado en la sesión en el registro de usuario que verá el manejador. Manténlo ligero, ya que se ejecuta en la ruta crítica.
10.10.2. Formulario de inicio de sesión, envío de inicio de sesión y cierre de sesión¶
El propio formulario de inicio de sesión es una página estática servida desde /sdcard/static/, igual que el panel. El formulario hace POST a /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() escribe la cookie de sesión y devuelve una redirección 302 a la página que el cliente intentó alcanzar originalmente (el argumento de consulta next=, con / como valor de reserva). remember=True también escribe una cookie _remember de mayor duración para que la sesión sobreviva a un reinicio del navegador.
microdot.login.Login.logout_user() borra ambas cookies y una redirección posterior devuelve el navegador al formulario.
10.10.3. Proteger el panel de control¶
Decora cada ruta del panel con @login para que una solicitud no autenticada reciba un 302 a /login en lugar del contenido:
@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):
...
Los endpoints de la API basados en tokens (/api/login, /api/ack) siguen siendo basados en tokens: son para la aplicación del teléfono, no para el navegador. La autenticación por token y la autenticación por sesión coexisten sin problemas en la misma app.
10.10.4. Sesiones nuevas frente a recordadas¶
La cookie _remember mantiene al usuario con la sesión iniciada entre reinicios del navegador, pero es una forma de autenticación más débil: el navegador podría haberse dejado en una cafetería. Para las rutas que cambian contraseñas, registran tokens de la API o hacen cualquier otra cosa que merezca una nueva autenticación, decora con @login.fresh en lugar de @login. Un inicio de sesión fresco es aquel en el que el usuario escribió su contraseña en esta sesión; un inicio de sesión recordado no lo es. El panel no tiene nada que alcance ese nivel, pero el decorador está ahí cuando lo necesites.
El panel ahora requiere un inicio de sesión antes de que cualquiera de sus rutas responda.