10.9. Autenticação para clientes programáticos¶
Os tokens Bearer são a forma como telemóveis, microcontroladores auxiliares e workers na cloud se autenticam num servidor — o cliente coloca um token no cabeçalho Authorization em cada pedido e o servidor verifica-o. A aplicação telefónica auxiliar faz POST de um «ack» quando o proprietário toca em «visto» numa notificação de movimento; esses pedidos precisam de um token.
10.9.1. O segredo de assinatura¶
O segredo é a chave privada da câmara para assinar e verificar tokens. Gere 32 bytes aleatórios a partir do RNG de hardware na primeira inicialização da câmara, persiste-os no sistema de ficheiros e reutiliza os mesmos bytes em cada inicialização 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 aleatória criptograficamente adequada em cada porta da câmara — na maioria das portas lê diretamente o gerador de números aleatórios de hardware do chip. O ficheiro reside no diretório de trabalho da câmara (/sdcard se um cartão SD estiver montado, /flash caso contrário) e nunca sai da câmara. Apagar secret.bin e reiniciar roda o segredo e invalida todos os tokens emitidos com o anterior.
Nota
machine.unique_id() não é um substituto. Em algumas portas da câmara é também utilizado para derivar o endereço MAC da rede, o que significa que o seu valor viaja com cada pacote que a câmara envia — não é a propriedade de que um segredo de assinatura necessita.
10.9.2. Emissão de tokens¶
O telemóvel precisa de uma forma de obter um token em primeiro lugar. Um JSON Web Token (JWT) de curta duração — um payload JSON codificado em base64 com uma assinatura anexada — é suficiente para uma primeira abordagem: o proprietário troca uma palavra-passe por um token e usa o token até 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 telemóvel faz POST de {user, pass} uma vez, guarda o token devolvido no armazenamento local e inclui-o como Authorization: Bearer <token> em todos os pedidos subsequentes.
A afirmação exp é um timestamp Unix. O verificador na secção seguinte baseia-se nesta afirmação para rejeitar automaticamente tokens expirados — o que significa que o relógio de parede da câmara tem de estar definido, porque caso contrário todos os tokens parecerão estar muito no futuro ou já expirados. Consulte Tempo e NTP para a receita de sincronização NTP.
10.9.3. Verificação de tokens com TokenAuth¶
microdot.auth.TokenAuth é a fábrica de decoradores para rotas que requerem um cabeçalho Authorization: Bearer <token>. O callback verificador recebe a string do token e devolve qualquer objeto de identidade que deva ser associado ao pedido — 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 é incorreta, o token está malformado ou a afirmação exp passou. O verificador absorve todos esses casos e devolve None, pelo que o microdot responde com 401 sem o handler ver nada.
Quando o verificador devolve um valor diferente de None, o microdot guarda-o em request.g.current_user para que o handler possa lê-lo — neste caso é a afirmação sub (sujeito) do JWT.
10.9.4. BasicAuth para redes fechadas¶
Para um endpoint de administração que é apenas acedido a partir de uma rede de confiança, microdot.auth.BasicAuth é mais simples — o browser apresenta uma caixa de diálogo nativa de utilizador/palavra-passe e envia-os no cabeçalho Authorization: Basic ... em cada pedido:
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 simples a menos que a ligação seja HTTPS, pelo que esta abordagem só faz sentido numa rede de confiança ou com HTTPS em uso.
/api/ack e /api/login compreendem agora tokens; /admin requer autenticação Basic.