API TOTP 2FA: Cara Pasang Google Authenticator di Aplikasi Anda (Node.js dan Python)

Oleh , Tim Engineering APIPedia · Terbit · Diperbarui · 9 menit baca

Pasang 2FA Google Authenticator lewat API TOTP APIPedia: buat secret dan QR, verifikasi kode 6 digit, cegah replay, dan terbitkan kode cadangan.

Sampul APIPedia: Pasang 2FA Google Authenticator lewat tiga endpoint, dengan alur tiga kartu: POST /otp/enroll menghasilkan secret dan QR, aplikasi autentikator menampilkan kode 6 digit tiap 30 detik, lalu POST /otp/verify membalas valid true

API TOTP APIPedia memasang 2FA yang kompatibel dengan Google Authenticator ke aplikasi Anda lewat tiga endpoint: /otp/enroll membuat secret dan QR, /otp/verify mencocokkan kode 6 digit, dan /otp/backup-codes menerbitkan kode cadangan. Anda tidak perlu menulis sendiri algoritma RFC 6238, pembuat QR, atau penanganan selisih jam.

Tutorial ini membahas alur lengkapnya dari tombol "Aktifkan 2FA" sampai login, dengan contoh curl, Node.js, dan Python. Semua contoh respons di bawah diambil dari panggilan nyata ke API pada 14 November 2026 memakai akun contoh.

Ringkasan cepat

ItemDetail
ProdukTOTP/HOTP Authenticator (kategori Keamanan)
Base URLhttps://api.apipedia.id/v1/otp
EndpointPOST /enroll, POST /verify, POST /backup-codes
StandarTOTP (RFC 6238) dan HOTP (RFC 4226)
Nilai bawaanSHA1, 6 digit, 30 detik per kode, window 1
Pilihan lainSHA256/SHA512, 8 digit, period 15 sampai 120 detik, window 0 sampai 10
Penyimpanan secretDi server Anda. API tidak menyimpannya (stored: false)
AutentikasiHeader Authorization: Bearer apk_xxx

Masalah apa yang diselesaikan 2FA berbasis TOTP?

2FA berbasis TOTP menutup risiko akun diambil alih ketika kata sandi bocor, karena penyerang masih butuh kode 6 digit yang hanya muncul di ponsel pemilik akun. Kode itu dihitung dari secret bersama dan jam saat ini, lalu berganti setiap 30 detik.

Dibanding kode lewat SMS, TOTP punya tiga kelebihan praktis:

  • Tanpa biaya per kode. Kode dihitung di ponsel pengguna, tidak ada pesan yang dikirim.
  • Tetap jalan tanpa sinyal. Aplikasi autentikator bekerja offline.
  • Tidak bergantung pada nomor telepon. Ganti nomor atau SIM tidak memutus akses.

Bagian yang sering salah saat ditulis sendiri adalah detailnya: encoding base32, format URI otpauth://, toleransi jam, dan pencegahan kode dipakai dua kali. Tiga endpoint ini mengurus bagian tersebut.

Bagaimana alur 2FA dengan API TOTP?

Alurnya terdiri dari dua tahap: aktivasi sekali per akun, lalu verifikasi di setiap login. Server Anda selalu berada di tengah, sehingga kunci API tidak pernah sampai ke browser.

Diagram alur 2FA tiga kolom: Pengguna, Server Anda, dan APIPedia. Tahap aktivasi: pengguna klik Aktifkan 2FA, server memanggil POST /otp/enroll dengan account dan issuer, API membalas secret, otpauthUrl, dan qr dengan stored false, server menyimpan secret lalu menampilkan QR, pengguna scan dan mengetik kode 6 digit. Tahap login: server memanggil POST /otp/verify dengan secret dan code, API membalas valid true beserta delta dan matchedStep, server menolak step yang sudah dipakai lalu membuat sesi login.

Satu hal penting sebelum menulis kode: API ini stateless. Secret tidak disimpan di sisi APIPedia, jadi Anda yang menyimpannya (terenkripsi) dan mengirimnya ulang setiap kali verifikasi.

Langkah 1: Bagaimana membuat secret dan QR?

Secret dan QR dibuat dengan satu panggilan POST /otp/enroll yang hanya mewajibkan field account. Isi issuer dengan nama aplikasi Anda, karena nama itulah yang tampil di aplikasi autentikator.

curl -s -X POST https://api.apipedia.id/v1/otp/enroll \
  -H "Authorization: Bearer apk_xxx" \
  -H "Content-Type: application/json" \
  -d '{"account": "[email protected]", "issuer": "TokoKita", "qrFormat": "svg"}'

Contoh respons (isi qr dipotong):

{
  "is_success": true,
  "message": "Secret OTP berhasil dibuat. APIPedia TIDAK menyimpan secret ini - simpan di sisi Anda (terenkripsi) karena wajib dikirim ulang saat verifikasi.",
  "data": {
    "secret": "7XWQF5HU4OGXF2F36UTB56Y6NS7UH5QJ",
    "otpauthUrl": "otpauth://totp/TokoKita:budi%40contoh.id?secret=7XWQF5HU4OGXF2F36UTB56Y6NS7UH5QJ&issuer=TokoKita&algorithm=SHA1&digits=6&period=30",
    "qr": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0i...",
    "qrFormat": "svg",
    "type": "totp",
    "issuer": "TokoKita",
    "account": "[email protected]",
    "algorithm": "SHA1",
    "digits": 6,
    "period": 30,
    "secretSource": "generated",
    "stored": false
  }
}

Yang perlu Anda lakukan dengan respons ini:

  • Simpan secret di database dalam keadaan terenkripsi, dengan status "menunggu konfirmasi".
  • Tampilkan qr langsung sebagai <img src="...">. Nilainya sudah berupa data URI, dalam format PNG (bawaan) atau SVG.
  • Sediakan juga secret sebagai teks untuk pengguna yang tidak bisa memindai QR.

Parameter opsional lain: digits (6 atau 8), period (15 sampai 120 detik), algorithm (SHA1, SHA256, SHA512), type (totp atau hotp), dan secret bila Anda ingin memakai secret base32 sendiri minimal 128 bit. Secret pada contoh di atas hanya untuk demonstrasi, jangan dipakai ulang.

Langkah 2: Bagaimana memverifikasi kode pengguna?

Kode diverifikasi dengan POST /otp/verify yang menerima secret dan code, lalu membalas valid: true atau valid: false. Kirim code sebagai teks, bukan angka, supaya nol di depan tidak hilang.

curl -s -X POST https://api.apipedia.id/v1/otp/verify \
  -H "Authorization: Bearer apk_xxx" \
  -H "Content-Type: application/json" \
  -d '{"secret": "7XWQF5HU4OGXF2F36UTB56Y6NS7UH5QJ", "code": "625307"}'

Respons saat kode benar:

{
  "is_success": true,
  "message": "Kode OTP valid.",
  "data": {
    "valid": true,
    "delta": 0,
    "timeStep": 59821764,
    "matchedStep": 59821764,
    "type": "totp",
    "algorithm": "SHA1",
    "digits": 6,
    "period": 30,
    "window": 1
  }
}

Respons saat kode salah atau sudah kedaluwarsa:

{
  "is_success": true,
  "message": "Kode OTP tidak valid atau sudah di luar jendela toleransi.",
  "data": { "valid": false, "delta": null, "timeStep": 59821764, "matchedStep": null, "window": 1 }
}

Perhatikan bahwa kode salah tetap dijawab is_success: true dengan status HTTP 200. Yang menentukan lolos atau tidak adalah data.valid, bukan is_success.

Setelah kode pertama lolos, ubah status 2FA akun menjadi aktif. Jangan mengaktifkan 2FA sebelum pengguna berhasil memasukkan satu kode, karena QR yang gagal dipindai akan mengunci akun.

Langkah 3: Bagaimana memasangnya di backend?

Di backend Anda cukup membuat satu fungsi pembungkus untuk memanggil API, lalu dua fungsi bisnis: mulai aktivasi dan cek kode. Fungsi db, enkripsi, dan dekripsi pada contoh adalah milik aplikasi Anda sendiri.

Node.js

// otp.js (Node.js 18+, fetch bawaan)
const BASE = "https://api.apipedia.id/v1/otp";
const KEY = process.env.APIPEDIA_API_KEY; // apk_xxx

async function otp(path, body) {
  const res = await fetch(BASE + path, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (!json.is_success) throw new Error(json.message);
  return json.data;
}

export async function mulaiAktivasi(user) {
  const data = await otp("/enroll", { account: user.email, issuer: "TokoKita" });
  await db.simpan2fa(user.id, { secret: enkripsi(data.secret), aktif: false, lastStep: 0 });
  return { qr: data.qr, secretManual: data.secret }; // tampilkan sekali di halaman aktivasi
}

export async function cekKode(user, code) {
  if (!/^\d{6}$/.test(code)) return false;
  const row = await db.ambil2fa(user.id);
  const data = await otp("/verify", { secret: dekripsi(row.secret), code });

  // tolak kode salah, dan kode benar yang sudah pernah dipakai (replay)
  if (!data.valid || data.matchedStep <= row.lastStep) return false;

  await db.simpan2fa(user.id, { aktif: true, lastStep: data.matchedStep });
  return true;
}

Python

# otp.py
import os
import requests

BASE = "https://api.apipedia.id/v1/otp"
HEADERS = {"Authorization": f"Bearer {os.environ['APIPEDIA_API_KEY']}"}  # apk_xxx


def otp(path: str, body: dict) -> dict:
    res = requests.post(BASE + path, json=body, headers=HEADERS, timeout=10)
    payload = res.json()
    if not payload["is_success"]:
        raise RuntimeError(payload["message"])
    return payload["data"]


def mulai_aktivasi(email: str) -> dict:
    data = otp("/enroll", {"account": email, "issuer": "TokoKita"})
    return {"secret": data["secret"], "qr": data["qr"]}  # enkripsi secret sebelum disimpan


def cek_kode(secret: str, code: str, last_step: int) -> tuple[bool, int]:
    hasil = otp("/verify", {"secret": secret, "code": code})
    if not hasil["valid"] or hasil["matchedStep"] <= last_step:
        return False, last_step
    return True, hasil["matchedStep"]  # simpan nilai ini sebagai last_step baru

Di halaman login, panggil cekKode hanya setelah kata sandi benar. Batasi juga jumlah percobaan, misalnya 5 kali per 15 menit per akun, karena kode 6 digit hanya punya satu juta kemungkinan.

Langkah 4: Bagaimana membuat kode cadangan?

Kode cadangan dibuat dengan POST /otp/backup-codes, yang mengembalikan daftar kode beserta hash SHA-256 masing-masing. Kode ini dipakai pengguna saat ponselnya hilang atau rusak.

curl -s -X POST https://api.apipedia.id/v1/otp/backup-codes \
  -H "Authorization: Bearer apk_xxx" \
  -H "Content-Type: application/json" \
  -d '{"count": 3}'
{
  "is_success": true,
  "message": "3 kode cadangan berhasil dibuat. Tampilkan kode ke pengguna SEKALI saja dan simpan hanya hash-nya; APIPedia tidak menyimpan keduanya.",
  "data": {
    "codes": ["k9wdv-c72k7", "yybeq-phv27", "82wkx-dy6sx"],
    "hashes": [
      "7209ce944539fe549366fd4005015214302a9a0ed84ed0944a1d943c7c48c8cb",
      "7a3e1f53802cc3472cfb0cdc7eb1ee9b194c37811e72665d0a9dc26efa2eca12",
      "5864f048342f375f5216fdbb0472ad2c5ab7ffb3b0caa7402a779f7102a74229"
    ],
    "hashAlgorithm": "sha256",
    "normalization": "huruf kecil, tanpa spasi dan tanda hubung",
    "count": 3,
    "length": 10,
    "format": "grouped",
    "entropyBits": 49,
    "stored": false
  }
}

Tampilkan codes satu kali, lalu simpan hanya hashes. Saat pengguna memasukkan kode cadangan, normalisasi dulu, hitung hash-nya, cocokkan, lalu hapus hash yang terpakai:

import hashlib

def cek_kode_cadangan(masukan: str, daftar_hash: list[str]) -> bool:
    norm = masukan.lower().replace("-", "").replace(" ", "")
    h = hashlib.sha256(norm.encode()).hexdigest()
    if h in daftar_hash:
        daftar_hash.remove(h)  # sekali pakai
        return True
    return False

Kami menguji fungsi ini dengan kode pertama pada contoh: k9wdv-c72k7 menghasilkan hash 7209ce94... yang sama dengan respons API. Jumlah kode bisa diatur 1 sampai 20 (bawaan 10) dan panjangnya 8 sampai 16 karakter.

Berapa nilai window yang tepat?

Nilai window: 1, yaitu nilai bawaan, sudah tepat untuk sebagian besar aplikasi. Dengan nilai itu API menerima kode dari satu langkah sebelum dan satu langkah sesudah waktu server, sehingga selisih jam ponsel sekitar 30 detik masih ditoleransi.

Diagram jendela toleransi TOTP dengan period 30 dan window 1: lima kotak langkah waktu, langkah minus 2 dan plus 2 ditolak, langkah minus 1, 0, dan plus 1 diterima. Di bawahnya empat kartu: window 0 paling ketat, window 1 nilai bawaan, window 2 sampai 10 makin longgar dengan batas API 10, dan matchedStep yang perlu disimpan per akun untuk menolak step yang sama atau lebih kecil.

  • delta menunjukkan di langkah mana kode cocok: 0 berarti tepat waktu, -1 berarti kode dari 30 detik sebelumnya.
  • matchedStep adalah nomor langkah yang cocok. Simpan nilainya per akun untuk menolak kode yang sama dipakai dua kali.
  • window besar memperpanjang masa berlaku kode curian. Naikkan hanya bila banyak pengguna gagal karena jam perangkat.

Kesalahan umum apa yang perlu dihindari?

Kesalahan paling umum adalah memanggil API ini langsung dari browser atau aplikasi mobile, sehingga kunci API dan secret pengguna ikut terbuka. Berikut daftar lengkapnya:

  • Memanggil dari frontend. Semua panggilan harus lewat server Anda.
  • Tidak menyimpan secret. Respons stored: false berarti secret hilang selamanya bila Anda tidak menyimpannya.
  • Menyimpan secret tanpa enkripsi. Secret setara kata sandi kedua. Enkripsi di database dan jangan tulis ke log.
  • Mengecek is_success saja. Kode salah tetap is_success: true. Selalu cek data.valid.
  • Mengirim kode sebagai angka. 012345 menjadi 12345 dan ditolak. Kirim sebagai string.
  • Tidak mencegah replay. Tanpa pengecekan matchedStep, kode yang diintip bisa dipakai lagi dalam jendela yang sama.
  • Tanpa batas percobaan. Pasang rate limit per akun di endpoint login Anda.
  • Tanpa jalur pemulihan. Tanpa kode cadangan, ponsel hilang berarti tiket bantuan dan verifikasi manual.
  • Memakai titik dua di account. Karakter itu pemisah di URI otpauth://, jadi tidak diterima.

Pertanyaan yang sering diajukan

Apakah API ini hanya untuk Google Authenticator?

Tidak. Secret dan URI otpauth:// yang dihasilkan mengikuti RFC 6238, sehingga bisa dipindai aplikasi autentikator mana pun yang mendukung TOTP. Pakai nilai bawaan SHA1, 6 digit, dan 30 detik untuk kompatibilitas paling luas.

Apakah APIPedia menyimpan secret pengguna saya?

Tidak. Respons /otp/enroll dan /otp/backup-codes memuat stored: false, dan pesannya menyatakan secret wajib disimpan di sisi Anda. Karena itu secret harus dikirim ulang di setiap panggilan /otp/verify.

Apa bedanya TOTP dan HOTP?

TOTP menghitung kode dari waktu, sedangkan HOTP menghitungnya dari counter yang naik setiap kode dipakai. Untuk HOTP, kirim type: "hotp" beserta counter tersimpan, lalu simpan nextCounter dari respons. Untuk login aplikasi, TOTP lebih umum.

Kenapa kode pengguna selalu ditolak?

Penyebab tersering adalah jam ponsel yang tidak sinkron. Minta pengguna mengaktifkan waktu otomatis di ponselnya. Bila delta pada pengguna lain sering bernilai -1 atau 1, itu tanda yang sama dalam skala lebih kecil.

Apakah kode 2FA ini dikirim lewat SMS atau email?

Tidak. Tidak ada pesan yang dikirim. Kode dihitung di ponsel pengguna dari secret yang dipindai saat aktivasi, dan API hanya mencocokkannya.

Bacaan terkait di APIPedia: cara API key dicuri dan cara menjaganya, arti rate limit dan error 429, API OCR KTP untuk onboarding, dan menghubungkan APIPedia ke Claude lewat MCP.

Coba sekarang: buat kunci API di console.apipedia.id, panggil /otp/enroll dengan email Anda sendiri, pindai QR-nya, lalu kirim kode yang muncul ke /otp/verify. Dari nol sampai valid: true biasanya selesai dalam beberapa menit.

Sumber

Topik:

Tentang penulis

Tim APIPedia — Tim Engineering APIPedia

Tim engineering di balik APIPedia yang membangun dan memelihara API data Indonesia - ongkir, OCR KTP, data wilayah, BMKG, hingga server MCP - dan menulis panduan integrasinya dari pengalaman langsung.