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.