Autentikasi kunci sandi sisi server

Ringkasan

Berikut adalah ringkasan umum langkah-langkah penting yang diperlukan dalam autentikasi kunci sandi:

Alur autentikasi kunci sandi

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

Bagian berikut membahas detail setiap langkah.

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 tantangan yang sama tidak pernah digunakan lebih dari sekali. Buat tantangan baru setiap kali login. Hapus tantangan setelah setiap upaya masuk, baik berhasil maupun gagal. Buang juga tantangan setelah durasi tertentu. Jangan terima verifikasi login yang sama dalam satu respons lebih dari sekali.
  2. Pastikan tantangan ini aman secara kriptografis. Tantangan hampir tidak mungkin ditebak. Untuk membuat sisi server tantangan yang aman secara kriptografi, 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 secret di Python. Sesuai dengan spesifikasi, tantangan harus setidaknya sepanjang 16 byte agar dianggap aman.

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

Opsi pembuatan permintaan kredensial

Buat opsi permintaan kredensial sebagai objek publicKeyCredentialRequestOptions.

Untuk melakukannya, andalkan library sisi server FIDO. Biasanya alat ini akan menawarkan fungsi utilitas yang dapat membuat opsi tersebut untuk Anda. Penawaran SimpleWebAuthn, 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 dari publicKeyCredentialRequestOptions' bidang bisa berupa konstanta. Yang lainnya harus ditentukan secara dinamis di server:

  • rpId: ID RP mana yang Anda harapkan akan 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 selama pendaftaran kredensial.
  • challenge: Data yang akan ditandatangani oleh penyedia kunci sandi untuk membuktikan bahwa pengguna memiliki kunci sandi pada saat permintaan autentikasi. Tinjau detailnya 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 Mengambil tantangan dari server RP dan Pembahasan mendalam tentang kredensial yang dapat ditemukan untuk mengetahui detailnya.
  • userVerification: Menunjukkan apakah verifikasi pengguna yang menggunakan kunci layar perangkat "diperlukan", "lebih disukai" atau "tidak direkomendasikan". Tinjau Mengambil verifikasi login dari server RP.
  • timeout: Berapa lama waktu (dalam milidetik) yang dapat diperlukan pengguna untuk menyelesaikan autentikasi. Nilai ini harus cukup longgar, dan lebih singkat daripada masa aktif challenge. Nilai default yang direkomendasikan adalah 5 menit, tetapi Anda dapat meningkatkannya — hingga 10 menit, yang masih dalam rentang yang direkomendasikan. Waktu tunggu yang lama wajar jika Anda memperkirakan pengguna akan 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 dikirim oleh server
Opsi yang dikirim oleh server. Dekode challenge terjadi di sisi klien.

Contoh kode: opsi permintaan kredensial

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

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 memproses login pengguna

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. Hal ini merepresentasikan respons penyedia kunci sandi terhadap petunjuk klien untuk membuat hal yang diperlukan untuk mencoba dan 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 pada nilai ini.

Kirim objek PublicKeyCredential ke server.

Di server, lakukan tindakan berikut:

Skema database
Skema database yang disarankan. Pelajari desain ini lebih lanjut di Pendaftaran kunci sandi sisi server.
  • Kumpulkan informasi yang Anda perlukan untuk memverifikasi pernyataan dan mengautentikasi pengguna:
    • Dapatkan verifikasi login yang diharapkan yang Anda simpan dalam sesi saat Anda membuat opsi autentikasi.
    • Dapatkan origin dan ID RP yang diharapkan.
    • Temukan di database Anda siapa penggunanya. 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 kredensial id yang cocok dengan kredensial id yang ada di objek PublicKeyCredential. Kemudian, cari pengguna yang sesuai menggunakan kunci asing passkey_user_id untuk tabel Users 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 kredensial id yang cocok dengan kredensial id yang ada di objek PublicKeyCredential.
  • Verifikasi pernyataan autentikasi. Serahkan langkah verifikasi ini ke library sisi server FIDO Anda, yang biasanya akan menawarkan fungsi utilitas untuk tujuan ini. Penawaran SimpleWebAuthn, misalnya, verifyAuthenticationResponse. Pelajari apa yang terjadi di balik layar di Lampiran: verifikasi respons autentikasi.

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

  • Buat pengguna login. Jika verifikasi berhasil, perbarui informasi sesi untuk menandai pengguna sebagai login. Anda juga dapat menampilkan objek user ke klien, sehingga frontend dapat menggunakan informasi yang terkait dengan pengguna yang baru login.

Kode contoh: memverifikasi dan memproses login pengguna

Kami menggunakan library SimpleWebAuthn dalam contoh kami. Di sini, kami 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 asal permintaan cocok dengan asal login situs Anda. Untuk aplikasi Android, tinjau 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 diwajibkan untuk kunci sandi.
  • Verifikasi tanda tangan. Untuk memverifikasi tanda tangan, Anda memerlukan:
    • Signature, yang merupakan tantangan bertanda tangan: response.signature
    • Kunci publik, yang digunakan untuk memverifikasi 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 SimpleWebAuthn untuk verifyAuthenticationResponse atau pelajari daftar lengkap verifikasi di spesifikasi.