10.9. Autentisering för programmatiska klienter¶
Bärartokens är hur telefoner, medföljande mikrokontroller och molnarbetare autentiserar sig mot en server – klienten lägger en token i Authorization-headern på varje begäran och servern kontrollerar den. Den medföljande telefonappen POST:ar en ”ack” när ägaren trycker på ”sedd” i en rörelsenotis; dessa begäranden behöver en token.
10.9.1. Signeringshemligheten¶
Hemligheten är kamerans privata nyckel för att signera och verifiera tokens. Generera 32 slumpmässiga byte från hårdvaru-RNG:n vid kamerans första uppstart, spara dem beständigt till filsystemet och återanvänd samma byte vid varje efterföljande uppstart:
# 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() är den kryptografiskt lämpliga slumpkällan på varje kameraport – på de flesta portar läser den direkt från chipets hårdvarubaserade slumptalsgenerator. Filen ligger i kamerans arbetskatalog (/sdcard om ett SD-kort är monterat, annars /flash) och lämnar aldrig kameran. Att radera secret.bin och starta om roterar hemligheten och ogiltigförklarar varje token som utfärdats under den gamla.
Anteckning
machine.unique_id() är inte en ersättning. På vissa kameraportar används den även för att härleda nätverkets MAC-adress, vilket innebär att dess värde följer med varje paket kameran skickar – inte den egenskap en signeringshemlighet behöver.
10.9.2. Att utfärda tokens¶
Telefonen behöver ett sätt att över huvud taget få en token. En kortlivad JSON Web Token (JWT) – en base64-kodad JSON-nyttolast med en signatur tillagd – räcker för ett första steg: ägaren byter ett lösenord mot en token och använder sedan token tills den löper ut:
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}
Telefonen POST:ar {user, pass} en gång, lagrar den returnerade token i lokal lagring och inkluderar den som Authorization: Bearer <token> på varje efterföljande begäran.
exp-anspråket är en Unix-tidsstämpel. Verifieraren i nästa avsnitt förlitar sig på detta anspråk för att automatiskt avvisa utgångna tokens – vilket innebär att kamerans väggklocka måste vara inställd, eftersom varje token annars kommer att se ut antingen som långt in i framtiden eller redan utgången. Se Tid och NTP för receptet på NTP-synkronisering.
10.9.3. Att verifiera tokens med TokenAuth¶
microdot.auth.TokenAuth är dekoratorfabriken för rutter som kräver en Authorization: Bearer <token>-header. Verifierarens återanrop tar emot tokensträngen och returnerar vilket identitetsobjekt som ska kopplas till begäran – eller None för att avvisa:
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() höjer jwt.exceptions.PyJWTError (eller en underklass) när signaturen är felaktig, token är felformaterad eller exp-anspråket har passerats. Verifieraren sväljer alla dessa och returnerar None, så microdot svarar 401 utan att hanteraren ser något.
När verifieraren returnerar ett värde som inte är None lagrar microdot det på request.g.current_user så att hanteraren kan läsa det – i det här fallet är det JWT:ns sub-anspråk (subject).
10.9.4. BasicAuth för slutna nätverk¶
För en administrationsslutpunkt som bara någonsin nås från ett betrott nätverk är microdot.auth.BasicAuth enklare – webbläsaren visar en inbyggd dialogruta för användarnamn/lösenord och skickar dem i Authorization: Basic ...-headern på varje begäran:
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
Inloggningsuppgifterna färdas i klartext om inte anslutningen är HTTPS, så detta tillvägagångssätt är bara meningsfullt på ett betrott nätverk eller med HTTPS på plats.
/api/ack och /api/login förstår nu tokens; /admin kräver Basic-autentisering.