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.