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.