10.9. Todennus ohjelmallisille asiakkaille

Bearer-tunnukset (bearer tokens) ovat tapa, jolla puhelimet, kumppanimikrokontrollerit ja pilvityöntekijät todentautuvat palvelimelle – asiakas asettaa tunnuksen Authorization-otsakkeeseen jokaisessa pyynnössä ja palvelin tarkistaa sen. Kumppanipuhelinsovellus POSTaa ”ack”-vahvistuksen, kun omistaja napauttaa ”nähty” liikeilmoituksessa; nämä pyynnöt tarvitsevat tunnuksen.

10.9.1. Allekirjoitussalaisuus

Salaisuus on kameran yksityinen avain tunnusten allekirjoittamiseen ja varmentamiseen. Luo 32 satunnaista tavua laitteiston RNG:stä kameran ensimmäisellä käynnistyksellä, säilytä ne tiedostojärjestelmässä ja käytä samoja tavuja uudelleen jokaisella seuraavalla käynnistyksellä:

# 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() on kryptografisesti sopiva satunnaislähde jokaisessa kameran portissa – useimmissa porteissa se lukee suoraan sirun laitteistopohjaista satunnaislukugeneraattoria. Tiedosto sijaitsee kameran työhakemistossa (/sdcard, jos SD-kortti on liitetty, muutoin /flash) eikä koskaan poistu kamerasta. secret.bin-tiedoston poistaminen ja uudelleenkäynnistys kierrättää salaisuuden ja mitätöi jokaisen vanhalla salaisuudella myönnetyn tunnuksen.

Muista

machine.unique_id() ei ole korvike. Joissakin kameran porteissa sitä käytetään myös verkon MAC-osoitteen johtamiseen, mikä tarkoittaa, että sen arvo kulkee mukana jokaisessa kameran lähettämässä paketissa – ei ominaisuus, jota allekirjoitussalaisuus tarvitsee.

10.9.2. Tunnusten myöntäminen

Puhelin tarvitsee ensinnäkin tavan saada tunnus. Lyhytikäinen JSON Web Token (JWT) – base64-koodattu JSON-hyötykuorma allekirjoituksella varustettuna – riittää ensimmäiseen vaiheeseen: omistaja vaihtaa salasanan tunnukseen ja käyttää sitten tunnusta, kunnes se vanhenee:

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}

Puhelin POSTaa {user, pass} kerran, tallentaa palautetun tunnuksen paikalliseen tallennustilaan ja sisällyttää sen muodossa Authorization: Bearer <token> jokaiseen seuraavaan pyyntöön.

exp-vaade on Unix-aikaleima. Seuraavan osion varmentaja luottaa tähän vaateeseen hylätessään vanhentuneet tunnukset automaattisesti – mikä tarkoittaa, että kameran seinäkello on asetettava, koska muutoin jokainen tunnus näyttää joko kaukana tulevaisuudessa olevalta tai jo vanhentuneelta. Katso NTP-synkronointiohje kohdasta Aika ja NTP.

10.9.3. Tunnusten varmentaminen TokenAuthilla

microdot.auth.TokenAuth on koristelijatehdas reiteille, jotka vaativat Authorization: Bearer <token> -otsakkeen. Varmentajan takaisinkutsu vastaanottaa tunnusmerkkijonon ja palauttaa minkä tahansa identiteettiobjektin, joka pyyntöön tulisi liittää – tai None, jos pyyntö hylätään:

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() nostaa poikkeuksen jwt.exceptions.PyJWTError (tai sen aliluokan), kun allekirjoitus on virheellinen, tunnus on virheellisesti muodostettu tai exp-vaade on mennyt umpeen. Varmentaja nielaisee kaikki nämä ja palauttaa None, joten microdot vastaa 401:llä ilman että käsittelijä näkee mitään.

Kun varmentaja palauttaa arvon, joka ei ole None, microdot tallentaa sen kohteeseen request.g.current_user, jotta käsittelijä voi lukea sen – tässä tapauksessa se on JWT:n sub-vaade (subject).

10.9.4. BasicAuth suljetuille verkoille

Hallintapäätepisteelle, johon otetaan yhteyttä vain luotetusta verkosta, microdot.auth.BasicAuth on yksinkertaisempi – selain avaa natiivin käyttäjätunnus/salasana-ikkunan ja lähettää ne Authorization: Basic ... -otsakkeessa jokaisessa pyynnössä:

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

Kirjautumistiedot kulkevat selkokielisinä, ellei yhteys ole HTTPS, joten tämä lähestymistapa on järkevä vain luotetussa verkossa tai HTTPS:n ollessa käytössä.

/api/ack ja /api/login ymmärtävät nyt tunnuksia; /admin vaatii Basic-todennuksen.