10.9. Ověřování pro programové klienty¶
Bearer tokeny jsou způsob, jakým se telefony, doprovodné mikrokontroléry a cloudoví workeři autentizují vůči serveru – klient vloží token do hlavičky Authorization v každém požadavku a server jej ověří. Doprovodná telefonní aplikace odešle POST s „ack“, když majitel klepne na „viděno“ u oznámení o pohybu; tyto požadavky potřebují token.
10.9.1. Podpisové tajemství¶
Tajemství je soukromý klíč kamery pro podepisování a ověřování tokenů. Při prvním spuštění kamery vygenerujte 32 náhodných bajtů z hardwarového RNG, uložte je trvale do souborového systému a opětovně používejte tytéž bajty při každém dalším spuštění:
# 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 kryptograficky vhodný zdroj náhodnosti na každém portu kamery – na většině portů čte přímo hardwarový generátor náhodných čísel čipu. Soubor se nachází v pracovním adresáři kamery (/sdcard, pokud je připojena SD karta, jinak /flash) a kameru nikdy neopustí. Smazání secret.bin a restart obmění tajemství a zneplatní každý token vydaný pod tím starým.
Poznámka
machine.unique_id() není náhrada. Na některých portech kamery se používá i k odvození síťové MAC adresy, což znamená, že jeho hodnota putuje s každým paketem, který kamera odešle – nikoli vlastnost, kterou podpisové tajemství potřebuje.
10.9.2. Vydávání tokenů¶
Telefon potřebuje nejprve nějaký způsob, jak token získat. Krátkodobý JSON Web Token (JWT) – JSON payload zakódovaný v base64 s připojeným podpisem – pro první přiblížení stačí: majitel vymění heslo za token a poté token používá, dokud nevyprší jeho platnost:
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 jednou odešle POST {user, pass}, vrácený token uloží do lokálního úložiště a v každém dalším požadavku jej zahrne jako Authorization: Bearer <token>.
Nárok exp je unixový časový údaj. Ověřovatel v další části se na tento nárok spoléhá při automatickém odmítání vypršených tokenů – což znamená, že hodiny kamery musí být nastaveny, protože jinak bude každý token vypadat buď daleko v budoucnosti, nebo již vypršelý. Návod na synchronizaci přes NTP najdete v Čas a NTP.
10.9.3. Ověřování tokenů pomocí TokenAuth¶
microdot.auth.TokenAuth je továrna na dekorátory pro trasy, které vyžadují hlavičku Authorization: Bearer <token>. Ověřovací callback obdrží řetězec tokenu a vrátí libovolný objekt identity, který má být připojen k požadavku – nebo None pro odmítnutí:
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() vyvolá jwt.exceptions.PyJWTError (nebo podtřídu), když je podpis nesprávný, token poškozený nebo nárok exp již uplynul. Ověřovatel všechny tyto případy pohltí a vrátí None, takže microdot odpoví 401, aniž by handler cokoli viděl.
Když ověřovatel vrátí hodnotu odlišnou od None, microdot ji uloží do request.g.current_user, aby ji handler mohl přečíst – v tomto případě je to nárok sub (subjekt) z JWT.
10.9.4. BasicAuth pro uzavřené sítě¶
Pro administrátorský endpoint, na který se přistupuje vždy jen z důvěryhodné sítě, je microdot.auth.BasicAuth jednodušší – prohlížeč vyvolá nativní dialog pro zadání uživatelského jména a hesla a odešle je v hlavičce Authorization: Basic ... v každém požadavku:
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
Přihlašovací údaje putují v otevřené podobě, pokud připojení není HTTPS, takže tento přístup dává smysl pouze v důvěryhodné síti nebo se zavedeným HTTPS.
/api/ack a /api/login nyní rozumějí tokenům; /admin vyžaduje Basic ověřování.