10.9. Auth untuk klien programatik¶
Bearer token adalah cara ponsel, mikrokontroler pendamping, dan pekerja cloud mengautentikasi ke server -- klien menempatkan token di header Authorization pada setiap permintaan dan server memeriksanya. Aplikasi ponsel pendamping melakukan POST "ack" ketika pemilik mengetuk "terlihat" pada notifikasi gerakan; permintaan tersebut membutuhkan token.
10.9.1. Rahasia penandatanganan¶
Rahasia adalah kunci privat cam untuk menandatangani dan memverifikasi token. Buat 32 byte acak dari RNG hardware pada boot pertama cam, simpan ke filesystem, dan gunakan kembali byte yang sama di setiap boot berikutnya:
# 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() adalah sumber acak yang sesuai secara kriptografis pada setiap port cam -- pada sebagian besar port ia membaca generator angka acak hardware chip secara langsung. File ini berada di direktori kerja cam (/sdcard jika kartu SD terpasang, /flash jika tidak) dan tidak pernah meninggalkan cam. Menghapus secret.bin dan melakukan reboot merotasi rahasia dan membatalkan setiap token yang diterbitkan di bawah rahasia lama.
Catatan
machine.unique_id() bukan penggantinya. Pada beberapa port cam, ini juga digunakan untuk mendapatkan alamat MAC jaringan, yang berarti nilainya ikut bersama setiap paket yang dikirim cam -- bukan properti yang dibutuhkan rahasia penandatanganan.
10.9.2. Menerbitkan token¶
Ponsel membutuhkan cara untuk mendapatkan token pada awalnya. JSON Web Token (JWT) berumur pendek -- payload JSON yang dikodekan base64 dengan tanda tangan yang ditambahkan -- sudah cukup untuk tahap pertama: pemilik menukar kata sandi dengan token, lalu menggunakan token hingga kedaluwarsa:
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}
Ponsel melakukan POST {user, pass} sekali, menyimpan token yang dikembalikan di penyimpanan lokal, dan menyertakannya sebagai Authorization: Bearer <token> pada setiap permintaan berikutnya.
Klaim exp adalah Unix timestamp. Verifikator di bagian berikutnya mengandalkan klaim ini untuk menolak token yang sudah kedaluwarsa secara otomatis -- yang berarti jam dinding cam harus diatur, karena jika tidak setiap token akan tampak jauh di masa depan atau sudah kedaluwarsa. Lihat Waktu dan NTP untuk resep sinkronisasi NTP.
10.9.3. Memverifikasi token dengan TokenAuth¶
microdot.auth.TokenAuth adalah pabrik dekorator untuk rute yang membutuhkan header Authorization: Bearer <token>. Callback verifikator menerima string token dan mengembalikan objek identitas apa pun yang harus dilampirkan ke permintaan -- atau None untuk menolak:
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() memunculkan jwt.exceptions.PyJWTError (atau subkelas) ketika tanda tangan salah, token tidak valid, atau klaim exp telah berlalu. Verifikator menelan semua itu dan mengembalikan None, sehingga microdot merespons 401 tanpa handler melihat apa pun.
Ketika verifikator mengembalikan nilai non-None, microdot menyimpannya di request.g.current_user sehingga handler dapat membacanya -- dalam hal ini adalah klaim JWT sub (subjek).
10.9.4. BasicAuth untuk jaringan tertutup¶
Untuk endpoint admin yang hanya diakses dari jaringan tepercaya, microdot.auth.BasicAuth lebih sederhana -- browser menampilkan dialog nama pengguna/kata sandi bawaan dan mengirimnya di header Authorization: Basic ... pada setiap permintaan:
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
Kredensial berpindah dalam teks biasa kecuali koneksinya HTTPS, jadi pendekatan ini hanya masuk akal di jaringan tepercaya atau dengan HTTPS di tempat.
/api/ack dan /api/login sekarang memahami token; /admin memerlukan autentikasi Basic.