Autentikasi kunci sandi sisi server

Ringkasan

Berikut adalah ringkasan tingkat tinggi langkah-langkah utama yang terlibat dalam autentikasi kunci sandi:

Alur autentikasi kunci sandi

  • Tentukan verifikasi login dan opsi lain yang diperlukan untuk melakukan autentikasi dengan kunci sandi. Kirim kunci tersebut ke klien agar Anda dapat meneruskannya ke panggilan autentikasi kunci sandi (navigator.credentials.get di web). Setelah pengguna mengonfirmasi autentikasi kunci sandi, panggilan autentikasi kunci sandi akan diselesaikan dan menampilkan kredensial (PublicKeyCredential). Kredensial tersebut berisi pernyataan autentikasi.
  • Verifikasi pernyataan autentikasi.
  • Jika pernyataan otentikasi valid, lakukan autentikasi pengguna.

Bagian berikut akan membahas setiap langkah secara spesifik.

Membuat tantangan

Dalam praktiknya, tantangan adalah array byte acak yang direpresentasikan sebagai objek ArrayBuffer.

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

Untuk memastikan tantangan memenuhi tujuannya, Anda harus:

  1. Pastikan verifikasi login yang sama tidak pernah digunakan lebih dari sekali. Buat tantangan baru pada setiap upaya login. Hapus tantangan setelah setiap upaya login, baik berhasil maupun gagal. Buang juga tantangan setelah durasi tertentu. Jangan terima verifikasi login yang sama dalam satu respons lebih dari sekali.
  2. Pastikan verifikasi login aman secara kriptografis. Tantangan seharusnya sulit ditebak. Untuk membuat tantangan sisi server yang aman secara kriptografis, sebaiknya andalkan library sisi server FIDO yang Anda percayai. Jika Anda membuat tantangan sendiri, gunakan fungsi kriptografi bawaan yang tersedia di tech stack Anda, atau cari library yang didesain untuk kasus penggunaan kriptografi. Contohnya mencakup iso-crypto di Node.js, atau secrets di Python. Sesuai dengan spesifikasi, tantangan harus memiliki panjang minimal 16 byte agar dianggap aman.

Setelah Anda membuat tantangan, simpan di sesi pengguna untuk memverifikasinya nanti.

Membuat opsi permintaan kredensial

Buat opsi permintaan kredensial sebagai objek publicKeyCredentialRequestOptions.

Untuk melakukannya, andalkan library sisi server FIDO. Biasanya akan menawarkan fungsi utilitas yang dapat membuat opsi tersebut untuk Anda. SimpleWebAuthn menawarkan, misalnya, generateAuthenticationOptions.

publicKeyCredentialRequestOptions harus berisi semua informasi yang diperlukan untuk autentikasi kunci sandi. Teruskan informasi ini ke fungsi di library sisi server FIDO yang bertanggung jawab untuk membuat objek publicKeyCredentialRequestOptions.

Beberapa kolom publicKeyCredentialRequestOptions dapat berupa konstanta. Atribut lainnya harus ditentukan secara dinamis di server:

  • rpId: ID RP mana yang Anda harapkan untuk dikaitkan dengan kredensial, misalnya example.com. Autentikasi hanya akan berhasil jika ID RP yang Anda berikan di sini cocok dengan ID RP yang terkait dengan kredensial. Untuk mengisi ID RP, gunakan nilai yang sama dengan ID RP yang Anda tetapkan di publicKeyCredentialCreationOptions saat pendaftaran kredensial.
  • challenge: Data yang akan ditandatangani oleh penyedia kunci sandi untuk membuktikan bahwa pengguna memegang kunci sandi pada saat permintaan autentikasi. Tinjau detail di Membuat tantangan.
  • allowCredentials: Array kredensial yang dapat diterima untuk autentikasi ini. Teruskan array kosong agar pengguna dapat memilih kunci sandi yang tersedia dari daftar yang ditampilkan oleh browser. Tinjau artikel Mengambil tantangan dari server RP dan Mempelajari kredensial yang dapat ditemukan untuk mengetahui detailnya.
  • userVerification: Menunjukkan apakah verifikasi pengguna yang menggunakan kunci layar perangkat "wajib", "lebih disarankan", atau "tidak direkomendasikan". Tinjau Mengambil tantangan dari server RP.
  • timeout: Berapa lama (dalam milidetik) waktu yang dapat dibutuhkan pengguna untuk menyelesaikan autentikasi. Nilai ini harus cukup murah, dan lebih singkat daripada masa pakai challenge. Nilai default yang direkomendasikan adalah 5 menit, tetapi Anda dapat meningkatkannya — hingga 10 menit, yang masih berada dalam rentang yang direkomendasikan. Waktu tunggu yang lama dapat diterima jika Anda mengharapkan pengguna menggunakan alur kerja campuran, yang biasanya memerlukan waktu sedikit lebih lama. Jika waktu operasi habis, NotAllowedError akan ditampilkan.

Setelah Anda membuat publicKeyCredentialRequestOptions, kirimkan ke klien.

publicKeyCredentialCreationOptions yang dikirim oleh server
Opsi yang dikirim oleh server. Dekode challenge terjadi di sisi klien.

Kode contoh: membuat opsi permintaan kredensial

Kami menggunakan library SimpleWebAuthn dalam contoh kami. Di sini, kita menyerahkan pembuatan opsi permintaan kredensial ke fungsi generateAuthenticationOptions-nya.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';

router.post('/signinRequest', csrfCheck, async (req, res) => {

  // Ensure you nest calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Use the generateAuthenticationOptions function from SimpleWebAuthn
    const options = await generateAuthenticationOptions({
      rpID: process.env.HOSTNAME,
      allowCredentials: [],
    });
    // Save the challenge in the user session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).json({ error: e.message });
  }
});

Memverifikasi dan membuat pengguna login

Saat navigator.credentials.get berhasil di-resolve di klien, objek PublicKeyCredential akan ditampilkan.

Objek PublicKeyCredential yang dikirim oleh server
navigator.credentials.get menampilkan PublicKeyCredential.

response adalah AuthenticatorAssertionResponse. Kunci sandi mewakili respons penyedia kunci sandi terhadap petunjuk klien untuk membuat hal yang diperlukan untuk mencoba melakukan autentikasi dengan kunci sandi di RP. File tersebut berisi:

  • response.authenticatorDatadanresponse.clientDataJSON, seperti pada langkah pendaftaran kunci sandi.
  • response.signature yang berisi tanda tangan atas nilai ini.

Kirim objek PublicKeyCredential ke server.

Di server, lakukan hal berikut:

Skema database
Skema database yang disarankan. Pelajari lebih lanjut desain ini di Pendaftaran kunci sandi sisi server.
  • Kumpulkan informasi yang Anda perlukan untuk memverifikasi pernyataan dan mengautentikasi pengguna:
    • Dapatkan verifikasi login yang Anda simpan di sesi saat membuat opsi autentikasi.
    • Dapatkan origin dan ID RP yang diharapkan.
    • Menemukan siapa pengguna di database Anda. Dalam kasus kredensial yang dapat ditemukan, Anda tidak tahu siapa pengguna yang membuat permintaan autentikasi. Untuk mengetahuinya, Anda memiliki dua opsi:
      • Opsi 1: Gunakan response.userHandle di objek PublicKeyCredential. Di tabel Pengguna, cari passkey_user_id yang cocok dengan userHandle.
      • Opsi 2: Gunakan kredensial id yang ada di objek PublicKeyCredential. Di tabel Public key credentials, cari id kredensial yang cocok dengan id kredensial yang ada di objek PublicKeyCredential. Kemudian cari pengguna yang sesuai menggunakan kunci asing passkey_user_id untuk tabel Pengguna Anda.
    • Temukan informasi kredensial kunci publik yang cocok dengan pernyataan autentikasi yang Anda terima di database Anda. Untuk melakukannya, di tabel Public key credentials, cari id kredensial yang cocok dengan kredensial idyang ada di objek PublicKeyCredential.
  • Verifikasi pernyataan autentikasi. Serahkan langkah verifikasi ini ke library sisi server FIDO, yang biasanya akan menawarkan fungsi utilitas untuk tujuan ini. SimpleWebAuthn menawarkan, misalnya, verifyAuthenticationResponse. Pelajari seluk-beluknya di Lampiran: verifikasi respons autentikasi.

  • Menghapus tantangan apakah verifikasi berhasil atau tidak, untuk mencegah serangan replay.

  • Membuat pengguna login. Jika verifikasi berhasil, perbarui informasi sesi untuk menandai pengguna sebagai login. Sebaiknya tampilkan objek user ke klien juga agar frontend dapat menggunakan informasi yang terkait dengan pengguna yang baru login.

Kode contoh: memverifikasi dan membuat pengguna login

Kami menggunakan library SimpleWebAuthn dalam contoh kami. Di sini, kita menyerahkan verifikasi respons autentikasi ke fungsi verifyAuthenticationResponse-nya.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';

router.post('/signinResponse', csrfCheck, async (req, res) => {
  const response = req.body;
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;

  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Find the credential stored to the database by the credential ID
    const cred = Credentials.findById(response.id);
    if (!cred) {
      throw new Error('Credential not found.');
    }
    // Find the user - Here alternatively we could look up the user directly
    // in the Users table via userHandle
    const user = Users.findByPasskeyUserId(cred.passkey_user_id);
    if (!user) {
      throw new Error('User not found.');
    }
    // Base64URL decode some values
    const authenticator = {
      credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
      credentialID: isoBase64URL.toBuffer(cred.id),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      authenticator,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('User verification failed.');
    }

    // Kill the challenge for this session.
    delete req.session.challenge;

    req.session.username = user.username;
    req.session['signed-in'] = 'yes';

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).json({ error: e.message });
  }
});

Lampiran: verifikasi respons autentikasi

Verifikasi respons autentikasi terdiri dari pemeriksaan berikut:

  • Pastikan ID RP cocok dengan situs Anda.
  • Pastikan origin permintaan cocok dengan asal login situs Anda. Untuk aplikasi Android, tinjau bagian Memverifikasi origin.
  • Periksa apakah perangkat dapat memberikan tantangan yang Anda berikan.
  • Verifikasi bahwa selama autentikasi, pengguna telah mengikuti persyaratan yang Anda mandatkan sebagai RP. Jika Anda mewajibkan verifikasi pengguna, pastikan tanda uv (diverifikasi pengguna) di authenticatorData adalah true. Pastikan tanda up (ada pengguna) di authenticatorData adalah true, karena kehadiran pengguna selalu diperlukan untuk kunci sandi.
  • Memverifikasi tanda tangan. Untuk memverifikasi tanda tangan, Anda memerlukan:
    • Tanda tangan, yang merupakan tantangan yang ditandatangani: response.signature
    • Kunci publik, untuk melakukan verifikasi tanda tangan.
    • Data asli yang ditandatangani. Ini adalah data yang tanda tangannya akan diverifikasi.
    • Algoritma kriptografi yang digunakan untuk membuat tanda tangan.

Untuk mempelajari langkah-langkah ini lebih lanjut, periksa kode sumber untuk verifyAuthenticationResponse SimpleWebAuthn atau pelajari daftar lengkap verifikasi di spesifikasi.