10.9. Autenticación para clientes programáticos#
Los tokens bearer son la forma en que los teléfonos, los microcontroladores acompañantes y los workers en la nube se autentican ante un servidor – el cliente coloca un token en el encabezado Authorization en cada solicitud y el servidor lo verifica. La app de teléfono acompañante hace POST de un «ack» cuando el propietario toca «visto» en una notificación de movimiento; esas solicitudes necesitan un token.
10.9.1. El secreto de firma#
El secreto es la clave privada de la cámara para firmar y verificar tokens. Genera 32 bytes aleatorios desde el RNG de hardware en el primer arranque de la cámara, persístelos en el sistema de archivos y reutiliza los mismos bytes en cada arranque posterior:
# auth/tokens.py
import os
try:
with open('secret.bin', 'rb') as f:
SECRET = f.read()
except OSError:
SECRET = os.urandom(32)
with open('secret.bin', 'wb') as f:
f.write(SECRET)
os.urandom() es la fuente aleatoria criptográficamente adecuada en cada port de la cámara – en la mayoría de los ports lee directamente el generador de números aleatorios por hardware del chip. El archivo reside en el directorio de trabajo de la cámara (/sdcard si hay una tarjeta SD montada, /flash en caso contrario) y nunca sale de la cámara. Eliminar secret.bin y reiniciar rota el secreto e invalida todos los tokens emitidos con el anterior.
Nota
machine.unique_id() no es un sustituto. En algunos ports de la cámara también se usa para derivar la dirección MAC de red, lo que significa que su valor viaja con cada paquete que envía la cámara – no es la propiedad que necesita un secreto de firma.
10.9.2. Emisión de tokens#
El teléfono necesita una forma de obtener un token en primer lugar. Un JSON Web Token (JWT) de corta duración – una carga útil JSON codificada en base64 con una firma adjunta – basta para una primera aproximación: el propietario cambia una contraseña por un token y luego usa el token hasta que caduca:
import jwt
import time
@app.post('/api/login')
async def api_login(request):
creds = request.json
if creds.get('user') != 'owner' or creds.get('pass') != load_password():
abort(401)
token = jwt.encode({
'sub': 'owner',
'exp': int(time.time()) + 3600,
}, SECRET)
return {'token': token}
El teléfono hace POST de {user, pass} una vez, almacena el token devuelto en el almacenamiento local y lo incluye como Authorization: Bearer <token> en cada solicitud posterior.
La reclamación exp es una marca de tiempo Unix. El verificador de la siguiente sección se basa en esta reclamación para rechazar automáticamente los tokens caducados – lo que significa que el reloj de la cámara debe estar ajustado, porque de lo contrario todo token parecerá estar muy en el futuro o ya caducado. Consulta Hora y NTP para la receta de sincronización NTP.
10.9.3. Verificación de tokens con TokenAuth#
microdot.auth.TokenAuth es la fábrica de decoradores para rutas que requieren un encabezado Authorization: Bearer <token>. La función de retorno (callback) del verificador recibe la cadena del token y devuelve el objeto de identidad que deba adjuntarse a la solicitud – o None para rechazarla:
from microdot.auth import TokenAuth
tokens = TokenAuth()
@tokens.authenticate
async def check_token(request, token):
try:
claims = jwt.decode(token, SECRET)
except jwt.exceptions.PyJWTError:
return None
return claims['sub']
@app.post('/api/ack')
@tokens
async def ack(request):
body = request.json
if not body or 'count' not in body:
abort(400, 'missing count')
return {'ok': True, 'user': request.g.current_user}
jwt.decode() lanza jwt.exceptions.PyJWTError (o una subclase) cuando la firma es incorrecta, el token está malformado o la reclamación exp ha caducado. El verificador absorbe todos esos casos y devuelve None, de modo que microdot responde 401 sin que el manejador vea nada.
Cuando el verificador devuelve un valor distinto de None, microdot lo almacena en request.g.current_user para que el manejador pueda leerlo – en este caso es la reclamación sub (subject) del JWT.
10.9.4. BasicAuth para redes cerradas#
Para un endpoint de administración al que solo se accede desde una red de confianza, microdot.auth.BasicAuth es más simple – el navegador muestra un diálogo nativo de usuario/contraseña y los envía en el encabezado Authorization: Basic ... en cada solicitud:
from microdot.auth import BasicAuth
basic = BasicAuth(realm='Backyard cam admin')
@basic.authenticate
async def check_basic(request, username, password):
if username == 'owner' and password == load_password():
return 'owner'
return None
@app.get('/admin')
@basic
async def admin(request):
return 'hi ' + request.g.current_user
Las credenciales viajan en texto plano a menos que la conexión sea HTTPS, por lo que este enfoque solo tiene sentido en una red de confianza o con HTTPS implementado.
/api/ack y /api/login ahora entienden tokens; /admin requiere autenticación Basic.