10.9. การพิสูจน์ตัวตนสำหรับไคลเอนต์แบบโปรแกรม

Bearer token คือวิธีที่โทรศัพท์, ไมโครคอนโทรลเลอร์คู่หู และ cloud worker พิสูจน์ตัวตนกับเซิร์ฟเวอร์ -- ไคลเอนต์ใส่ token ใน Authorization header ในทุกคำขอและเซิร์ฟเวอร์ตรวจสอบ แอปโทรศัพท์คู่หูส่ง POST "ack" เมื่อเจ้าของแตะ "เห็น" บนการแจ้งเตือนการเคลื่อนไหว คำขอเหล่านั้นต้องการ token

10.9.1. ความลับการลงนาม

ความลับคือคีย์ส่วนตัวของกล้องสำหรับการลงนามและการยืนยัน token สร้าง 32 ไบต์สุ่มจาก hardware RNG บนการบูตครั้งแรกของกล้อง บันทึกลงในระบบไฟล์ และนำไบต์เดียวกันมาใช้ซ้ำในทุกการบูตถัดไป:

# 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 และรีบูตจะหมุนเวียนความลับและทำให้ token ทุกตัวที่ออกภายใต้ตัวเก่าไม่ถูกต้อง

Note

machine.unique_id() ไม่ใช่ สิ่งทดแทน บนพอร์ตกล้องบางพอร์ต มันยังถูกใช้เพื่อสร้างที่อยู่ MAC เครือข่าย ซึ่งหมายความว่าค่าของมันเดินทางกับทุกแพ็กเก็ตที่กล้องส่ง -- ไม่ใช่คุณสมบัติที่ความลับการลงนามต้องการ

10.9.2. การออก token

โทรศัพท์ต้องการวิธีที่จะ ได้รับ token ในตอนแรก JSON Web Token (JWT) อายุสั้น -- เพย์โหลด JSON ที่เข้ารหัส base64 พร้อมลายเซ็นต่อท้าย -- เพียงพอสำหรับการผ่านครั้งแรก: เจ้าของแลกรหัสผ่านกับ token จากนั้นใช้ token จนหมดอายุ:

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} ครั้งเดียว เก็บ token ที่ส่งคืนใน local storage และรวมมันเป็น Authorization: Bearer <token> ในทุกคำขอถัดไป

claim exp คือ Unix timestamp ตัวตรวจสอบในส่วนถัดไปอาศัย claim นี้เพื่อปฏิเสธ token ที่หมดอายุโดยอัตโนมัติ -- ซึ่งหมายความว่านาฬิกาแขวนของกล้องต้องตั้งค่า เพราะมิฉะนั้น token ทุกตัวจะดูเหมือนอยู่ในอนาคตไกลหรือหมดอายุแล้ว ดู เวลาและ NTP สำหรับสูตร NTP-sync

10.9.3. การยืนยัน token ด้วย TokenAuth

microdot.auth.TokenAuth คือ decorator factory สำหรับเส้นทางที่ต้องการ Authorization: Bearer <token> header คอลแบ็กตัวตรวจสอบรับสตริง token และส่งคืนออบเจกต์ identity ใดก็ตามที่ควรแนบกับคำขอ -- หรือ 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 (หรือคลาสย่อย) เมื่อลายเซ็นผิด token มีรูปแบบไม่ถูกต้อง หรือ claim exp ผ่านไปแล้ว ตัวตรวจสอบกลืนทั้งหมดนั้นและส่งคืน None ดังนั้น microdot ตอบสนอง 401 โดยที่ handler ไม่เห็นอะไร

เมื่อตัวตรวจสอบส่งคืนค่าที่ไม่ใช่ None microdot จะจัดเก็บไว้ใน request.g.current_user เพื่อให้ handler อ่านได้ -- ในกรณีนี้คือ sub (subject) claim ของ JWT

10.9.4. BasicAuth สำหรับเครือข่ายปิด

สำหรับ admin endpoint ที่ถูกเข้าถึงจากเครือข่ายที่เชื่อถือได้เท่านั้น microdot.auth.BasicAuth ง่ายกว่า -- เบราว์เซอร์แสดงกล่องโต้ตอบชื่อผู้ใช้/รหัสผ่านของระบบเนทีฟและส่งมันใน Authorization: Basic ... header ในทุกคำขอ:

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 ตอนนี้เข้าใจ token /admin ต้องการการพิสูจน์ตัวตนแบบ Basic