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.