10.9. Uwierzytelnianie dla klientow programowych¶
Tokeny typu Bearer to sposob, w jaki telefony, towarzyszace mikrokontrolery i procesy chmurowe uwierzytelniaja sie na serwerze – klient umieszcza token w naglowku Authorization w kazdym zadaniu, a serwer go sprawdza. Towarzyszaca aplikacja telefonu wysyla POST z „ack”, gdy wlasciciel dotknie „seen” na powiadomieniu o ruchu; te zadania potrzebuja tokenu.
10.9.1. Sekret podpisujacy¶
Sekret to prywatny klucz kamery do podpisywania i weryfikowania tokenow. Wygeneruj 32 losowe bajty ze sprzetowego RNG przy pierwszym uruchomieniu kamery, zapisz je trwale w systemie plikow i uzywaj tych samych bajtow przy kazdym kolejnym uruchomieniu:
# 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() jest kryptograficznie odpowiednim zrodlem losowosci na kazdym porcie kamery – na wiekszosci portow odczytuje bezposrednio sprzetowy generator liczb losowych chipa. Plik znajduje sie w katalogu roboczym kamery (/sdcard, jesli zamontowana jest karta SD, w przeciwnym razie /flash) i nigdy nie opuszcza kamery. Usuniecie secret.bin i ponowne uruchomienie zmienia sekret i uniewaznia kazdy token wydany pod poprzednim.
Informacja
machine.unique_id() nie jest zamiennikiem. Na niektorych portach kamery jest rowniez uzywany do wyprowadzenia adresu MAC sieci, co oznacza, ze jego wartosc podrozuje z kazdym pakietem wysylanym przez kamere – nie jest to wlasciwosc, ktorej potrzebuje sekret podpisujacy.
10.9.2. Wydawanie tokenow¶
Telefon potrzebuje przede wszystkim sposobu na uzyskanie tokenu. Krotkotrwaly JSON Web Token (JWT) – zakodowany w base64 ladunek JSON z dolaczonym podpisem – wystarczy na pierwsze podejscie: wlasciciel wymienia haslo na token, a nastepnie uzywa tokenu, az wygasnie:
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 raz wysyla POST z {user, pass}, zapisuje zwrocony token w pamieci lokalnej i dolacza go jako Authorization: Bearer <token> w kazdym kolejnym zadaniu.
Roszczenie exp to znacznik czasu Unix. Weryfikator w nastepnej sekcji polega na tym roszczeniu, aby automatycznie odrzucac wygasle tokeny – co oznacza, ze zegar scienny kamery musi byc ustawiony, poniewaz w przeciwnym razie kazdy token bedzie wygladal albo na daleki w przyszlosci, albo na juz wygasly. Zobacz Czas i NTP, aby poznac przepis na synchronizacje NTP.
10.9.3. Weryfikowanie tokenow za pomoca TokenAuth¶
microdot.auth.TokenAuth to fabryka dekoratorow dla tras, ktore wymagaja naglowka Authorization: Bearer <token>. Wywolanie zwrotne weryfikatora otrzymuje ciag tokenu i zwraca dowolny obiekt tozsamosci, ktory powinien zostac dolaczony do zadania – lub None, aby odrzucic:
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() zglasza jwt.exceptions.PyJWTError (lub podklase), gdy podpis jest nieprawidlowy, token jest zniekształcony lub roszczenie exp minelo. Weryfikator wychwytuje wszystkie te wyjatki i zwraca None, wiec microdot odpowiada kodem 401 bez tego, by handler cokolwiek zobaczyl.
Gdy weryfikator zwraca wartosc inna niz None, microdot zapisuje ja w request.g.current_user, aby handler mogl ja odczytac – w tym przypadku jest to roszczenie sub (subject) tokenu JWT.
10.9.4. BasicAuth dla sieci zamknietych¶
Dla punktu koncowego administracyjnego, do ktorego dostep zawsze odbywa sie z zaufanej sieci, microdot.auth.BasicAuth jest prostszy – przegladarka wyswietla natywne okno dialogowe nazwy uzytkownika/hasla i wysyla je w naglowku Authorization: Basic ... w kazdym zadaniu:
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
Poswiadczenia podrozuja jawnie, chyba ze polaczenie odbywa sie przez HTTPS, wiec to podejscie ma sens tylko w zaufanej sieci lub gdy dostepne jest HTTPS.
/api/ack i /api/login rozumieja teraz tokeny; /admin wymaga uwierzytelniania Basic.