OAuth 2.0 Device Authorization Grant (RFC 8628) dirancang untuk perangkat dengan keterbatasan input teks atau ketiadaan peramban web mandiri, seperti Smart TV, konsol game, CLI terminal, dan perangkat IoT. Alur ini memisahkan kanal otorisasi menjadi dua: perangkat melakukan polling pada endpoint token menggunakan device_code, sementara pengguna menyelesaikan autentikasi di perangkat sekunder (ponsel/laptop) dengan memasukkan user_code.
Pemisahan kanal tersebut membuka celah keamanan spesifik. Format user_code yang sengaja dibuat pendek agar ramah ketik rentan terhadap serangan brute-force, frekuensi polling klien yang tidak terkendali dapat membebani backend (Polling DoS), dan relasi antara sesi perangkat dengan persetujuan pengguna rentan disusupi session fixation. Artikel ini membahas langkah konkret hardening arsitektur RFC 8628 pada backend service.
1. Desain Entropi user_code dan Mitigasi Brute-Force
Dilema utama user_code adalah kompromi antara kemudahan input manusia (usability) dan resistensi terhadap pencarian acak (brute-force). Menggunakan UUID atau string 32 karakter menghancurkan pengalaman pengguna di konsol game atau remote TV, sedangkan string 4 digit numerik dapat ditebak dalam hitungan detik.
Karakter Set Base20 RFC 8628
RFC 8628 merekomendasikan set karakter Base20: BCDFGHJKLMNPQRSTVWXZ. Karakter ini sengaja menghilangkan vokal untuk menghindari pembentukan kata-kata tidak senonoh secara acak, serta membuang karakter ambigu yang rawan salah baca (seperti angka 0 dengan huruf O, atau angka 1 dengan huruf I dan L).
Perhitungan ruang kunci (keyspace) untuk format 8 karakter Base20 (contoh: WDJB-MJHT):
Keyspace = 20^8 = 25.600.000.000 kombinasi (~34,5 bit entropi)Dengan $2,56 \times 10^{10}$ kemungkinan, penyerang tidak dapat menebak kode secara acak jika backend menerapkan parameter pembatas berikut:
- TTL Ketat: Batasi masa aktif
user_codemaksimal 300 hingga 600 detik (5–10 menit). - Rate Limiting Verifikasi Web: Terapkan pembatasan percobaan input pada endpoint web otorisasi (maksimal 5 kali gagal per alamat IP atau sesi browser per menit).
- Burn-on-Failure: Jika satu
user_codemenerima lebih dari 3 kali percobaan verifikasi yang salah secara berurutan, batalkan authorization request tersebut dari penyimpanan cache secara permanen.
2. Mitigasi Polling DoS dengan Redis dan Status slow_down
Klien perangkat harus melakukan polling ke endpoint /token untuk mengetahui status persetujuan user. RFC 8628 menetapkan parameter interval (satuan detik), yaitu jeda minimum antar permintaan polling yang diizinkan server (misalnya 5 detik).
Masalah muncul ketika ribuan perangkat klien melakukan polling agresif tanpa jeda, baik akibat bug klien, koneksi tidak stabil, maupun serangan terencana. Jika backend langsung mengembalikan respons database, koneksi pool akan habis seketika.
Mekanisme Penalti slow_down
Server wajib mencatat stempel waktu (timestamp) polling terakhir untuk setiap device_code. Ketika klien menembak endpoint sebelum jeda interval terpenuhi, server harus:
- Menaikkan nilai interval klien sebagai penalti (misalnya, $interval_{baru} = interval_{lama} + 5$).
- Mengembalikan HTTP status
400 Bad Requestdengan payload JSON{"error": "slow_down", "interval": interval_baru}.
Implementasi terbaik menggunakan Redis dengan atomic command atau pipeline untuk memverifikasi waktu tanpa menyentuh database relasional.
3. Mengatasi Device Session Fixation & Hijacking
Serangan session fixation pada alur device terjadi ketika penyerang menginisiasi flow, mendapatkan user_code, lalu memancing korban memasukkan kode tersebut (via phishing atau social engineering). Setelah korban login, penyerang yang memegang device_code memperoleh token akses korban.
Langkah Preventif Backend
- Entropy Asimetris:
device_codeharus memiliki entropi kriptografi tinggi (minimal 256-bit, contohnya string acaksecrets.token_urlsafe(32)), disimpan secara rahasia oleh perangkat klien, dan tidak pernah ditampilkan ke UI. - Device Fingerprint Binding: Ikat
device_codedengan atribut jaringan atau identitas klien (seperti Client ID dan subnet IP perangkat awal). Tolak polling jika request datang dari Client ID berbeda. - Atomic State Invalidation: Begitu token akses diterbitkan melalui endpoint
/token, musnahkandevice_codedan relasiuser_codeseketika dalam transaksi atomik untuk mencegah token replay.
4. Implementasi Handler /device/code dan /token
Berikut implementasi backend Python mandiri menggunakan struktur data Redis untuk menangani alur otorisasi, rate limiting polling, penalti slow_down, dan proteksi brute-force.
import time
import secrets
import json
BASE20_CHARSET = "BCDFGHJKLMNPQRSTVWXZ"
DEFAULT_INTERVAL = 5
PENALTY_STEP = 5
CODE_TTL = 300 # 5 menit
def generate_user_code(length=8) -> str:
raw = "".join(secrets.choice(BASE20_CHARSET) for _ in range(length))
return f"{raw[:4]}-{raw[4:]}"
class DeviceAuthStore:
def __init__(self, redis_client):
self.redis = redis_client
def create_device_flow(self, client_id: str) -> dict:
device_code = secrets.token_urlsafe(32)
user_code = generate_user_code()
session_data = {
"client_id": client_id,
"user_code": user_code,
"status": "authorization_pending",
"interval": DEFAULT_INTERVAL,
"last_poll": 0,
"user_id": None
}
pipe = self.redis.pipeline()
# Key untuk device polling lookup
pipe.set(f"dev_code:{device_code}", json.dumps(session_data), ex=CODE_TTL)
# Mapping dua arah untuk verifikasi via user_code di web
pipe.set(f"usr_code:{user_code}", device_code, ex=CODE_TTL)
pipe.execute()
return {
"device_code": device_code,
"user_code": user_code,
"verification_uri": "https://auth.example.com/activate",
"expires_in": CODE_TTL,
"interval": DEFAULT_INTERVAL
}
def poll_token(self, device_code: str, client_id: str) -> tuple[int, dict]:
key = f"dev_code:{device_code}"
raw_session = self.redis.get(key)
if not raw_session:
return 400, {"error": "expired_token", "error_description": "Kode kedaluwarsa atau tidak valid."}
session = json.loads(raw_session)
now = time.time()
if session["client_id"] != client_id:
return 401, {"error": "invalid_client"}
# Validasi jeda polling
last_poll = session.get("last_poll", 0)
current_interval = session.get("interval", DEFAULT_INTERVAL)
elapsed = now - last_poll
if last_poll > 0 and elapsed < current_interval:
# Penalti: tambah interval dan balas slow_down
session["interval"] = current_interval + PENALTY_STEP
session["last_poll"] = now
ttl = self.redis.ttl(key)
self.redis.set(key, json.dumps(session), ex=max(ttl, 1))
return 400, {
"error": "slow_down",
"interval": session["interval"],
"error_description": f"Polling terlalu cepat. Tingkatkan interval ke {session['interval']}s."
}
session["last_poll"] = now
if session["status"] == "authorization_pending":
ttl = self.redis.ttl(key)
self.redis.set(key, json.dumps(session), ex=max(ttl, 1))
return 400, {
"error": "authorization_pending",
"error_description": "Otorisasi belum selesai di browser."
}
if session["status"] == "authorized":
# Burn kode segera untuk mencegah replay
user_code = session["user_code"]
self.redis.delete(key, f"usr_code:{user_code}")
return 200, {
"access_token": secrets.token_hex(32),
"token_type": "Bearer",
"expires_in": 3600,
"user_id": session["user_id"]
}
return 400, {"error": "invalid_grant"}
5. Runnable Assert Test: Verifikasi Polling Throttling
Skrip berikut membuktikan kepatuhan terhadap RFC 8628 tanpa ketergantungan library pihak ketiga. Mock Redis internal digunakan untuk memverifikasi transisi state authorization_pending, slow_down akibat polling agresif, serta kegagalan token kedaluwarsa.
class InMemoryRedisMock:
def __init__(self):
self.store = {}
self.ttls = {}
def pipeline(self):
return self
def set(self, key, value, ex=300):
self.store[key] = value
self.ttls[key] = ex
return self
def get(self, key):
return self.store.get(key)
def delete(self, *keys):
for k in keys:
self.store.pop(k, None)
self.ttls.pop(k, None)
def ttl(self, key):
return self.ttls.get(key, 0)
def execute(self):
return True
def run_tests():
mock_redis = InMemoryRedisMock()
service = DeviceAuthStore(mock_redis)
client_id = "console_app_v1"
# 1. Inisiasi alur
auth_data = service.create_device_flow(client_id)
dev_code = auth_data["device_code"]
user_code = auth_data["user_code"]
assert len(user_code) == 9, "Format user_code harus XXXX-XXXX (9 karakter)"
assert user_code[4] == "-", "Format delimiter user_code harus tanda hubung"
# 2. Polling pertama: ekspektasi authorization_pending
status, res = service.poll_token(dev_code, client_id)
assert status == 400
assert res["error"] == "authorization_pending"
# 3. Polling langsung tanpa jeda: ekspektasi slow_down dan penalti interval
status, res = service.poll_token(dev_code, client_id)
assert status == 400
assert res["error"] == "slow_down"
assert res["interval"] == DEFAULT_INTERVAL + PENALTY_STEP, "Interval harus meningkat sebesar penalti"
# 4. Simulasi persetujuan pengguna di web
session = json.loads(mock_redis.get(f"dev_code:{dev_code}"))
session["status"] = "authorized"
session["user_id"] = "usr_10928"
session["last_poll"] = 0 # Reset agar lolos jeda polling
mock_redis.set(f"dev_code:{dev_code}", json.dumps(session), ex=300)
# 5. Polling setelah disetujui: ekspektasi 200 OK dan pembakaran session
status, res = service.poll_token(dev_code, client_id)
assert status == 200
assert "access_token" in res
assert res["user_id"] == "usr_10928"
# 6. Pastikan session telah dibakar (Burn-on-Use)
assert mock_redis.get(f"dev_code:{dev_code}") is None
assert mock_redis.get(f"usr_code:{user_code}") is None
# 7. Polling ulang dengan kode yang sama harus ditolak
status, res = service.poll_token(dev_code, client_id)
assert status == 400
assert res["error"] == "expired_token"
print("Semua assert lolos: Validasi RFC 8628, slow_down throttling, dan state cleanup terverifikasi.")
if __name__ == "__main__":
run_tests()
Ringkasan Trade-Off Arsitektur
Catatan Implementasi: Menggunakan Base20 dengan 8 karakter memberikan proteksi brute-force optimal hanya jika dikombinasikan dengan TTL ≤ 10 menit. Jika sistem Anda membutuhkan masa hidup kode lebih lama (> 15 menit), jangan menambah masa aktif pada satu kode; instruksikan klien untuk meminta pasangan
device_code/user_codebaru ketika sesi lama habis.
Penerapan validasi ketat, pengelolaan status atomik di Redis, dan pengembalian kode error standar RFC 8628 (authorization_pending, slow_down, expired_token) mengeliminasi risiko Denial of Service pada backend sekaligus memitigasi pembajakan sesi otentikasi lintas perangkat.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!