10.9. Autentifikacija za programske klijente

Bearer tokeni način su na koji se telefoni, prateći mikrokontroleri i radnici u oblaku autentificiraju na poslužitelju – klijent stavlja token u zaglavlje Authorization na svakom zahtjevu, a poslužitelj ga provjerava. Prateća telefonska aplikacija šalje POST „ack” kada vlasnik dodirne „viđeno” na obavijesti o kretanju; ti zahtjevi trebaju token.

10.9.1. Tajna za potpisivanje

Tajna je privatni ključ kamere za potpisivanje i provjeru tokena. Generirajte 32 nasumična bajta iz hardverskog RNG-a pri prvom pokretanju kamere, pohranite ih trajno u datotečni sustav i ponovno koristite iste bajtove pri svakom sljedećem pokretanju:

# 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() je kriptografski prikladan izvor nasumičnosti na svakom kameri (port) – na većini portova izravno čita hardverski generator nasumičnih brojeva čipa. Datoteka se nalazi u radnom direktoriju kamere (/sdcard ako je SD kartica montirana, inače /flash) i nikad ne napušta kameru. Brisanje datoteke secret.bin i ponovno pokretanje rotira tajnu i poništava svaki token izdan pod starom.

Napomena

machine.unique_id() nije zamjena. Na nekim portovima kamere koristi se i za izvođenje mrežne MAC adrese, što znači da njegova vrijednost putuje sa svakim paketom koji kamera pošalje – a to nije svojstvo koje tajna za potpisivanje treba.

10.9.2. Izdavanje tokena

Telefon najprije treba način da dobije token. Kratkotrajni JSON Web Token (JWT) – JSON sadržaj kodiran base64-om s pridodanim potpisom – dovoljan je za prvi prolaz: vlasnik mijenja lozinku za token, a zatim koristi token dok ne istekne:

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}

Telefon jednom šalje POST {user, pass}, pohranjuje vraćeni token u lokalnu pohranu i uključuje ga kao Authorization: Bearer <token> na svakom sljedećem zahtjevu.

Tvrdnja exp je Unix vremenska oznaka. Provjeritelj u sljedećem odjeljku oslanja se na tu tvrdnju kako bi automatski odbio istekle tokene – što znači da satovi kamere moraju biti postavljeni, jer će inače svaki token izgledati ili daleko u budućnosti ili već istekao. Pogledajte Vrijeme i NTP za recept sinkronizacije putem NTP-a.

10.9.3. Provjera tokena pomoću TokenAuth

microdot.auth.TokenAuth je tvornica dekoratora za rute koje zahtijevaju zaglavlje Authorization: Bearer <token>. Povratni poziv provjeritelja prima niz tokena i vraća bilo koji objekt identiteta koji treba priložiti zahtjevu – ili None za odbijanje:

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() podiže jwt.exceptions.PyJWTError (ili podklasu) kada je potpis pogrešan, token neispravno oblikovan ili je tvrdnja exp prošla. Provjeritelj proguta sve to i vraća None, pa microdot odgovara 401 a da rukovatelj ništa ne vidi.

Kada provjeritelj vrati vrijednost različitu od None, microdot je pohranjuje u request.g.current_user kako bi je rukovatelj mogao pročitati – u ovom slučaju to je JWT tvrdnja sub (subjekt).

10.9.4. BasicAuth za zatvorene mreže

Za administratorsku krajnju točku kojoj se pristupa samo s pouzdane mreže, microdot.auth.BasicAuth je jednostavniji – preglednik otvara izvorni dijalog za korisničko ime/lozinku i šalje ih u zaglavlju Authorization: Basic ... na svakom zahtjevu:

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

Vjerodajnice putuju u otvorenom obliku osim ako je veza HTTPS, pa ovaj pristup ima smisla samo na pouzdanoj mreži ili uz uspostavljen HTTPS.

/api/ack i /api/login sada razumiju tokene; /admin zahtijeva Basic autentifikaciju.