10.9. Programatik istemciler için kimlik doğrulama

Bearer token’ları, telefonların, eşlik eden mikrodenetleyicilerin ve bulut çalışanlarının bir sunucuda kimlik doğrulaması yapma şeklidir – istemci, her istekte Authorization başlığına bir token koyar ve sunucu bunu denetler. Eşlik eden telefon uygulaması, sahibi bir hareket bildiriminde “görüldü” simgesine dokunduğunda bir “ack” gönderir (POST); bu istekler bir token gerektirir.

10.9.1. İmzalama gizli anahtarı

Gizli anahtar, kameranın token’ları imzalamak ve doğrulamak için kullandığı özel anahtarıdır. Kameranın ilk açılışında donanım RNG’sinden 32 rastgele bayt üretin, bunları dosya sistemine kalıcı olarak kaydedin ve sonraki her açılışta aynı baytları yeniden kullanın:

# 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(), her kamera portunda kriptografik olarak uygun rastgele kaynaktır – çoğu portta yongun donanım rastgele sayı üretecini doğrudan okur. Dosya, kameranın çalışma dizininde (bir SD kart takılıysa /sdcard, aksi takdirde /flash) bulunur ve kameradan asla ayrılmaz. secret.bin dosyasını silip yeniden başlatmak, gizli anahtarı döndürür ve eski anahtar altında verilen her token’ı geçersiz kılar.

Not

machine.unique_id() bir alternatif değildir. Bazı kamera portlarında ağ MAC adresini türetmek için de kullanılır; bu da değerinin kameranın gönderdiği her paketle birlikte gittiği anlamına gelir – bir imzalama gizli anahtarının ihtiyaç duyduğu bir özellik değil.

10.9.2. Token verme

Telefonun öncelikle bir token almak için bir yola ihtiyacı vardır. Kısa ömürlü bir JSON Web Token (JWT) – imzası eklenmiş base64 kodlanmış bir JSON yükü – ilk geçiş için yeterlidir: sahip, bir parolayı bir token ile takas eder, ardından token’ı süresi dolana kadar kullanı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}

Telefon, bir kez {user, pass} gönderir (POST), dönen token’ı yerel depolamada saklar ve sonraki her istekte onu Authorization: Bearer <token> olarak ekler.

exp talebi (claim) bir Unix zaman damgasıdır. Bir sonraki bölümdeki doğrulayıcı, süresi dolmuş token’ları otomatik olarak reddetmek için bu talebe dayanır – bu da kameranın duvar saatinin ayarlanması gerektiği anlamına gelir, çünkü aksi takdirde her token ya çok ileri bir tarihteymiş ya da çoktan süresi dolmuş gibi görünecektir. NTP eşitleme tarifi için bkz. Zaman ve NTP.

10.9.3. Token’ları TokenAuth ile doğrulama

microdot.auth.TokenAuth, Authorization: Bearer <token> başlığı gerektiren rotalar için dekoratör fabrikasıdır. Doğrulayıcı geri çağırması (callback), token dizesini alır ve isteğe eklenecek kimlik nesnesini – ya da reddetmek için None – döndürür:

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(), imza yanlış olduğunda, token bozuk olduğunda ya da exp talebinin (claim) süresi dolduğunda jwt.exceptions.PyJWTError (ya da bir alt sınıfını) yükseltir. Doğrulayıcı bunların tümünü yutar ve None döndürür, böylece microdot, işleyicinin hiçbir şey görmesine gerek kalmadan 401 yanıtı verir.

Doğrulayıcı None olmayan bir değer döndürdüğünde, microdot bunu request.g.current_user üzerinde saklar, böylece işleyici onu okuyabilir – bu durumda bu, JWT sub (subject) talebidir (claim).

10.9.4. Kapalı ağlar için BasicAuth

Yalnızca güvenilir bir ağdan erişilen bir yönetici uç noktası için, microdot.auth.BasicAuth daha basittir – tarayıcı yerel bir kullanıcı adı/parola iletişim kutusu açar ve bunları her istekte Authorization: Basic ... başlığında gönderir:

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

Kimlik bilgileri, bağlantı HTTPS olmadıkça açık metin olarak gider, dolayısıyla bu yaklaşım yalnızca güvenilir bir ağda ya da HTTPS devredeyken anlamlıdır.

/api/ack ve /api/login artık token’ları anlıyor; /admin ise Basic auth gerektiriyor.