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.