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.