10.9. المصادقة للعملاء البرمجيين

الرموز الحاملة (Bearer tokens) هي الطريقة التي تصادق بها الهواتف، والمتحكمات الدقيقة المرافقة، وعمّال السحابة لدى الخادم -- إذ يضع العميل رمزاً في ترويسة Authorization في كل طلب ويتحقق منه الخادم. ويرسل تطبيق الهاتف المرافق طلب POST بـ "إقرار" عندما يضغط المالك على "تمت المشاهدة" في إشعار حركة؛ وتحتاج هذه الطلبات إلى رمز.

10.9.1. السر الموقِّع

السر هو المفتاح الخاص بالكاميرا لتوقيع الرموز والتحقق منها. أنشئ 32 بايتاً عشوائياً من مولّد الأرقام العشوائية العتادي عند أول إقلاع للكاميرا، واحفظها على نظام الملفات، وأعد استخدام نفس البايتات في كل إقلاع لاحق:

# 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() المصدر العشوائي المناسب تشفيرياً على كل منفذ من منافذ الكاميرا -- وعلى معظم المنافذ تقرأ مولّد الأرقام العشوائية العتادي للشريحة مباشرة. ويوجد الملف في دليل عمل الكاميرا (/sdcard إذا كانت بطاقة SD مركّبة، و/flash خلاف ذلك) ولا يغادر الكاميرا أبداً. وحذف secret.bin وإعادة الإقلاع يدوّر السر ويُبطل كل رمز صدر في ظل السر القديم.

ملاحظة

إن machine.unique_id() ليست بديلاً. فعلى بعض منافذ الكاميرا تُستخدم أيضاً لاشتقاق عنوان MAC للشبكة، ما يعني أن قيمتها تنتقل مع كل حزمة ترسلها الكاميرا -- وهذه ليست الخاصية التي يحتاجها السر الموقِّع.

10.9.2. إصدار الرموز

يحتاج الهاتف إلى وسيلة للحصول على رمز في المقام الأول. ويكفي رمز ويب JSON (JWT) قصير العمر -- وهو حمولة JSON مرمَّزة بـ base64 مع توقيع ملحق -- لمحاولة أولى: يقايض المالك كلمة مرور برمز، ثم يستخدم الرمز حتى انتهاء صلاحيته:

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}

يرسل الهاتف طلب POST بـ {user, pass} مرة واحدة، ويخزّن الرمز المُعاد في التخزين المحلي، ويضمّنه بصيغة Authorization: Bearer <token> في كل طلب لاحق.

إن مطالبة exp هي طابع زمني بصيغة Unix. ويعتمد المُتحقِّق في القسم التالي على هذه المطالبة لرفض الرموز منتهية الصلاحية تلقائياً -- ما يعني أنه يجب ضبط ساعة الكاميرا، وإلا فإن كل رمز سيبدو إما بعيداً في المستقبل أو منتهي الصلاحية بالفعل. راجع الوقت وبروتوكول NTP للاطلاع على وصفة المزامنة عبر NTP.

10.9.3. التحقق من الرموز باستخدام TokenAuth

إن microdot.auth.TokenAuth هو مصنع المُزخرِفات للمسارات التي تتطلب ترويسة Authorization: Bearer <token>. وتتلقى دالة رد نداء التحقق سلسلة الرمز وتُعيد أي كائن هوية ينبغي إرفاقه بالطلب -- أو None للرفض:

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() الاستثناء jwt.exceptions.PyJWTError (أو فئة فرعية منه) عندما يكون التوقيع خاطئاً، أو يكون الرمز مشوّهاً، أو تكون مطالبة exp قد انقضت. ويبتلع المُتحقِّق كل ذلك ويُعيد None، فيستجيب microdot بالرمز 401 دون أن يرى المعالج أي شيء.

عندما يُعيد المُتحقِّق قيمة غير None، يخزّنها microdot على request.g.current_user ليتمكن المعالج من قراءتها -- وهي في هذه الحالة مطالبة sub (الموضوع) في JWT.

10.9.4. BasicAuth للشبكات المغلقة

بالنسبة لنقطة نهاية إدارية لا يجري الوصول إليها إلا من شبكة موثوقة، تكون microdot.auth.BasicAuth أبسط -- إذ يُظهر المتصفح حواراً أصلياً لاسم المستخدم وكلمة المرور ويرسلهما في ترويسة Authorization: Basic ... في كل طلب:

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

تنتقل بيانات الاعتماد بشكل صريح ما لم يكن الاتصال عبر HTTPS، لذا لا يكون هذا النهج منطقياً إلا على شبكة موثوقة أو مع وجود HTTPS.

أصبح المساران /api/ack و/api/login الآن يفهمان الرموز؛ ويتطلب /admin مصادقة Basic.