Amankan situs Anda dengan autentikasi 2 langkah dengan kunci keamanan (WebAuthn)

1. Yang akan Anda buat

Anda akan memulai dengan aplikasi web dasar yang mendukung login berbasis sandi.

Kemudian, Anda akan menambahkan dukungan untuk autentikasi 2 langkah melalui kunci keamanan, berdasarkan WebAuthn. Untuk melakukannya, Anda harus mengimplementasikan hal berikut:

  • Cara bagi pengguna untuk mendaftarkan kredensial WebAuthn.
  • Alur autentikasi 2 langkah saat pengguna diminta memasukkan faktor kedua—kredensial WebAuthn—jika telah mendaftarkannya.
  • Antarmuka pengelolaan kredensial: daftar kredensial yang memungkinkan pengguna mengganti nama dan menghapus kredensial.

16ce77744061c5f7.png

Lihat aplikasi web yang telah selesai dan cobalah.

2. Tentang WebAuthn

Dasar-dasar WebAuthn

Mengapa WebAuthn?

Phishing adalah masalah keamanan yang sangat serius di web: sebagian besar pelanggaran akun memanfaatkan sandi yang lemah atau dicuri yang digunakan kembali di berbagai situs. Respons kolektif industri terhadap masalah ini merupakan autentikasi multi-faktor, namun implementasinya terpecah-pecah dan banyak yang masih belum dapat menangani phishing secara memadai.

Web Authentication API, atau WebAuthn, adalah protokol tahan phishing standar yang dapat digunakan oleh aplikasi web apa pun.

Cara kerja

Sumber: webauthn.guide

WebAuthn memungkinkan server mendaftarkan dan mengautentikasi pengguna menggunakan kriptografi kunci publik, bukan sandi. Situs dapat membuat kredensial yang terdiri dari pasangan kunci pribadi-publik.

  • Kunci pribadi disimpan dengan aman di perangkat pengguna.
  • Kunci publik dan ID kredensial yang dibuat secara acak akan dikirim ke server untuk disimpan.

Kunci publik digunakan oleh server untuk membuktikan identitas pengguna. Hal ini tidak bersifat rahasia, karena tidak berguna tanpa kunci pribadi yang sesuai.

Manfaat

WebAuthn memiliki dua manfaat utama:

  • Tidak ada rahasia bersama: server tidak menyimpan rahasia. Hal ini membuat database kurang menarik bagi peretas, karena kunci publik tidak berguna bagi mereka.
  • Kredensial cakupan: kredensial yang terdaftar untuk site.example tidak dapat digunakan di evil-site.example. Tindakan ini membuat WebAuthn kebal phishing.

Kasus penggunaan

Salah satu kasus penggunaan untuk WebAuthn adalah autentikasi 2 langkah dengan kunci keamanan. Hal ini mungkin sangat relevan untuk aplikasi web perusahaan.

Dukungan browser

WebAuthn ditulis oleh W3C dan FIDO, dengan partisipasi Google, Mozilla, Microsoft, Yubico, dan lainnya.

Glosarium

  • Pengautentikasi: entitas software atau hardware yang dapat mendaftarkan pengguna dan kemudian menegaskan kepemilikan kredensial yang terdaftar. Ada dua jenis pengautentikasi:
  • Pengautentikasi roaming: pengautentikasi yang dapat digunakan dengan setiap perangkat yang digunakan untuk login oleh pengguna. Contoh: kunci keamanan USB, smartphone.
  • Pengautentikasi platform: pengautentikasi yang terpasang di perangkat pengguna. Contoh: Touch ID Apple.
  • Kredensial: pasangan kunci pribadi-publik
  • Pihak pengandal: (server untuk) situs yang mencoba mengautentikasi pengguna
  • Server FIDO: server yang digunakan untuk autentikasi. FIDO adalah sekumpulan protokol yang dikembangkan oleh aliansi FIDO; salah satu protokol ini adalah WebAuthn.

Dalam workshop ini, kita akan menggunakan pengautentikasi roaming.

3. Sebelum memulai

Yang Anda perlukan

Untuk menyelesaikan codelab ini, Anda memerlukan:

  • Pemahaman dasar tentang WebAuthn.
  • Pengetahuan dasar tentang JavaScript dan HTML.
  • Browser terbaru yang mendukung WebAuthn.
  • Kunci keamanan yang sesuai dengan U2F.

Anda dapat menggunakan salah satu dari berikut ini sebagai kunci keamanan:

  • Ponsel Android dengan Android>=7 (Nougat) yang menjalankan Chrome. Dalam kasus ini, Anda juga memerlukan komputer Windows, macOS, atau ChromeOS dengan Bluetooth yang berfungsi.
  • Kunci USB, seperti YubiKey.

6539dc7ffec2538c.png

Sumber: https://www.yubico.com/products/security-key/

dd56e2cfe0f7ced2.png

Yang akan Anda pelajari

Anda akan mempelajari ✅

  • Cara mendaftar dan menggunakan kunci keamanan sebagai faktor kedua untuk autentikasi WebAuthn.
  • Cara membuat proses ini mudah digunakan.

Anda tidak akan mempelajari ❌

  • Cara mem-build server FIDO—server yang digunakan untuk autentikasi. Ini tidak masalah karena biasanya, sebagai aplikasi web atau developer situs, Anda akan mengandalkan implementasi server FIDO yang sudah ada. Pastikan untuk selalu memverifikasi fungsi dan kualitas implementasi server yang Anda andalkan. Dalam codelab ini, server FIDO menggunakan SimpleWebAuthn. Untuk opsi lainnya, lihat halaman resmi FIDO Alliance. Untuk library open source, lihat webauthn.io atau AwesomeWebAuthn.

Pernyataan penyangkalan

Pengguna harus memasukkan sandi untuk login. Namun, untuk memudahkan codelab ini, sandi tidak disimpan atau diperiksa. Dalam aplikasi yang sebenarnya, Anda akan memeriksa apakah sisi server tersebut benar atau tidak.

Pemeriksaan keamanan dasar seperti pemeriksaan CSRF, validasi sesi, dan pembersihan input diimplementasikan dalam codelab ini. Namun, ada banyak langkah keamanan yang tidak diperlukan. Misalnya, tidak ada batasan input sandi untuk mencegah serangan brute force. Tidak masalah di sini karena sandi tidak disimpan, namun pastikan Anda tidak menggunakan kode ini apa adanya dalam produksi.

4. Menyiapkan pengautentikasi Anda

Jika Anda menggunakan ponsel Android sebagai pengautentikasi

  • Pastikan Chrome merupakan versi terbaru di desktop dan ponsel Anda.
  • Di desktop dan ponsel Anda, buka Chrome dan login dengan profil yang sama⏤profil yang ingin Anda gunakan untuk workshop ini.
  • Aktifkan Sinkronisasi untuk profil ini, di desktop dan ponsel Anda. Gunakan chrome://settings/syncSetup untuk ini.
  • Aktifkan Bluetooth di desktop dan ponsel Anda.
  • Di desktop Chrome tempat login dengan profil yang sama, buka webauthn.io.
  • Masukkan nama pengguna yang sederhana. Biarkan Jenis pengesahan dan Jenis pengautentikasi ke nilai Tidak ada dan Tidak ditentukan (default). Klik Daftar.

6b49ff0298f5a0af.png

  • Jendela browser akan terbuka, yang meminta Anda untuk memverifikasi identitas. Pilih ponsel Anda dalam daftar.

ffebe58ac826eaf2.png 852de328fcd4eb42.png

  • Di ponsel, Anda akan mendapatkan notifikasi berjudul Verifikasi identitas Anda. Ketuk notifikasi tersebut.
  • Di ponsel, Anda akan dimintai kode PIN ponsel (atau menyentuh sensor sidik jari). Masukkan kode.
  • Di webauthn.io pada desktop, indikator "Berhasil" akan muncul.

fc0acf00a4d412fa.png

  • Di webauthn.io pada desktop, klik tombol Login.
  • Sekali lagi, jendela browser akan terbuka; pilih ponsel Anda dalam daftar.
  • Di ponsel, ketuk notifikasi yang muncul, dan masukkan PIN Anda (atau sentuh sensor sidik jari).
  • webauthn.io akan memberi tahu bahwa Anda telah login. Ponsel Anda berfungsi dengan semestinya sebagai kunci keamanan; Anda siap mengikuti workshop ini.

Jika Anda menggunakan kunci keamanan USB sebagai pengautentikasi

  • Di desktop Chrome, buka webauthn.io.
  • Masukkan nama pengguna yang sederhana. Biarkan Jenis pengesahan dan Jenis pengautentikasi ke nilai Tidak ada dan Tidak ditentukan (default). Klik Daftar.
  • Jendela browser akan terbuka, yang meminta Anda untuk memverifikasi identitas. Pilih Kunci keamanan USB dalam daftar.

ffebe58ac826eaf2.png 9fe75f04e43da035.png

  • Masukkan kunci keamanan Anda ke desktop dan sentuh kunci tersebut.

923d5adb8aa8286c.png

  • Di webauthn.io pada desktop, indikator "Berhasil" akan muncul.

fc0acf00a4d412fa.png

  • Di webauthn.io pada desktop, klik tombol Login.
  • Sekali lagi, jendela browser akan terbuka; pilih Kunci keamanan USB dalam daftar.
  • Sentuh kunci.
  • Webauthn.io akan memberi tahu bahwa Anda telah login. Kunci keamanan USB Anda berfungsi dengan semestinya; Anda siap mengikuti workshop ini.

7e1c0bb19c9f3043.png

5. Memulai persiapan

Dalam codelab ini, Anda akan menggunakan Glitch, editor kode online yang secara otomatis dan instan men-deploy kode Anda.

Membuat fork kode awal

Buka project awal.

Klik tombol Remix.

Tindakan ini akan membuat salinan kode awal. Kini, Anda memiliki kode sendiri untuk diedit. Fork (disebut "remix" di Glitch) adalah tempat Anda akan melakukan semua pekerjaan untuk codelab ini.

cf2b9f552c9809b6.png

Mempelajari kode awal

Pelajari kode awal yang baru saja Anda buat fork-nya.

Perhatikan bahwa di bagian libs, library bernama auth.js sudah disediakan. Ini adalah library kustom yang menangani logika autentikasi sisi server. Library ini menggunakan library fido sebagai dependensi.

6. Mengimplementasikan pendaftaran kredensial

Mengimplementasikan pendaftaran kredensial

Hal pertama yang kita perlukan untuk menyiapkan autentikasi 2 langkah dengan kunci keamanan adalah memungkinkan pengguna membuat kredensial.

Mari kita tambahkan fungsi tersebut terlebih dahulu dalam kode sisi klien.

Di public/auth.client.js, perhatikan bahwa terdapat fungsi bernama registerCredential() yang belum melakukan apa pun. Tambahkan kode berikut ke dalamnya:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    "/auth/credential-options",
    "POST"
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
    }
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Send the encoded credential to the backend for storage
  return await _fetch("/auth/credential", "POST", encodedCredential);
}

Perhatikan bahwa fungsi ini sudah diekspor untuk Anda.

Berikut adalah hal yang dilakukan registerCredential:

  • Fungsi ini mengambil opsi pembuatan kredensial dari server (/auth/credential-options)
  • Karena opsi server kembali dienkode, opsi ini menggunakan fungsi utilitas decodeServerOptions untuk mendekodenya.
  • Fungsi ini membuat kredensial dengan memanggil API web navigator.credential.create. Saat navigator.credential.create dipanggil, browser akan mengambil alih dan meminta pengguna memilih kunci keamanan.
  • Fungsi ini mendekode kredensial yang baru dibuat
  • Fungsi ini mendaftarkan kredensial sisi server baru dengan membuat permintaan ke /auth/credential yang berisi kredensial yang dienkode.

Tambahan lain: lihat kode server

registerCredential() melakukan dua panggilan ke server, jadi mari kita luangkan waktu sejenak untuk melihat apa yang terjadi di backend.

Opsi pembuatan kredensial

Saat klien membuat permintaan ke (/auth/credential-options), server akan membuat objek opsi dan mengirimkannya kembali ke klien.

Objek ini kemudian digunakan oleh klien dalam panggilan pembuatan kredensial yang sebenarnya:

navigator.credentials.create({
    publicKey: {
    // Options generated server-side
    ...credentialCreationOptions
// ...
}

Jadi, apa yang ada dalam credentialCreationOptions ini yang pada akhirnya digunakan dalam registerCredential sisi klien yang telah Anda terapkan pada langkah sebelumnya?

Lihat kode server di bagian router.post("/credential-options", ....

Mari kita tidak melihat setiap properti, tetapi berikut adalah beberapa properti menarik yang dapat Anda lihat di objek opsi kode server, yang dihasilkan menggunakan library fido2 dan pada akhirnya dikembalikan ke klien:

  • rpName dan rpId mendeskripsikan organisasi yang mendaftarkan dan mengautentikasi pengguna. Perlu diketahui bahwa di WebAuthn, kredensial dicakup untuk domain tertentu, yang merupakan manfaat keamanan; rpName dan rpId di sini digunakan untuk menentukan cakupan kredensial. rpId yang valid, misalnya, nama host situs Anda. Perhatikan bagaimana hal ini akan secara otomatis diperbarui saat Anda membuat fork ke project awal 🧘🏻‍♀️
  • excludeCredentials adalah daftar kredensial; kredensial baru tidak dapat dibuat di pengautentikasi yang juga berisi salah satu kredensial yang tercantum di excludeCredentials. Dalam codelab kita, excludeCredentials adalah daftar kredensial yang ada untuk pengguna ini. Dengan ini dan user.id, kita memastikan bahwa setiap kredensial yang dibuat pengguna akan berada di pengautentikasi yang berbeda (kunci keamanan). Ini adalah praktik yang baik karena artinya jika pengguna telah mendaftarkan beberapa kredensial, mereka akan menggunakan pengautentikasi yang berbeda (kunci keamanan), jadi kehilangan satu kunci keamanan tidak akan membuat pengguna terkunci dari akun mereka.
  • authenticatorSelection menentukan jenis pengautentikasi yang ingin Anda izinkan di aplikasi web. Mari kita pelajari lebih lanjut authenticatorSelection:
    • residentKey: preferred berarti aplikasi ini tidak menerapkan kredensial yang dapat ditemukan sisi klien. Kredensial yang dapat ditemukan sisi klien adalah jenis kredensial khusus yang memungkinkan autentikasi pengguna tanpa perlu mengidentifikasinya terlebih dahulu. Di sini, kita telah menyiapkan preferred karena codelab ini berfokus pada implementasi dasar; kredensial yang dapat ditemukan adalah untuk alur yang lebih canggih.
    • requireResidentKey hanya ada untuk kompatibilitas mundur dengan WebAuthn v1.
    • userVerification: preferred berarti bahwa jika pengautentikasi mendukung verifikasi pengguna—misalnya, jika itu adalah kunci keamanan biometrik atau kunci dengan fitur PIN bawaan—pihak pengandal akan memintanya saat membuat kredensial data. Jika pengautentikasi tidak melakukannya—kunci keamanan dasar—, server tidak akan meminta verifikasi pengguna.
  • ​​pubKeyCredParam menjelaskan properti kriptografi kredensial pilihan berdasarkan urutan preferensi.

Semua opsi ini adalah keputusan yang harus dibuat oleh aplikasi web untuk model keamanannya. Amati bahwa pada server, opsi ini ditentukan dalam satu objek authSettings.

Verifikasi login

Bagian lainnya yang lebih menarik di sini adalah req.session.challenge = options.challenge;.

Karena WebAuthn adalah protokol kriptografi, WebAuthn bergantung pada verifikasi login acak untuk menghindari serangan replay—saat penyerang mencuri payload untuk melakukan replay autentikasi, padahal ia bukan pemilik kunci pribadi yang akan mengaktifkan autentikasi.

Untuk mengurangi hal ini, verifikasi login dibuat di server, dan akan ditandatangani dengan cepat; tanda tangan kemudian akan dibandingkan dengan yang diharapkan. Tindakan ini memverifikasi bahwa pengguna menyimpan kunci pribadi pada saat pembuatan kredensial.

Kode pendaftaran kredensial

Lihat kode server di bagian router.post("/credential", ....

Di sinilah kredensial didaftarkan sisi server.

Jadi, apa yang terjadi?

Salah satu bit yang paling penting dalam kode ini adalah panggilan terverifikasi, melalui fido2.verifyAttestationResponse:

  • Tantangan yang ditandatangani akan diperiksa, dan hal ini memastikan bahwa kredensial dibuat oleh seseorang yang benar-benar menyimpan kunci pribadi pada saat pembuatan.
  • ID pihak pengikat, yang terikat dengan asalnya, juga diverifikasi. Hal ini memastikan bahwa kredensial terikat ke aplikasi web ini (dan hanya aplikasi web ini).

Menambahkan fungsi ini ke UI

Setelah fungsi Anda membuat kredensial, ``registerCredential(),sudah siap, mari kita sediakan kredensial tersebut untuk pengguna.

Anda akan melakukannya dari halaman Akun, karena ini adalah lokasi standar untuk pengelolaan autentikasi.

Pada markup account.html, di bawah nama pengguna, ada div yang masih kosong dengan class tata letak class="flex-h-between". Kita akan menggunakan div ini untuk elemen UI yang terkait dengan fungsi 2FA.

Tambahkan ke div ini:

  • Judul yang bertuliskan "Autentikasi 2 langkah"
  • Tombol untuk membuat kredensial
 <div class="flex-h-between">
    <h3>
        Two-factor authentication
    </h3>
    <button class="create" id="registerButton" raised>
        ➕ Add a credential
    </button>
</div>

Di bawah div ini, tambahkan div kredensial yang akan kita perlukan nanti:

<div class="flex-h-between">
(HTML you've just added)
</div>
<div id="credentials"></div>

Di skrip inline account.html, impor fungsi yang baru saja Anda buat dan tambahkan fungsi register yang memanggilnya, serta pengendali peristiwa yang dipasangkan ke tombol yang baru saja Anda buat.

// Set up the handler for the button that registers credentials
const registerButton = document.querySelector('#registerButton');
registerButton.addEventListener('click', register);

// Register a credential
async function register() {
  let user = {};
  try {
    const user = await registerCredential();
  } catch (e) {
    // Alert the user that something went wrong
    if (Array.isArray(e)) {
      alert(
        // `msg` not `message`, this is the key's name as per the express validator API
        `Registration failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
      );
    } else {
      alert(`Registration failed. ${e}`);
    }
  }
}

Tampilkan kredensial yang dapat dilihat pengguna

Setelah Anda menambahkan fungsi untuk membuat kredensial, pengguna memerlukan cara untuk melihat kredensial yang telah mereka tambahkan.

Halaman Akun adalah tempat yang tepat untuk hal ini.

Di account.html, cari fungsi bernama updateCredentialList().

Tambahkan kode berikut yang membuat panggilan backend untuk mengambil semua kredensial terdaftar untuk pengguna yang saat ini login, dan yang menampilkan kredensial yang ditampilkan:

// Update the list that displays credentials
async function updateCredentialList() {
  // Fetch the latest credential list from the backend
  const response = await _fetch('/auth/credentials', 'GET');
  const credentials = response.credentials || [];
  // Generate the credential list as HTML and pass remove/rename functions as args
  const credentialListHtml = getCredentialListHtml(
    credentials,
    removeEl,
    renameEl
  );
  // Display the list of credentials in the DOM
  const list = document.querySelector('#credentials');
  render(credentialListHtml, list);
}

Untuk saat ini, jangan pikirkan tentang removeEl dan renameEl; Anda akan mempelajarinya nanti dalam codelab ini.

Tambahkan satu panggilan ke updateCredentialList di awal skrip inline, dalam account.html. Dengan panggilan ini, kredensial yang tersedia diambil saat pengguna membuka halaman akunnya.

<script type="module">
    // ... (imports)
    // Initialize the credential list by updating it once on page load
    updateCredentialList();

Sekarang, panggil updateCredentialList setelah registerCredential berhasil diselesaikan, sehingga daftar menampilkan kredensial yang baru dibuat:

async function register() {
  let user = {};
  try {
    // ...
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

Cobalah! 👩🏻‍💻

Anda sudah menyelesaikan pendaftaran kredensial. Pengguna kini dapat membuat kredensial berbasis kunci keamanan, dan memvisualisasikannya di halaman Akun.

Coba:

  • Logout.
  • Login—dengan pengguna dan sandi apa pun. Seperti yang disebutkan sebelumnya, sandi ini tidak benar-benar diperiksa keakuratannya agar codelab ini tetap sederhana. Sandi tidak boleh kosong.
  • Setelah berada di halaman Akun, klik Tambahkan kredensial.
  • Anda akan diminta memasukkan dan menyentuh kunci keamanan. Lakukan.
  • Setelah berhasil membuat kredensial, kredensial akan ditampilkan di halaman akun.
  • Muat ulang halaman Akun. Kredensial akan ditampilkan.
  • Jika Anda memiliki dua kunci yang tersedia, coba tambahkan dua kunci keamanan yang berbeda sebagai kredensial. Keduanya seharusnya ditampilkan.
  • Cobalah membuat dua kredensial dengan pengautentikasi yang sama (kunci); Anda akan melihat bahwa opsi tersebut tidak didukung. Hal ini memang disengaja—hal ini dikarenakan penggunaan excludeCredentials di backend kita.

7. Mengaktifkan autentikasi faktor kedua

Pengguna dapat mendaftarkan dan membatalkan pendaftaran kredensial, tetapi kredensial hanya ditampilkan dan belum digunakan.

Sekarang saatnya menggunakannya, dan siapkan autentikasi 2 langkah yang sebenarnya.

Di bagian ini, Anda akan mengubah alur autentikasi di aplikasi web Anda dari alur dasar ini:

6ff49a7e520836d0.png

Untuk alur dua langkah ini:

e7409946cd88efc7.png

Mengimplementasikan autentikasi faktor kedua

Pertama, mari kita tambahkan fungsi yang diperlukan dan implementasikan komunikasi dengan backend; kita akan menambahkannya di frontend pada langkah berikutnya.

Hal yang perlu Anda terapkan di sini adalah fungsi yang mengautentikasi pengguna dengan kredensial.

Di public/auth.client.js, cari fungsi kosong authenticateTwoFactor, lalu tambahkan kode berikut:

async function authenticateTwoFactor() {
  // Fetch the 2F options from the backend
  const optionsFromServer = await _fetch("/auth/two-factor-options", "POST");
  // Decode them
  const decodedOptions = decodeServerOptions(optionsFromServer);
  // Get a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.get({
    publicKey: decodedOptions
  });
  // Encode the credential
  const encodedCredential = encodeCredential(credential);
  // Send it to the backend for verification
  return await _fetch("/auth/authenticate-two-factor", "POST", {
    credential: encodedCredential
  });
}

Perhatikan bahwa fungsi ini sudah diekspor untuk Anda; kita akan membutuhkannya di langkah berikutnya.

Berikut adalah hal yang dilakukan authenticateTwoFactor:

  • Fungsi ini meminta opsi autentikasi 2 langkah dari server. Sama seperti opsi pembuatan kredensial yang telah Anda lihat sebelumnya, opsi ini ditentukan di server dan bergantung pada model keamanan aplikasi web. Pelajari kode server di bagian router.post("/two-factors-options", ... untuk mengetahui detailnya.
  • Dengan memanggil navigator.credentials.get, tindakan ini memungkinkan browser mengambil alih dan meminta pengguna memasukkan dan menyentuh kunci yang terdaftar sebelumnya. Hal ini membuat kredensial dipilih khusus untuk operasi autentikasi faktor kedua ini.
  • Kredensial yang dipilih kemudian akan diteruskan dalam permintaan backend untuk mengambil("/auth/authenticate-two-factor"`. Jika kredensial tersebut valid untuk pengguna tersebut, pengguna akan diautentikasi.

Tambahan lain: lihat kode server

Perhatikan bahwa server.js telah menangani beberapa navigasi dan akses: hal ini memastikan bahwa halaman Akun hanya dapat diakses oleh pengguna yang diautentikasi, dan melakukan beberapa pengalihan yang diperlukan.

Sekarang, lihat kode server di bagian router.post("/initialize-authentication", ....

Ada dua hal menarik yang perlu diperhatikan di sini:

  • Sandi dan kredensial diperiksa secara bersamaan pada tahap ini. Ini adalah tindakan pengamanan: bagi pengguna yang telah menyiapkan autentikasi 2 langkah, kita tidak ingin alur UI terlihat berbeda bergantung pada apakah sandi benar atau tidak. Jadi, kita memeriksa sandi dan kredensial secara bersamaan di langkah ini.
  • Jika sandi dan kredensial valid, kita kemudian menyelesaikan autentikasi dengan memanggil completeAuthentication(req, res); Artinya, dalam praktiknya kita beralih sesi, dari sesi auth sementara ketika pengguna belum diautentikasi, ke sesi utama main tempat pengguna diautentikasi.

Sertakan halaman autentikasi faktor kedua di alur penggunaan

Di folder views, perhatikan halaman baru second-factor.html.

Tombol ini bertuliskan Gunakan kunci keamanan, tetapi untuk saat ini tombol tersebut tidak melakukan apa pun.

Buat tombol ini memanggil authenticateTwoFactor() saat diklik.

  • Jika authenticateTwoFactor() berhasil, alihkan pengguna ke halaman Akun mereka.
  • Jika tidak berhasil, beri tahu pengguna bahwa terjadi error. Dalam aplikasi yang sebenarnya, Anda akan mengimplementasikan pesan error yang lebih membantu— agar demo ini sederhana, kita hanya akan menggunakan peringatan jendela.
    <main>
...
    </main>
    <script type="module">
      import { authenticateTwoFactor, authStatuses } from "/auth.client.js";

      const button = document.querySelector("#authenticateButton");
      button.addEventListener("click", async e => {
        try {
          // Ask the user to authenticate with the second factor; this will trigger a browser prompt
          const response = await authenticateTwoFactor();
          const { authStatus } = response;
          if (authStatus === authStatuses.COMPLETE) {
            // The user is properly authenticated => Navigate to the Account page
            location.href = "/account";
          } else {
            throw new Error("Two-factor authentication failed");
          }
        } catch (e) {
          // Alert the user that something went wrong
          alert(`Two-factor authentication failed. ${e}`);
        }
      });
    </script>
  </body>
</html>

Menggunakan autentikasi faktor kedua

Sekarang Anda sudah siap untuk menambahkan langkah autentikasi faktor kedua.

Yang perlu Anda lakukan sekarang adalah menambahkan langkah ini dari index.html, untuk pengguna yang telah mengonfigurasi autentikasi 2 langkah.

322a5c49d865a0d8.png

Pada index.html, di bagian location.href = "/account";, tambahkan kode yang secara bersyarat menavigasi pengguna ke halaman autentikasi faktor kedua jika mereka telah menyiapkan 2FA.

Dalam codelab ini, membuat kredensial secara otomatis mengikutsertakan pengguna ke autentikasi 2 langkah.

Perhatikan bahwa server.js juga mengimplementasikan pemeriksaan sesi sisi server, yang memastikan bahwa hanya pengguna terautentikasi yang dapat mengakses account.html.

const { authStatus } = response;
if (authStatus === authStatuses.COMPLETE) {
  // The user is properly authenticated => navigate to account
  location.href = '/account';
} else if (authStatus === authStatuses.NEED_SECOND_FACTOR) {
  // Navigate to the two-factor-auth page because two-factor-auth is set up for this user
  location.href = '/second-factor';
}

Cobalah! 👩🏻‍💻

  • Login dengan pengguna baru johndoe.
  • Logout.
  • Login ke akun Anda sebagai johndoe; perhatikan bahwa hanya sandi yang diperlukan.
  • Buat kredensial. Hal ini berarti Anda telah mengaktifkan autentikasi 2 langkah sebagai johndoe.
  • Logout.
  • Masukkan nama pengguna johndoe dan sandi Anda.
  • Lihat bagaimana Anda secara otomatis dinavigasikan ke halaman autentikasi faktor kedua.
  • (Coba akses halaman Akun di /account; perhatikan bagaimana Anda dialihkan ke halaman indeks karena Anda belum sepenuhnya diautentikasi: Anda tidak memiliki faktor kedua)
  • Kembali ke halaman autentikasi faktor kedua, lalu klik Gunakan kunci keamanan untuk mengautentikasi faktor kedua.
  • Sekarang Anda telah login dan akan melihat halaman Akun Anda.

8. Memudahkan penggunaan kredensial

Anda telah mempelajari fungsi dasar autentikasi 2 langkah dengan kunci keamanan 🚀

Tapi... Apakah Anda memperhatikan?

Untuk saat ini, daftar kredensial kita tidak praktis: ID kredensial dan kunci publik adalah string panjang yang tidak membantu saat mengelola kredensial. Manusia tidak terlalu baik dalam hal string dan angka yang panjang 🤖

Jadi, mari kita tingkatkan, dan tambahkan fungsi untuk memberi nama dan mengganti nama kredensial dengan string yang dapat dibaca manusia.

Melihat renameCredential

Fungsi untuk mengganti nama kredensial telah ditambahkan untuk Anda dalam kode awal, di auth.client.js, guna menghemat waktu saat menerapkan fungsi ini yang tidak terlalu berpengaruh.

async function renameCredential(credId, newName) {
  const params = new URLSearchParams({
    credId,
    name: newName
  });
  return _fetch(
    `/auth/credential?${params}`,
    "PUT"
  );
}

Ini adalah panggilan update database rutin: klien mengirimkan permintaan PUT ke backend, dengan ID kredensial dan nama baru untuk kredensial tersebut.

Mengimplementasikan nama kredensial kustom

Di account.html, perhatikan fungsi kosong rename.

Tambahkan kode berikut:

// Rename a credential
async function rename(credentialId) {
  // Let the user input a new name
  const newName = window.prompt(`Name this credential:`);
  // Rename only if the user didn't cancel AND didn't enter an empty name
  if (newName && newName.trim()) {
    try {
      // Make the backend call to rename the credential (the name is sanitized) server-side
      await renameCredential(credentialId, newName);
    } catch (e) {
      // Alert the user that something went wrong
      if (Array.isArray(e)) {
        alert(
          // `msg` not `message`, this is the key's name as per the express validator API
          `Renaming failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
        );
      } else {
        alert(`Renaming failed. ${e}`);
      }
    }
    // Refresh the credential list to display the new name
    await updateCredentialList();
  }
}

Mungkin lebih masuk akal untuk memberi nama kredensial hanya setelah kredensial berhasil dibuat. Jadi, mari kita buat kredensial tanpa nama, dan setelah berhasil dibuat, ganti nama kredensial tersebut. Namun, hal ini akan menghasilkan dua panggilan backend.

Gunakan fungsi rename di register(), agar pengguna dapat menamai kredensial saat pendaftaran:

async function register() {
  let user = {};
  try {
    const user = await registerCredential();
    // Get the latest credential's ID (newly created credential)
    const allUserCredentials = user.credentials;
    const newCredential = allUserCredentials[allUserCredentials.length - 1];
    // Rename it
    await rename(newCredential.credId);
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

Perhatikan bahwa input pengguna akan divalidasi dan dibersihkan dalam backend:

  check("name")
    .trim()
    .escape()

Menampilkan nama kredensial

Buka getCredentialHtml di templates.js.

Perlu diperhatikan bahwa sudah ada kode untuk menampilkan nama kredensial di bagian atas kartu kredensial:

// Register credential
const getCredentialHtml = (credential, removeEl, renameEl) => {
 const { name, credId, publicKey } = credential;
 return html`
    <div class="credential-card">
      <div class="credential-name">
        ${name
          ? html`
              ${name}
            `
          : html`
              <span class="unnamed">(Unnamed)</span>
            `}
      </div>
     // ...
    </div>
  `;
};

Cobalah! 👩🏻‍💻

  • Buat kredensial.
  • Anda akan diminta untuk menamainya.
  • Masukkan nama baru, lalu klik OK.
  • Kredensial kini telah diganti namanya.
  • Ulangi dan periksa apakah semuanya berjalan lancar saat kolom nama dibiarkan kosong.

Mengaktifkan penggantian nama kredensial

Pengguna mungkin perlu mengganti nama kredensial–misalnya, mereka menambahkan kunci kedua dan ingin mengganti nama kunci pertama mereka untuk dapat membedakannya dengan lebih baik.

Di account.html, cari fungsi renameEl yang masih kosong dan tambahkan kode berikut ke dalamnya:

// Rename a credential via HTML element
async function renameEl(el) {
  // Define the ID of the credential to update
  const credentialId = el.srcElement.dataset.credentialId;
  // Rename the credential
  await rename(credentialId);
  // Refresh the credential list to display the new name
  await updateCredentialList();
}

Sekarang, di getCredentialHtml templates.js, dalam div class="flex-end", tambahkan kode berikut. Kode ini menambahkan tombol Ganti nama ke template kartu kredensial; saat diklik, tombol tersebut akan memanggil fungsi renameEl yang baru saja kita buat:

const getCredentialHtml = (credential, removeEl, renameEl) => {
// ...
 <div class="flex-end">
  <button
    data-credential-id="${credId}"
    @click="${renameEl}"
    class="secondary right"
  >
   Rename
  </button>
 </div>
 // ...
  `;
};

Cobalah! 👩🏻‍💻

  • Klik Ganti nama.
  • Masukkan nama baru saat diminta.
  • Klik OK.
  • Kredensial seharusnya berhasil diganti namanya, dan daftar akan diperbarui secara otomatis.
  • Memuat ulang halaman akan tetap menampilkan nama baru (ini menunjukkan bahwa nama baru tetap dipertahankan di sisi server).

Menampilkan tanggal pembuatan kredensial

Tanggal pembuatan tidak ada di kredensial yang dibuat melalui navigator.credential.create().

Namun, karena informasi ini dapat berguna bagi pengguna untuk membedakan kredensial, kami telah menyesuaikan library sisi server dalam kode awal untuk Anda, dan menambahkan kolom creationDate yang sama dengan Date.now() setelah menyimpan kredensial baru.

Pada templates.js di class="creation-date" div, tambahkan hal berikut untuk menampilkan informasi tanggal pembuatan kepada pengguna:

<div class="creation-date">
  <label>Created:</label>
  <div class="info">
    ${new Date(creationDate).toLocaleDateString()}
    ${new Date(creationDate).toLocaleTimeString()}
  </div>
</div>

9. Membuat kode Anda sesuai untuk masa mendatang

Sejauh ini, kita hanya meminta pengguna mendaftarkan pengautentikasi roaming sederhana yang kemudian digunakan sebagai faktor kedua selama login.

Satu pendekatan yang lebih canggih adalah mengandalkan jenis pengautentikasi yang lebih kuat: pengautentikasi roaming pemverifikasi pengguna (UVRA). UVRA dapat memberikan dua faktor autentikasi dan ketahanan terhadap phishing dalam alur login satu langkah.

Idealnya, Anda mendukung kedua pendekatan tersebut. Untuk melakukannya, Anda harus menyesuaikan pengalaman pengguna:

  • Jika pengguna hanya memiliki pengautentikasi roaming sederhana (yang bukan pemverifikasi pengguna), izinkan mereka menggunakannya untuk mencapai bootstrap akun tahan phishing, namun mereka juga harus mengetik nama pengguna dan sandi. Inilah yang sudah dilakukan oleh codelab kita.
  • Jika pengguna lain memiliki pengautentikasi roaming pemverifikasi pengguna yang lebih canggih, mereka akan dapat melewati langkah sandi—dan mungkin juga langkah nama pengguna—selama bootstrap akun.

Pelajari hal ini lebih lanjut di Melakukan Bootstrap Akun Tahan Phishing dengan Login Tanpa Sandi Opsional.

Dalam codelab ini, kita tidak akan menyesuaikan pengalaman pengguna, tetapi kita akan menyiapkan codebase agar Anda memiliki data yang diperlukan untuk menyesuaikan pengalaman pengguna.

Anda memerlukan dua hal:

  • Menetapkan residentKey: preferred di setelan backend Anda. Hal ini sudah dilakukan untuk Anda.
  • Menyiapkan cara untuk mengetahui apakah kredensial yang dapat ditemukan (juga disebut kunci penduduk) dibuat atau tidak.

Untuk mengetahui apakah kredensial yang dapat ditemukan dibuat atau tidak:

  • Buat kueri nilai credProps setelah pembuatan kredensial (credProps: true).
  • Buat kueri nilai transports setelah pembuatan kredensial. Tindakan ini akan membantu Anda menentukan apakah dasar platform mendukung fungsi UVRA, misalnya apakah benar-benar telepon seluler.
  • Simpan nilai credProps dan transports di backend. Hal ini sudah dilakukan untuk Anda di kode awal. Lihat auth.js jika Anda ingin tahu.

Mari kita dapatkan nilai credProps dan transports, lalu kirimkan ke backend. Di auth.client.js, ubah registerCredential sebagai berikut:

  • Tambahkan kolom extensions saat memanggil navigator.credentials.create
  • Siapkan encodedCredential.transports dan encodedCredential.credProps sebelum mengirimkan kredensial ke backend untuk disimpan.

registerCredential akan terlihat seperti berikut:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    '/auth/credential-options',
    'POST'
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
      extensions: {
        credProps: true,
      },
    },
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Set transports and credProps for more advanced user flows
  encodedCredential.transports = credential.response.getTransports();
  encodedCredential.credProps =
    credential.getClientExtensionResults().credProps;
  // Send the encoded credential to the backend for storage
  return await _fetch('/auth/credential', 'POST', encodedCredential);
}

10. Memastikan dukungan lintas browser

Mendukung browser non-Chromium

Dalam fungsi registerCredential public/auth.client.js, kita memanggil credential.response.getTransports() pada kredensial yang baru dibuat untuk menyimpan informasi ini di backend sebagai petunjuk ke server.

Namun, getTransports() saat ini tidak diimplementasikan di semua browser (tidak seperti getClientExtensionResults yang didukung di seluruh browser): panggilan getTransports() akan menampilkan error di Firefox dan Safari, yang akan mencegah pembuatan kredensial di browser ini.

Untuk memastikan kode Anda berjalan di semua browser utama, gabungkan panggilan encodedCredential.transports dalam kondisi:

if (credential.response.getTransports) {
  encodedCredential.transports = credential.response.getTransports();
}

Perlu diketahui bahwa pada server, transports ditetapkan ke transports || []. Di Firefox dan Safari, daftar transports tidak akan menjadi undefined, tetapi daftar kosong [], yang mencegah error.

Berikan peringatan kepada pengguna yang menggunakan browser yang tidak mendukung WebAuthn

1e9c1be837d66ce8.png

Meskipun WebAuthn didukung di semua browser utama, sebaiknya tampilkan peringatan di browser yang tidak mendukung WebAuthn.

Di index.html, amati kehadiran div ini:

<div id="warningbanner" class="invisible">
⚠️ Your browser doesn't support WebAuthn. Open this demo in Chrome, Edge, Firefox or Safari.
</div>

Di skrip inline index.html, tambahkan kode berikut untuk menampilkan banner di browser yang tidak mendukung WebAuthn:

// Display a banner in browsers that don't support WebAuthn
if (!window.PublicKeyCredential) {
  document.querySelector('#warningbanner').classList.remove('invisible');
}

Dalam aplikasi web yang sebenarnya, Anda akan melakukan hal yang lebih rumit dan memiliki mekanisme penggantian yang sesuai untuk browser tersebut—tetapi hal ini menunjukkan cara memeriksa dukungan WebAuthn.

11. Kerja bagus!

✨Selesai!

Anda telah mengimplementasikan autentikasi 2 langkah dengan kunci keamanan.

Dalam codelab ini, kita telah membahas dasar-dasarnya. Jika Anda ingin menjelajahi WebAuthn untuk 2FA lebih lanjut, berikut beberapa ide yang dapat Anda coba selanjutnya:

  • Tambahkan informasi "Terakhir digunakan" ke kartu kredensial. Informasi ini berguna bagi pengguna untuk menentukan apakah kunci keamanan tertentu digunakan secara aktif atau tidak—terutama jika mereka telah mendaftarkan beberapa kunci.
  • Mengimplementasikan penanganan error yang lebih kuat dan pesan error yang lebih tepat.
  • Lihat auth.js, dan pelajari hal yang terjadi jika Anda mengubah beberapa authSettings, khususnya saat menggunakan kunci yang mendukung verifikasi pengguna.