10.9. Autenticazione per i client programmatici#
I bearer token sono il modo in cui telefoni, microcontrollori companion e worker cloud si autenticano presso un server – il client inserisce un token nell’header Authorization a ogni richiesta e il server lo verifica. L’app companion del telefono invia tramite POST un «ack» quando il proprietario tocca «visto» su una notifica di movimento; queste richieste richiedono un token.
10.9.1. Il segreto di firma#
Il segreto e la chiave privata della cam per firmare e verificare i token. Genera 32 byte casuali dall’RNG hardware al primo avvio della cam, salvali in modo persistente sul filesystem e riutilizza gli stessi byte a ogni avvio successivo:
# 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() e la sorgente casuale crittograficamente adatta su ogni port della cam – sulla maggior parte dei port legge direttamente il generatore di numeri casuali hardware del chip. Il file risiede nella directory di lavoro della cam (/sdcard se e montata una scheda SD, altrimenti /flash) e non lascia mai la cam. Eliminare secret.bin e riavviare ruota il segreto e invalida ogni token emesso con quello precedente.
Nota
machine.unique_id() non e un sostituto. Su alcuni port della cam viene anche usato per derivare l’indirizzo MAC di rete, il che significa che il suo valore viaggia con ogni pacchetto inviato dalla cam – non e la proprieta di cui ha bisogno un segreto di firma.
10.9.2. Emissione dei token#
Il telefono ha bisogno di un modo per ottenere un token in primo luogo. Un JSON Web Token (JWT) di breve durata – un payload JSON codificato in base64 con una firma appesa – e sufficiente per un primo approccio: il proprietario scambia una password per un token, poi usa il token finche non scade:
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}
Il telefono invia tramite POST {user, pass} una volta, memorizza il token restituito nel local storage e lo include come Authorization: Bearer <token> a ogni richiesta successiva.
Il claim exp e un timestamp Unix. Il verificatore della sezione successiva si affida a questo claim per rifiutare automaticamente i token scaduti – il che significa che l’orologio della cam deve essere impostato, perche altrimenti ogni token sembrera lontano nel futuro o gia scaduto. Vedi Ora e NTP per la procedura di sincronizzazione NTP.
10.9.3. Verifica dei token con TokenAuth#
microdot.auth.TokenAuth e la factory di decoratori per le route che richiedono un header Authorization: Bearer <token>. La callback di verifica riceve la stringa del token e restituisce qualunque oggetto identita debba essere associato alla richiesta – oppure None per rifiutare:
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() solleva jwt.exceptions.PyJWTError (o una sottoclasse) quando la firma e errata, il token e malformato o il claim exp e scaduto. Il verificatore intercetta tutti questi casi e restituisce None, cosi microdot risponde 401 senza che l’handler veda alcunche.
Quando il verificatore restituisce un valore diverso da None, microdot lo memorizza in request.g.current_user cosi che l’handler possa leggerlo – in questo caso si tratta del claim sub (subject) del JWT.
10.9.4. BasicAuth per le reti chiuse#
Per un endpoint di amministrazione raggiunto solo da una rete fidata, microdot.auth.BasicAuth e piu semplice – il browser apre una finestra di dialogo nativa per nome utente/password e li invia nell’header Authorization: Basic ... a ogni richiesta:
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
Le credenziali viaggiano in chiaro a meno che la connessione non sia HTTPS, quindi questo approccio ha senso solo su una rete fidata o con HTTPS in uso.
/api/ack e /api/login ora comprendono i token; /admin richiede l’autenticazione Basic.