Pendaftaran kunci sandi sisi server

Ringkasan

Berikut adalah ringkasan umum langkah-langkah utama yang diperlukan dalam pendaftaran kunci sandi:

Alur pendaftaran kunci sandi

  • Tentukan opsi untuk membuat kunci sandi. Kirim kunci sandi ke klien, agar Anda dapat meneruskannya ke panggilan pembuatan kunci sandi: panggilan API WebAuthn navigator.credentials.create di web, dan credentialManager.createCredential di Android. Setelah pengguna mengonfirmasi pembuatan kunci sandi, panggilan pembuatan kunci sandi akan diselesaikan dan menampilkan kredensial PublicKeyCredential.
  • Verifikasi kredensial dan simpan di server.

Bagian berikut membahas detail setiap langkah.

Membuat opsi pembuatan kredensial

Langkah pertama yang harus Anda lakukan di server adalah membuat objek PublicKeyCredentialCreationOptions.

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

PublicKeyCredentialCreationOptions harus menyertakan semua yang diperlukan untuk pembuatan kunci sandi: informasi tentang pengguna, tentang RP, dan konfigurasi untuk properti kredensial yang Anda buat. Setelah menentukan semua ini, teruskan sesuai kebutuhan ke fungsi di library sisi server FIDO yang bertanggung jawab untuk membuat objek PublicKeyCredentialCreationOptions.

Beberapa dari PublicKeyCredentialCreationOptions' bidang bisa berupa konstanta. Yang lainnya harus ditentukan secara dinamis di server:

  • rpId: Untuk mengisi ID RP di server, gunakan fungsi sisi server atau variabel yang memberikan nama host aplikasi web Anda, seperti example.com.
  • user.name dan user.displayName: Untuk mengisi kolom ini, gunakan informasi sesi pengguna yang login (atau informasi akun pengguna baru, jika pengguna membuat kunci sandi saat mendaftar). user.name biasanya berupa alamat email, dan bersifat unik untuk RP. user.displayName adalah nama yang mudah digunakan. Perlu diketahui bahwa tidak semua platform akan menggunakan displayName.
  • user.id: String unik dan acak yang dibuat saat pembuatan akun. Nama pengguna harus bersifat permanen, tidak seperti nama pengguna yang dapat diedit. ID pengguna mengidentifikasi akun, tetapi tidak boleh berisi informasi identitas pribadi (PII). Anda mungkin sudah memiliki ID pengguna di sistem, tetapi jika perlu, buat ID khusus untuk kunci sandi agar bebas dari PII.
  • excludeCredentials: Daftar kredensial yang ada ID kunci untuk mencegah duplikasi kunci sandi dari penyedia kunci sandi. Untuk mengisi kolom ini, cari kredensial yang ada untuk pengguna ini di database Anda. Tinjau detailnya di Mencegah pembuatan kunci sandi baru jika sudah ada.
  • challenge: Untuk pendaftaran kredensial, tantangan tidak relevan kecuali jika Anda menggunakan pengesahan, teknik yang lebih canggih untuk memverifikasi identitas penyedia kunci sandi dan data yang dihasilkannya. Namun, meskipun Anda tidak menggunakan pengesahan, tantangan masih menjadi bidang yang wajib diisi. Dalam hal ini, Anda dapat menetapkan tantangan ini ke satu 0 agar lebih praktis. Petunjuk untuk membuat verifikasi login aman untuk autentikasi tersedia di Autentikasi kunci sandi sisi server.

Encoding dan decoding

PublicKeyCredentialCreationOptions dikirim oleh server
PublicKeyCredentialCreationOptions dikirim oleh server. challenge, user.id, dan excludeCredentials.credentials harus dienkode sisi server menjadi base64URL, sehingga PublicKeyCredentialCreationOptions dapat dikirim melalui HTTPS.

PublicKeyCredentialCreationOptions menyertakan kolom yang berupa ArrayBuffer, sehingga tidak didukung oleh JSON.stringify(). Artinya, saat ini, untuk mengirimkan PublicKeyCredentialCreationOptions melalui HTTPS, beberapa kolom harus dienkode secara manual di server menggunakan base64URL, lalu didekode di klien.

  • Di server, encoding dan decoding biasanya ditangani oleh library sisi server FIDO.
  • Di klien, encoding dan decoding harus dilakukan secara manual pada saat itu. Hal ini akan menjadi lebih mudah di masa mendatang: metode untuk mengonversi opsi sebagai JSON menjadi PublicKeyCredentialCreationOptions akan tersedia. Lihat status penerapan di Chrome.

Contoh kode: membuat opsi pembuatan kredensial

Kami menggunakan library SimpleWebAuthn dalam contoh kami. Di sini, kita menyerahkan pembuatan opsi kredensial kunci publik ke fungsi generateRegistrationOptions.

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

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // 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 {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: isoBase64URL.toBuffer(cred.id),
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: user.id,
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

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

Menyimpan kunci publik

PublicKeyCredentialCreationOptions dikirim oleh server
navigator.credentials.create menampilkan objek PublicKeyCredential.

Jika navigator.credentials.create berhasil di-resolve di klien, artinya kunci sandi telah berhasil dibuat. Objek PublicKeyCredential ditampilkan.

Objek PublicKeyCredential berisi objek AuthenticatorAttestationResponse, yang mewakili respons penyedia kunci sandi terhadap petunjuk klien untuk membuat kunci sandi. Daftar ini berisi informasi tentang kredensial baru yang Anda perlukan sebagai RP untuk mengautentikasi pengguna nanti. Pelajari lebih lanjut AuthenticatorAttestationResponse di Lampiran: AuthenticatorAttestationResponse.

Kirim objek PublicKeyCredential ke server. Setelah Anda menerimanya, lakukan verifikasi.

Serahkan langkah verifikasi ini ke library sisi server FIDO. Ia biasanya akan menawarkan fungsi utilitas untuk tujuan ini. Penawaran SimpleWebAuthn, misalnya, verifyRegistrationResponse. Pelajari informasi di balik layar pada Lampiran: verifikasi respons pendaftaran.

Setelah verifikasi berhasil, simpan informasi kredensial di database Anda agar pengguna nantinya dapat melakukan autentikasi dengan kunci sandi yang terkait dengan kredensial tersebut.

Gunakan tabel khusus untuk kredensial kunci publik yang terkait dengan kunci sandi. Pengguna hanya dapat memiliki satu sandi, tetapi dapat memiliki beberapa kunci sandi. Misalnya, kunci sandi yang disinkronkan melalui Rantai Kunci iCloud Apple dan satu sandi melalui Pengelola Sandi Google.

Berikut adalah contoh skema yang dapat Anda gunakan untuk menyimpan informasi kredensial:

Skema database untuk kunci sandi

  • Tabel Pengguna:
    • user_id: ID pengguna utama. ID acak, unik, dan permanen untuk pengguna. Gunakan ini sebagai kunci utama untuk tabel Users.
    • username Nama pengguna yang ditetapkan pengguna, kemungkinan dapat diedit.
    • passkey_user_id: ID pengguna bebas PII khusus kunci sandi, yang diwakili oleh user.id dalam opsi pendaftaran Anda. Saat pengguna mencoba melakukan autentikasi di lain waktu, pengautentikasi akan menyediakan passkey_user_id ini dalam respons autentikasinya di userHandle. Sebaiknya jangan tetapkan passkey_user_id sebagai kunci utama. {i>Primary key<i} cenderung menjadi PII yang sebenarnya di dalam sistem, karena digunakan secara ekstensif.
  • Tabel Public key credentials:
    • id: ID Kredensial. Gunakan ini sebagai kunci utama untuk tabel Public key credentials Anda.
    • public_key: Kunci publik kredensial.
    • passkey_user_id: Gunakan ini sebagai kunci asing untuk membuat link dengan tabel Pengguna.
    • backed_up: Kunci sandi dicadangkan jika disinkronkan oleh penyedia kunci sandi. Menyimpan status cadangan berguna jika Anda ingin mempertimbangkan untuk mengganti sandi pada masa mendatang bagi pengguna yang memiliki kunci sandi backed_up. Anda dapat memeriksa apakah kunci sandi dicadangkan dengan memeriksa tanda di authenticatorData, atau dengan menggunakan fitur library sisi server FIDO yang biasanya tersedia untuk memberi Anda akses mudah ke informasi ini. Menyimpan kelayakan pencadangan dapat berguna untuk menjawab pertanyaan calon pengguna.
    • name: Opsional, nama tampilan untuk kredensial agar pengguna dapat memberikan nama khusus kredensial.
    • transports: Array transport. Menyimpan transport berguna untuk pengalaman pengguna dalam autentikasi. Saat transportasi tersedia, browser dapat berperilaku sesuai dan menampilkan UI yang cocok dengan transpor yang digunakan penyedia kunci sandi untuk berkomunikasi dengan klien—khususnya untuk kasus penggunaan autentikasi ulang saat allowCredentials tidak kosong.

Informasi lainnya dapat berguna untuk disimpan untuk tujuan pengalaman pengguna, termasuk item seperti penyedia kunci sandi, waktu pembuatan kredensial, dan waktu terakhir digunakan. Baca selengkapnya di Desain antarmuka pengguna kunci sandi.

Kode contoh: menyimpan kredensial

Kami menggunakan library SimpleWebAuthn dalam contoh kami. Di sini, kami menyerahkan verifikasi respons pendaftaran ke fungsi verifyRegistrationResponse.

import { isoBase64URL } from '@simplewebauthn/server/helpers';


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // 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 {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

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

    const { credentialPublicKey, credentialID } = registrationInfo;

    // Existing, signed-in user
    const { user } = res.locals;
    
    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      publicKey: base64PublicKey,
      // Optional: set the platform as a default name for the credential
      // (example: "Pixel 7")
      name: req.useragent.platform, 
      transports: response.response.transports,
      passkey_user_id: user.passkey_user_id,
      backed_up: registrationInfo.credentialBackedUp
    });

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

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

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

Lampiran: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse berisi dua objek penting:

  • response.clientDataJSON adalah versi JSON data klien, yang di web merupakan data seperti yang terlihat oleh browser. Class ini berisi asal RP, tantangan, dan androidPackageName jika klien adalah aplikasi Android. Sebagai RP, Anda dapat membaca clientDataJSON untuk mengakses informasi yang dilihat browser pada saat permintaan create.
  • response.attestationObjectberisi dua informasi:
    • attestationStatement yang tidak relevan kecuali jika Anda menggunakan pengesahan.
    • authenticatorData adalah data seperti yang dilihat oleh penyedia kunci sandi. Sebagai RP, membaca authenticatorData memberi Anda akses ke data yang dilihat oleh penyedia kunci sandi dan ditampilkan pada saat permintaan create.

authenticatorDataberisi informasi penting tentang kredensial kunci publik yang terkait dengan kunci sandi yang baru dibuat:

  • Kredensial kunci publik itu sendiri, dan ID kredensial unik untuknya.
  • ID RP yang terkait dengan kredensial.
  • Tanda yang mendeskripsikan status pengguna saat kunci sandi dibuat: apakah pengguna benar-benar ada, dan apakah pengguna berhasil diverifikasi (lihat userVerification).
  • AAGUID, yang mengidentifikasi penyedia kunci sandi. Menampilkan penyedia kunci sandi dapat berguna bagi pengguna Anda, terutama jika mereka memiliki kunci sandi yang terdaftar untuk layanan Anda di beberapa penyedia kunci sandi.

Meskipun authenticatorData disusun bertingkat dalam attestationObject, informasi yang terdapat di dalamnya diperlukan untuk penerapan kunci sandi Anda, baik Anda menggunakan pengesahan atau tidak. authenticatorData dienkode, dan berisi kolom yang dienkode dalam format biner. Library sisi server Anda biasanya akan menangani penguraian dan decoding. Jika Anda tidak menggunakan library sisi server, pertimbangkan untuk memanfaatkan sisi klien getAuthenticatorData() guna menghemat beberapa penguraian dan decoding sisi server pekerjaan.

Lampiran: verifikasi respons pendaftaran

Di balik layar, verifikasi respons pendaftaran terdiri dari pemeriksaan berikut:

  • Pastikan ID RP cocok dengan situs Anda.
  • Pastikan asal permintaan adalah asal yang diharapkan untuk situs Anda (URL situs utama, aplikasi Android).
  • Jika Anda mewajibkan verifikasi pengguna, pastikan tanda verifikasi pengguna authenticatorData.uv adalah true. Pastikan tanda kehadiran pengguna authenticatorData.up adalah true, karena kehadiran pengguna selalu diperlukan untuk kunci sandi.
  • Periksa apakah klien mampu memberikan tantangan yang Anda berikan. Jika Anda tidak menggunakan pengesahan, pemeriksaan ini tidak penting. Namun, menerapkan pemeriksaan ini adalah praktik terbaik: hal ini memastikan kode Anda siap jika Anda memutuskan untuk menggunakan pengesahan di masa mendatang.
  • Pastikan ID kredensial belum terdaftar untuk pengguna mana pun.
  • Pastikan algoritma yang digunakan oleh penyedia kunci sandi untuk membuat kredensial adalah algoritma yang Anda cantumkan (di setiap kolom alg dari publicKeyCredentialCreationOptions.pubKeyCredParams, yang biasanya ditentukan dalam library sisi server Anda dan tidak dapat dilihat dari Anda). Ini memastikan bahwa pengguna hanya dapat mendaftar dengan algoritma yang telah Anda pilih untuk diizinkan.

Untuk mempelajari lebih lanjut, periksa kode sumber SimpleWebAuthn untuk verifyRegistrationResponse atau pelajari daftar lengkap verifikasi di spesifikasi.

Berikutnya

Autentikasi kunci sandi sisi server