10.9. Hitelesítés programozott klienseknek

A bearer tokenek azok, amelyekkel a telefonok, a kísérő mikrovezérlők és a felhőbeli munkavégzők hitelesítik magukat egy szerver felé – a kliens minden kérésben elhelyez egy tokent az Authorization fejlécben, a szerver pedig ellenőrzi azt. A kísérő telefonos alkalmazás egy „ack” üzenetet POST-ol, amikor a tulajdonos egy mozgásértesítésnél a „látott” gombra koppint; ezek a kérések tokent igényelnek.

10.9.1. Az aláíró titok

A titok a kamera privát kulcsa a tokenek aláírásához és ellenőrzéséhez. A kamera első indításakor generálj 32 véletlenszerű bájtot a hardveres RNG-ből, tartsd meg azokat a fájlrendszerben, és minden későbbi indításkor használd újra ugyanazokat a bájtokat:

# 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)

Az os.urandom() a kriptográfiailag megfelelő véletlenforrás minden kamera porton – a legtöbb porton közvetlenül a chip hardveres véletlenszám-generátorát olvassa. A fájl a kamera munkakönyvtárában található (/sdcard, ha SD-kártya van csatlakoztatva, egyébként /flash), és soha nem hagyja el a kamerát. A secret.bin törlése és az újraindítás lecseréli a titkot, és érvényteleníti a régi alatt kiadott összes tokent.

Megjegyzés

A machine.unique_id() nem helyettesíti ezt. Egyes kamera portokon a hálózati MAC-cím származtatására is használják, ami azt jelenti, hogy az értéke minden csomaggal együtt utazik, amelyet a kamera küld – nem ez az a tulajdonság, amelyre egy aláíró titoknak szüksége van.

10.9.2. Tokenek kiadása

A telefonnak először is szüksége van valamilyen módra, hogy megszerezzen egy tokent. Egy rövid élettartamú JSON Web Token (JWT) – egy base64-kódolt JSON hasznos teher hozzáfűzött aláírással – elegendő egy első megközelítéshez: a tulajdonos jelszót cserél tokenre, majd a tokent használja, amíg le nem jár:

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}

A telefon egyszer POST-olja a {user, pass} adatokat, eltárolja a visszakapott tokent a helyi tárolóban, és minden további kérésben Authorization: Bearer <token> formában mellékeli.

Az exp állítás (claim) egy Unix időbélyeg. A következő szakaszban szereplő ellenőrző erre az állításra támaszkodik, hogy automatikusan elutasítsa a lejárt tokeneket – ami azt jelenti, hogy a kamera valós idejű óráját be kell állítani, mert egyébként minden token vagy távol a jövőben, vagy már lejártnak fog tűnni. Az NTP-szinkronizálás receptjéhez lásd: Idő és NTP.

10.9.3. Tokenek ellenőrzése a TokenAuth segítségével

A microdot.auth.TokenAuth a dekorátorgyár olyan útvonalakhoz, amelyek Authorization: Bearer <token> fejlécet igényelnek. Az ellenőrző visszahívás megkapja a token sztringet, és visszaadja azt az identitásobjektumot, amelyet a kéréshez kell csatolni – vagy None értéket az elutasításhoz:

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}

A jwt.decode() jwt.exceptions.PyJWTError kivételt (vagy egy alosztályát) vált ki, ha az aláírás hibás, a token hibásan formázott, vagy az exp állítás már lejárt. Az ellenőrző mindezeket lenyeli, és None értéket ad vissza, így a microdot 401-gyel válaszol anélkül, hogy a kezelő bármit is látna.

Amikor az ellenőrző nem None értéket ad vissza, a microdot eltárolja azt a request.g.current_user mezőben, hogy a kezelő kiolvashassa – ebben az esetben ez a JWT sub (subject) állítása.

10.9.4. BasicAuth zárt hálózatokhoz

Egy olyan adminisztrációs végponthoz, amelyet csak megbízható hálózatról érnek el, a microdot.auth.BasicAuth egyszerűbb – a böngésző egy natív felhasználónév/jelszó párbeszédablakot dob fel, és minden kérésben elküldi azokat az Authorization: Basic ... fejlécben:

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

A hitelesítő adatok nyíltan utaznak, hacsak a kapcsolat nem HTTPS, így ennek a megközelítésnek csak megbízható hálózaton vagy HTTPS megléte esetén van értelme.

Az /api/ack és az /api/login mostantól érti a tokeneket; az /admin Basic hitelesítést igényel.