10.9. Authenticatie voor programmatische clients¶
Bearer-tokens zijn de manier waarop telefoons, begeleidende microcontrollers en cloud-workers zich bij een server authenticeren – de client zet een token in de Authorization-header bij elk verzoek en de server controleert deze. De begeleidende telefoon-app POST een “ack” wanneer de eigenaar op “gezien” tikt bij een bewegingsmelding; die verzoeken hebben een token nodig.
10.9.1. Het ondertekengeheim¶
Het geheim is de privésleutel van de cam voor het ondertekenen en verifiëren van tokens. Genereer 32 willekeurige bytes uit de hardware-RNG bij de eerste keer opstarten van de cam, bewaar ze op het bestandssysteem en hergebruik dezelfde bytes bij elke volgende keer opstarten:
# 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() is de cryptografisch geschikte willekeurige bron op elke cam-port – op de meeste ports leest het de hardware-willekeurige-nummergenerator van de chip rechtstreeks uit. Het bestand bevindt zich in de werkmap van de cam (/sdcard als er een SD-kaart is aangekoppeld, anders /flash) en verlaat de cam nooit. Het verwijderen van secret.bin en opnieuw opstarten roteert het geheim en maakt elke token die onder het oude geheim is uitgegeven ongeldig.
Notitie
machine.unique_id() is geen vervanging. Op sommige cam-ports wordt het ook gebruikt om het netwerk-MAC-adres af te leiden, wat betekent dat de waarde ervan met elk pakket dat de cam verzendt meereist – niet de eigenschap die een ondertekengeheim nodig heeft.
10.9.2. Tokens uitgeven¶
De telefoon heeft eerst een manier nodig om een token te krijgen. Een kortlevend JSON Web Token (JWT) – een base64-gecodeerde JSON-payload met een handtekening eraan vast – is genoeg voor een eerste opzet: de eigenaar ruilt een wachtwoord in voor een token en gebruikt de token vervolgens totdat deze verloopt:
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}
De telefoon POST eenmalig {user, pass}, slaat de teruggegeven token op in local storage en voegt deze als Authorization: Bearer <token> toe aan elk volgend verzoek.
De exp-claim is een Unix-tijdstempel. De verifier in de volgende sectie vertrouwt op deze claim om verlopen tokens automatisch te weigeren – wat betekent dat de wandklok van de cam moet zijn ingesteld, omdat anders elke token er ofwel ver in de toekomst ofwel al verlopen uitziet. Zie Tijd en NTP voor het NTP-synchronisatierecept.
10.9.3. Tokens verifiëren met TokenAuth¶
microdot.auth.TokenAuth is de decorator-fabriek voor routes die een Authorization: Bearer <token>-header vereisen. De verifier-callback ontvangt de tokenstring en geeft terug welk identiteitsobject er ook aan het verzoek moet worden gekoppeld – of None om te weigeren:
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() werpt jwt.exceptions.PyJWTError (of een subklasse) op wanneer de handtekening verkeerd is, de token ongeldig opgemaakt is, of de exp-claim is verstreken. De verifier slikt al die uitzonderingen in en geeft None terug, zodat microdot met 401 reageert zonder dat de handler iets ziet.
Wanneer de verifier een niet-None-waarde teruggeeft, slaat microdot deze op in request.g.current_user zodat de handler hem kan lezen – in dit geval is dat de JWT-sub-claim (subject).
10.9.4. BasicAuth voor gesloten netwerken¶
Voor een admin-endpoint dat alleen ooit vanaf een vertrouwd netwerk wordt benaderd, is microdot.auth.BasicAuth eenvoudiger – de browser opent een native gebruikersnaam/wachtwoord-dialoog en stuurt ze in de Authorization: Basic ...-header bij elk verzoek:
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
De inloggegevens reizen onversleuteld tenzij de verbinding HTTPS is, dus deze aanpak is alleen zinvol op een vertrouwd netwerk of met HTTPS aanwezig.
/api/ack en /api/login begrijpen nu tokens; /admin vereist Basic-authenticatie.