Password bocor bukan lagi soal "kalau", tapi "kapan". Cek punyabreach.com dan kemungkinan besar email kamu sudah ada di daftar. Satu password dipakai di lima situs, satu situs kena bobol, sisanya menyusul dalam hitungan jam. Two-factor authentication (2FA) memutus rantai ini: meski password kamu bocor, penyerang tetap butuh kode 6 digit yang hanya muncul di HP kamu.
Kenapa TOTP, Bukan SMS?
OTP lewat SMS murah dan gampang, tapi bisa dibajak lewat SIM swap — penyerang menelepon operator, mengaku jadi kamu, dan memindahkan nomormu ke SIM baru. TOTP (Time-based One-Time Password) tidak butuh operator sama sekali. Kode dihitung dari secret yang dibagikan saat setup dan waktu saat ini. HP dan server menghitung kode yang sama secara terpisah, jadi tidak ada yang dikirim lewat jaringan. Standar ini yang dipakai Google Authenticator, Authy, 1Password, sampai Bitwarden.
Cara Kerja TOTP dalam Satu Menit
Saat kamu mengaktifkan 2FA, server membuat secret acak 16–32 karakter (base32). Secret ini diubah jadi QR code yang kamu scan dengan aplikasi authenticator. Setelah scan, aplikasi dan server memegang secret yang sama. Setiap 30 detik, keduanya menghitung HMAC-SHA1 dari counter waktu dan secret, lalu memotong hasilnya jadi 6 digit. Karena kode bergantung pada waktu, kode lama otomatis mati.
Kode TOTP tidak "dikirim" ke HP kamu. HP menghitungnya sendiri dari secret + waktu. Itu sebabnya 2FA tetap jalan meski HP tidak ada sinyal.
Siapkan Proyek
Buat folder, aktifkan virtual environment, lalu install tiga package: pyotp untuk logika TOTP, qrcode untuk QR, dan FastAPI untuk endpoint-nya.
mkdir totp-demo && cd totp-demo
python -m venv .venv
source .venv/bin/activate
pip install fastapi uvicorn pyotp "qrcode[pil]"
Generate Secret dan QR Code
Langkah pertama: buat secret dan bungkus jadi QR code siap-scan. random_base32() menghasilkan secret, provisioning_uri() menyusun URI standar otpauth://.
import pyotp
import qrcode
secret = pyotp.random_base32()
print("Secret:", secret)
totp = pyotp.TOTP(secret)
uri = totp.provisioning_uri(
name="kamu@bengkelkode.com",
issuer_name="BengkelKode",
)
img = qrcode.make(uri)
img.save("qrcode.png")
print("URI:", uri)
Jalankan skripnya, lalu scan qrcode.png pakai Google Authenticator atau Authy. Aplikasi langsung menampilkan kode 6 digit yang berganti tiap 30 detik. Catat nilai secret — kamu butuh ini di sisi server untuk verifikasi.
Verifikasi Kode dari Aplikasi
Sekarang cocokkan kode yang diketik pengguna dengan hasil hitung server. Method verify() menangani perbandingan ini, termasuk normalisasi spasi yang kadang menyelip.
import pyotp
secret = "JBSWY3DPEHPK3PXP" # ganti dengan secret milikmu
totp = pyotp.TOTP(secret)
kode = input("Masukkan kode 6 digit: ")
if totp.verify(kode):
print("Kode valid")
else:
print("Kode salah atau kedaluwarsa")
Kode TOTP berlaku 30 detik. Kalau jam server dan HP tidak sinkron, verify() bisa menolak kode yang sebenarnya benar. Tambah toleransi dengan totp.verify(kode, valid_window=1) — ini menerima kode dari 30 detik sebelum dan sesudah.
Integrasi ke FastAPI
Sekarang rakit flow lengkap: satu endpoint untuk mengaktifkan 2FA, satu endpoint untuk memverifikasi kode saat login. Saya pakai dictionary sebagai pengganti database supaya fokusnya ke logika 2FA.
import pyotp
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
# Ganti dengan database asli di produksi
users: dict[str, str] = {}
class Enable2FA(BaseModel):
username: str
class Verify2FA(BaseModel):
username: str
code: str
@app.post("/2fa/enable")
def enable_2fa(req: Enable2FA):
secret = pyotp.random_base32()
users[req.username] = secret
totp = pyotp.TOTP(secret)
uri = totp.provisioning_uri(
name=req.username, issuer_name="BengkelKode"
)
return {"secret": secret, "uri": uri}
@app.post("/2fa/verify")
def verify_2fa(req: Verify2FA):
secret = users.get(req.username)
if secret is None:
raise HTTPException(404, "User belum mengaktifkan 2FA")
totp = pyotp.TOTP(secret)
if not totp.verify(req.code):
raise HTTPException(401, "Kode salah atau kedaluwarsa")
return {"status": "ok", "message": "Login berhasil"}
uvicorn main:app --reload
Coba aktifkan 2FA dari terminal:
curl -X POST http://127.0.0.1:8000/2fa/enable -H "Content-Type: application/json" -d '{"username": "kamu"}'
Responsnya memuat secret dan uri. Scan URI itu (atau ubah dulu ke QR) ke authenticator, lalu kirim kode yang tampil:
curl -X POST http://127.0.0.1:8000/2fa/verify -H "Content-Type: application/json" -d '{"username": "kamu", "code": "123456"}'
Kode benar → server balas 200. Tunggu 30 detik, kirim ulang kode yang sama, dan kamu dapat 401 karena kode sudah kedaluwarsa. Itu bukti TOTP benar-benar bekerja.
Jangan Simpan Secret sebagai Teks Biasa
Secret adalah kunci kedua setelah password. Kalau database bocor dan secret tersimpan plaintext, penyerang bisa menghitung kode 6 digit tanpa menyentuh HP korban. Enkripsi secret sebelum disimpan, dan jangan pernah print nilainya ke console di produksi — contoh di atas sengaja print secret supaya alurnya jelas.
Jangan simpan secret TOTP di tabel yang sama dengan hash password. Kalau keduanya ada di satu tempat dan tempat itu bocor, 2FA jadi tidak berguna.
Langkah Berikutnya
2FA ini baru separuh sistem autentikasi. Padukan dengan JWT supaya login menghasilkan token yang dipakai endpoint lain, dan pastikan token tidak bisa diterbitkan sebelum verifikasi 2FA lolos. Untuk bagian JWT, saya sudah bahas tuntas di Autentikasi JWT di FastAPI. Kalau kamu baru mulai dengan FastAPI, baca dulu Cara Membuat REST API dengan FastAPI supaya paham struktur project-nya. Sebelum rilis, cek juga Mengamankan Aplikasi Web Python untuk menutup celah SQL injection, XSS, dan CSRF yang sering menempel di form login.