10.9. Xác thực cho các client lập trình

Bearer token là cách điện thoại, vi điều khiển đồng hành và worker trên đám mây xác thực với server -- client đặt token trong header Authorization trên mỗi yêu cầu và server kiểm tra nó. Ứng dụng điện thoại đồng hành gửi POST "ack" khi chủ sở hữu nhấn "đã xem" trên thông báo chuyển động; các yêu cầu đó cần một token.

10.9.1. Bí mật ký

Bí mật là khóa riêng tư của cam để ký và xác minh token. Tạo 32 byte ngẫu nhiên từ RNG phần cứng trên lần khởi động đầu tiên của cam, lưu trữ vào hệ thống tập tin và tái sử dụng cùng các byte đó cho mỗi lần khởi động tiếp theo:

# 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() là nguồn ngẫu nhiên phù hợp về mặt mật mã trên mỗi cổng cam -- trên hầu hết các cổng nó đọc trực tiếp từ bộ tạo số ngẫu nhiên phần cứng của chip. Tập tin nằm trong thư mục làm việc của cam (/sdcard nếu thẻ SD được gắn, /flash trong trường hợp khác) và không bao giờ rời khỏi cam. Xóa secret.bin và khởi động lại sẽ xoay vòng bí mật và vô hiệu hóa mọi token được cấp dưới bí mật cũ.

Ghi chú

machine.unique_id() không phải là thay thế. Trên một số cổng cam nó cũng được sử dụng để suy ra địa chỉ MAC mạng, có nghĩa là giá trị của nó đi cùng với mọi gói tin mà cam gửi -- đây không phải là thuộc tính mà một bí mật ký cần có.

10.9.2. Cấp phát token

Điện thoại cần một cách để nhận token ngay từ đầu. Một JSON Web Token (JWT) có thời hạn ngắn -- một payload JSON được mã hóa base64 với chữ ký được thêm vào -- là đủ cho lần đầu: chủ sở hữu đổi mật khẩu lấy token, sau đó sử dụng token cho đến khi nó hết hạn:

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}

Điện thoại gửi POST {user, pass} một lần, lưu trữ token trả về trong local storage và bao gồm nó dưới dạng Authorization: Bearer <token> trên mọi yêu cầu tiếp theo.

Claim exp là timestamp Unix. Bộ xác minh ở phần tiếp theo dựa vào claim này để tự động từ chối các token đã hết hạn -- điều đó có nghĩa là đồng hồ treo tường của cam phải được đặt, vì nếu không mọi token sẽ trông có vẻ còn rất lâu trong tương lai hoặc đã hết hạn. Xem Thời gian và NTP để biết công thức đồng bộ NTP.

10.9.3. Xác minh token với TokenAuth

microdot.auth.TokenAuth là factory decorator cho các route yêu cầu header Authorization: Bearer <token>. Callback xác minh nhận chuỗi token và trả về bất kỳ đối tượng nhận dạng nào cần được đính kèm vào yêu cầu -- hoặc None để từ chối:

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() ném ra jwt.exceptions.PyJWTError (hoặc một lớp con) khi chữ ký sai, token không hợp lệ, hoặc claim exp đã qua. Bộ xác minh nuốt tất cả những lỗi đó và trả về None, vì vậy microdot phản hồi 401 mà handler không thấy bất kỳ điều gì.

Khi bộ xác minh trả về giá trị khác None, microdot lưu trữ nó trên request.g.current_user để handler có thể đọc -- trong trường hợp này đó là claim sub (subject) của JWT.

10.9.4. BasicAuth cho mạng nội bộ

Đối với endpoint quản trị chỉ được truy cập từ mạng tin cậy, microdot.auth.BasicAuth đơn giản hơn -- trình duyệt hiển thị hộp thoại tên người dùng/mật khẩu gốc và gửi chúng trong header Authorization: Basic ... trên mỗi yêu cầu:

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

Thông tin xác thực được truyền dưới dạng văn bản rõ ràng trừ khi kết nối là HTTPS, vì vậy cách tiếp cận này chỉ hợp lý trên mạng tin cậy hoặc khi HTTPS đã được triển khai.

/api/ack/api/login hiện đã hiểu token; /admin yêu cầu xác thực Basic.