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.