10.9. Autenticação para clientes programáticos

Tokens Bearer são a forma como telefones, microcontroladores companheiros e workers em nuvem se autenticam em um servidor – o cliente coloca um token no cabeçalho Authorization em cada requisição e o servidor o verifica. O aplicativo de telefone companheiro envia um POST com um “ack” quando o dono toca em “visto” em uma notificação de movimento; essas requisições precisam de um token.

10.9.1. O segredo de assinatura

O segredo é a chave privada da câmera para assinar e verificar tokens. Gere 32 bytes aleatórios a partir do RNG de hardware no primeiro boot da câmera, persista-os no sistema de arquivos e reutilize os mesmos bytes em todo boot subsequente:

# 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() é a fonte de aleatoriedade criptograficamente adequada em cada porta de câmera – na maioria das portas ele lê diretamente o gerador de números aleatórios de hardware do chip. O arquivo fica no diretório de trabalho da câmera (/sdcard se um cartão SD estiver montado, /flash caso contrário) e nunca sai da câmera. Apagar secret.bin e reiniciar gira o segredo e invalida todos os tokens emitidos sob o antigo.

Nota

machine.unique_id() não é um substituto. Em algumas portas de câmera ele também é usado para derivar o endereço MAC de rede, o que significa que seu valor viaja com cada pacote que a câmera envia – não é a propriedade que um segredo de assinatura precisa ter.

10.9.2. Emitindo tokens

O telefone precisa de uma maneira de obter um token em primeiro lugar. Um JSON Web Token (JWT) de curta duração – uma carga útil JSON codificada em base64 com uma assinatura anexada – é suficiente para uma primeira abordagem: o dono troca uma senha por um token e então usa o token até ele expirar:

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}

O telefone envia um POST com {user, pass} uma vez, armazena o token retornado no armazenamento local e o inclui como Authorization: Bearer <token> em cada requisição subsequente.

A reivindicação exp é um timestamp Unix. O verificador na próxima seção depende dessa reivindicação para rejeitar automaticamente tokens expirados – o que significa que o relógio da câmera precisa estar ajustado, pois caso contrário todo token parecerá estar muito no futuro ou já expirado. Veja Tempo e NTP para a receita de sincronização por NTP.

10.9.3. Verificando tokens com TokenAuth

microdot.auth.TokenAuth é a fábrica de decoradores para rotas que exigem um cabeçalho Authorization: Bearer <token>. O callback verificador recebe a string do token e retorna qualquer objeto de identidade que deva ser anexado à requisição – ou None para rejeitar:

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() lança jwt.exceptions.PyJWTError (ou uma subclasse) quando a assinatura está errada, o token está malformado ou a reivindicação exp já passou. O verificador engole todas essas exceções e retorna None, então o microdot responde 401 sem que o handler veja nada.

Quando o verificador retorna um valor diferente de None, o microdot o armazena em request.g.current_user para que o handler possa lê-lo – neste caso, é a reivindicação sub (subject) do JWT.

10.9.4. BasicAuth para redes fechadas

Para um endpoint de administração que só é acessado a partir de uma rede confiável, microdot.auth.BasicAuth é mais simples – o navegador exibe uma caixa de diálogo nativa de usuário/senha e os envia no cabeçalho Authorization: Basic ... em cada requisição:

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

As credenciais viajam em texto claro a menos que a conexão seja HTTPS, então essa abordagem só faz sentido em uma rede confiável ou com HTTPS implementado.

/api/ack e /api/login agora entendem tokens; /admin exige autenticação Basic.