---
url: 'https://www.corbado.com/id/blog/passkey-tutorial-cara-implementasi-passkey'
title: 'Tutorial Passkey: Cara Mengimplementasikan Passkey di Aplikasi Web'
description: 'Tutorial ini menjelaskan cara mengimplementasikan passkey di aplikasi web Anda. Kami menggunakan Node.js (TypeScript), SimpleWebAuthn, Vanilla HTML / JavaScript, dan MySQL.'
lang: 'id'
author: 'Vincent Delitz'
date: '2025-06-17T16:15:49.683Z'
lastModified: '2026-03-25T10:06:32.436Z'
keywords: 'tutorial passkey'
category: 'Passkeys Implementation'
---

# Tutorial Passkey: Cara Mengimplementasikan Passkey di Aplikasi Web

## 1. Pendahuluan: Cara Mengimplementasikan Passkey

Dalam [tutorial](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql) ini, kami membantu Anda dalam
upaya implementasi passkey, dengan menawarkan panduan langkah demi langkah tentang cara
menambahkan passkey ke situs web Anda.

Memiliki [autentikasi](https://www.corbado.com/id/blog/cara-menjadi-sepenuhnya-tanpa-password) yang modern, kuat,
dan ramah pengguna adalah kunci ketika Anda ingin membangun situs web atau aplikasi yang
hebat. Passkey telah muncul sebagai jawaban atas tantangan ini. Berfungsi sebagai standar
baru untuk login, passkey menjanjikan masa depan tanpa kerugian dari kata sandi
tradisional, menyediakan pengalaman login yang benar-benar
[tanpa kata sandi](https://www.corbado.com/id/blog/cara-mengaktifkan-passkey-android) (yang tidak hanya aman
tetapi juga sangat nyaman).

Yang benar-benar mengekspresikan potensi passkey adalah dukungan yang telah mereka
peroleh. Setiap peramban signifikan baik itu Chrome, Firefox, Safari, atau Edge dan semua
produsen perangkat penting (Apple, Microsoft, Google) telah memasukkan dukungan.
Penerimaan serentak ini menunjukkan bahwa passkey adalah standar baru untuk login.

Ya, sudah ada [tutorial](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql) tentang
mengintegrasikan passkey ke dalam
[aplikasi web](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql). Baik itu untuk kerangka kerja
frontend seperti [React](https://www.corbado.com/blog/react-passkeys), [Vue.js](https://www.corbado.com/blog/vuejs-passkeys), atau
[Next.js](https://www.corbado.com/blog/nextjs-passkeys), ada banyak panduan yang dirancang untuk mengurangi
tantangan dan mempercepat implementasi passkey Anda. Namun, **tutorial end-to-end** yang
tetap **minimalis dan bare-metal** masih kurang. Banyak pengembang telah mendekati kami
dan meminta [tutorial](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql) yang memberikan
kejelasan tentang **implementasi passkey untuk aplikasi web**.

Inilah tepatnya mengapa kami membuat panduan ini. Tujuan kami? Untuk membuat pengaturan
minimal yang layak untuk passkey, mencakup **lapisan frontend, backend, dan database**
(yang terakhir sering diabaikan meskipun dapat menyebabkan beberapa sakit kepala serius).

Di akhir perjalanan ini, Anda akan telah membangun
[aplikasi web](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql) minimal yang layak, di mana
Anda dapat:

- Membuat passkey
- Menggunakan passkey untuk login

Bagi mereka yang terburu-buru atau menginginkan referensi, seluruh basis kode tersedia di
[GitHub](https://github.com/corbado/passkey-tutorial).

Penasaran bagaimana hasil akhirnya? Berikut adalah cuplikan dari proyek akhir (kami akui
terlihat sangat dasar tetapi hal-hal menarik ada di bawah permukaan):

![Layar Login Tutorial Passkey](https://www.corbado.com/website-assets/6572cd2ed5d547903c8ad74d_passkey_tutorial_register_login_screen_f55a0f5ae9.png)

Kami sepenuhnya sadar bahwa bagian dari kode dan proyek dapat dilakukan secara berbeda
atau lebih canggih tetapi kami ingin fokus pada hal-hal penting. Itulah mengapa kami
sengaja menjaga hal-hal tetap sederhana dan berpusat pada passkey.

**Bagaimana cara menambahkan passkey ke situs web produksi saya?**

Ini adalah contoh yang sangat minimal untuk
[autentikasi passkey](https://www.corbado.com/id/blog/penyedia-passkey-aaguid-adopsi). Hal-hal berikut **TIDAK**
dipertimbangkan / diimplementasikan dalam tutorial ini atau hanya sangat dasar:

- [Conditional UI](https://www.corbado.com/glossary/conditional-ui) /
  [Conditional Mediation](https://www.corbado.com/blog/webauthn-conditional-ui-passkeys-autofill) / pengisian
  otomatis passkey
- Manajemen perangkat
- Manajemen sesi
- Menambahkan beberapa perangkat secara aman ke akun
- Kompatibilitas mundur
- Dukungan lintas platform dan lintas perangkat yang tepat
- [Autentikasi](https://www.corbado.com/id/blog/cara-menjadi-sepenuhnya-tanpa-password) cadangan
- Penanganan kesalahan yang tepat
- Halaman [manajemen passkey](https://www.corbado.com/id/blog/penyedia-passkey-aaguid-adopsi)

Mendapatkan dukungan penuh untuk semua fitur ini membutuhkan upaya pengembangan yang jauh
lebih besar. Bagi mereka yang tertarik, kami merekomendasikan untuk melihat artikel
kesalahpahaman pengembang passkey ini.

## 2. Prasyarat untuk Mengintegrasikan Passkey

Sebelum menyelam lebih dalam ke implementasi passkey, mari kita lihat keterampilan dan
alat yang diperlukan. Inilah yang Anda butuhkan untuk memulai:

### 2.1 Frontend: Vanilla HTML & JavaScript

Pemahaman yang kuat tentang blok bangunan web HTML, CSS, dan
[JavaScript](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql) sangat penting. Kami sengaja
menjaga hal-hal tetap sederhana, menahan diri dari kerangka kerja
[JavaScript](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql) modern apa pun dan mengandalkan
Vanilla [JavaScript](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql) / HTML. Satu-satunya hal
yang lebih canggih yang kami gunakan adalah pustaka pembungkus WebAuthn
[@simplewebauthn/browser](https://simplewebauthn.dev/docs/packages/browser/).

### 2.2 Backend: Node.js (Express) dalam TypeScript + SimpleWebAuthn

Untuk backend kami, kami menggunakan server [Node.js](https://www.corbado.com/blog/nodejs-passkeys) (Express)
yang ditulis dalam TypeScript. Kami juga telah memutuskan untuk bekerja dengan
implementasi server WebAuthn dari SimpleWebAuthn (`@simplewebauthn/server` bersama dengan
`@simplewebauthn/typescript-types`). Ada banyak implementasi server WebAuthn yang
tersedia, jadi Anda tentu saja juga dapat menggunakan salah satunya. Karena kami telah
memutuskan untuk server WebAuthn TypeScript, pengetahuan dasar
[Node.js](https://www.corbado.com/blog/nodejs-passkeys) dan npm diperlukan.

### 2.3 Database: MySQL

Semua data pengguna dan kunci publik dari passkey disimpan dalam database. Kami telah
memilih [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) sebagai teknologi database.
Pemahaman dasar tentang [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) dan database
relasional bermanfaat, meskipun kami akan memandu Anda melalui langkah-langkah tunggal.

Berikut ini, kami sering menggunakan istilah WebAuthn dan passkey secara bergantian
meskipun secara resmi mungkin tidak berarti sama. Untuk pemahaman yang lebih baik,
terutama di bagian kode, kami membuat asumsi ini.

Dengan prasyarat ini, Anda siap untuk menyelami dunia passkey.

## 3. Tinjauan Arsitektur: Contoh Implementasi Passkey

Sebelum masuk ke kode dan konfigurasi, mari kita lihat arsitektur sistem yang ingin kita
bangun. Berikut adalah rincian arsitektur yang akan kita siapkan:

- **Frontend:** Terdiri dari dua tombol, satu untuk pendaftaran pengguna (membuat passkey)
  dan yang lainnya untuk [autentikasi](https://www.corbado.com/id/blog/cara-menjadi-sepenuhnya-tanpa-password)
  (login menggunakan passkey).
- **Perangkat & Peramban:** Setelah tindakan dipicu di frontend, perangkat dan peramban
  ikut berperan. Mereka memfasilitasi pembuatan dan verifikasi passkey, bertindak sebagai
  perantara antara pengguna dan backend.
- **Backend:** Backend adalah tempat keajaiban sesungguhnya terjadi dalam aplikasi kita.
  Ini menangani semua permintaan yang dimulai oleh frontend. Proses ini melibatkan
  pembuatan dan verifikasi passkey. Inti dari operasi backend adalah server WebAuthn.
  Berlawanan dengan apa yang mungkin disarankan oleh namanya, ini bukan server mandiri.
  Sebaliknya, ini adalah pustaka atau paket yang mengimplementasikan standar WebAuthn. Dua
  fungsi utamanya adalah: **Pendaftaran (Sign-up) di mana pengguna baru membuat passkey
  mereka dan Autentikasi (Login): Di mana pengguna yang ada login menggunakan passkey
  mereka.** Dalam bentuk paling sederhana, server WebAuthn menyediakan empat titik akhir
  API publik, dibagi menjadi dua kategori: dua untuk pendaftaran dan dua untuk
  autentikasi. Mereka dirancang untuk menerima data dalam format tertentu, yang kemudian
  diproses oleh server WebAuthn. Server WebAuthn bertanggung jawab atas semua operasi
  kriptografi yang diperlukan. Aspek penting yang perlu diperhatikan adalah bahwa titik
  akhir API ini harus disajikan melalui HTTPS.
- **Database MySQL:** Bertindak sebagai tulang punggung penyimpanan kami, database
  [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) bertanggung jawab untuk menyimpan data
  pengguna dan kredensial mereka yang sesuai.

Dengan tinjauan arsitektur ini, Anda seharusnya memiliki peta konseptual tentang bagaimana
komponen aplikasi kita. Seiring kita melanjutkan, kita akan menyelam lebih dalam ke setiap
komponen ini, merinci pengaturan, konfigurasi, dan interaksinya.

Bagan berikut menjelaskan alur proses selama pendaftaran (sign-up):

![Bagan Proses Pendaftaran Passkey](https://www.corbado.com/website-assets/6572cd4d3243003bb3589e88_passkey_sign_up_process_chart_b73d643b4c.png)

Bagan berikut menjelaskan alur proses selama autentikasi (login):

![Bagan Proses Login Passkey](https://www.corbado.com/website-assets/6572cd5f04fd73a7ff5d501b_passkey_login_process_chart_a44262b767.png)

Selain itu, Anda dapat menemukan struktur proyek di sini (hanya file yang paling penting):

```
passkeys-tutorial
├── src                         # Berisi semua kode sumber TypeScript backend
│   ├── controllers             # Logika bisnis untuk menangani jenis permintaan tertentu
│   │   ├── authentication.ts   # Logika autentikasi passkey
│   │   └── registration.ts     # Logika pendaftaran passkey
│   ├── middleware
│   │   ├── customError.ts      # Tambahkan pesan kesalahan kustom secara terstandarisasi
│   │   └── errorHandler.ts     # Penangan kesalahan umum
│   ├── public
│   │   ├── index.html          # File HTML utama di frontend
│   │   ├── css
│   │   │   └── style.css       # Gaya dasar
│   │   └── js
│   │       └── script.js       # Logika JavaScript (termasuk API WebAuthn)
│   ├── routes                  # Definisi rute API dan penangannya
│   │   └── routes.ts           # Rute passkey spesifik
│   ├── services
│   │   ├── credentialService.ts# Berinteraksi dengan tabel kredensial
│   │   └── userService.ts      # Berinteraksi dengan tabel pengguna
│   ├── utils                   # Fungsi pembantu dan utilitas
│   |   ├── constants.ts        # Beberapa konstanta (misalnya rpID)
│   |   └── utils.ts            # Fungsi pembantu
│   ├── database.ts             # Membuat koneksi dari Node.js ke database MySQL
│   ├── index.ts                # Titik masuk dari server Node.js
│   └── server.ts               # Mengelola semua pengaturan server
├── config.json                 # Beberapa konfigurasi untuk proyek Node.js
├── docker-compose.yml          # Mendefinisikan layanan, jaringan, dan volume untuk kontainer Docker
├── Dockerfile                  # Membuat gambar Docker dari proyek
├── init-db.sql                 # Mendefinisikan skema database MySQL kami
├── package.json                # Mengelola dependensi dan skrip proyek Node.js
└── tsconfig.json               # Mengonfigurasi bagaimana TypeScript mengkompilasi kode Anda
```

## 4. Pengaturan Database MySQL

Saat mengimplementasikan passkey, pengaturan database adalah komponen kunci. Pendekatan
kami menggunakan kontainer Docker yang menjalankan MySQL, menawarkan lingkungan yang
sederhana dan terisolasi yang penting untuk pengujian dan penerapan yang andal.

Skema database kami sengaja dibuat minimalis, hanya menampilkan dua tabel. Kesederhanaan
ini membantu dalam pemahaman yang lebih jelas dan pemeliharaan yang lebih mudah.

**Struktur Tabel Rinci**

**1. Tabel Kredensial:** Pusat dari
[autentikasi passkey](https://www.corbado.com/id/blog/penyedia-passkey-aaguid-adopsi), tabel ini menyimpan
kredensial passkey. Kolom Kritis:

- **credential_id:** Pengidentifikasi unik untuk setiap kredensial. Memilih tipe data yang
  benar untuk bidang ini sangat penting untuk menghindari kesalahan format.
- **public_key:** Menyimpan kunci publik untuk setiap kredensial. Seperti halnya
  `credential_id`, tipe data dan format yang sesuai sangat penting.

**2. Tabel Pengguna:** Menghubungkan akun pengguna dengan kredensial mereka yang sesuai.

Perhatikan bahwa kami menamai tabel pertama `credentials` karena ini sesuai dengan
pengalaman kami dan apa yang direkomendasikan oleh pustaka lain lebih cocok (berlawanan
dengan saran SimpleWebAuthn untuk menamainya `authenticator` atau `authenticator_device`).

Jenis data untuk `credential_id` dan `public_key` sangat penting. Kesalahan sering muncul
dari tipe data, pengkodean, atau format yang salah (terutama perbedaan antara Base64 dan
Base64URL adalah penyebab umum kesalahan), yang dapat mengganggu seluruh proses
pendaftaran (sign-up) atau autentikasi (login).

Semua perintah SQL yang diperlukan untuk menyiapkan tabel-tabel ini terdapat dalam file
`init-db.sql`. Skrip ini memastikan inisialisasi database yang cepat dan bebas kesalahan.

Untuk kasus yang lebih canggih, Anda dapat menambahkan `credential_device_type` atau
`credential_backed_up` untuk menyimpan lebih banyak informasi tentang kredensial dan
meningkatkan pengalaman pengguna. Namun, kami tidak melakukannya dalam tutorial ini.

```sql filename="init-db.sql"
CREATE TABLE users
(
    id       VARCHAR(255) PRIMARY KEY,
    username VARCHAR(255) NOT NULL UNIQUE
);

CREATE TABLE credentials
(
    id            INT AUTO_INCREMENT PRIMARY KEY,
    user_id       VARCHAR(255) NOT NULL,
    credential_id VARCHAR(255) NOT NULL,
    public_key    TEXT         NOT NULL,
    counter       INT          NOT NULL,
    transports    VARCHAR(255),
    FOREIGN KEY (user_id) REFERENCES users (id)
);
```

Setelah kita membuat file ini, kita membuat file `docker-compose.yml` baru di tingkat root
proyek:

```yaml filename="docker-compose.yml"
version: "3.1"

services:
    db:
        image: mysql
        command: --default-authentication-plugin=mysql_native_password
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: my-secret-pw
            MYSQL_DATABASE: webauthn_db
        ports:
            - "3306:3306"
        volumes:
            - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
```

File ini memulai database MySQL pada port 3306 dan membuat struktur database yang
ditentukan. Penting untuk dicatat bahwa nama dan kata sandi untuk database yang digunakan
di sini dibuat sederhana untuk tujuan demonstrasi. Di lingkungan produksi, Anda harus
menggunakan kredensial yang lebih kompleks untuk
[keamanan](https://www.corbado.com/id/blog/cara-mengaktifkan-passkey-android) yang ditingkatkan.

Selanjutnya, kita beralih ke menjalankan kontainer Docker kita. Pada titik ini, file
`docker-compose.yml` kita hanya menyertakan kontainer tunggal ini, tetapi kita akan
menambahkan lebih banyak komponen nanti. Untuk memulai kontainer, gunakan perintah
berikut:

```bash
docker compose up -d
```

Setelah kontainer berjalan, kita perlu memverifikasi apakah database berfungsi seperti
yang diharapkan. Buka terminal dan jalankan perintah berikut untuk berinteraksi dengan
database MySQL:

```bash
docker exec -it <container ID> mysql -uroot -p
```

Anda akan diminta untuk memasukkan kata sandi root, yaitu `my-secret-pw` dalam contoh
kami. Setelah login, pilih database `webauthn_db` dan tampilkan tabel menggunakan perintah
ini:

```sql
use
webauthn_db;
show
tables;
```

Pada tahap ini, Anda seharusnya melihat dua tabel yang didefinisikan dalam skrip kami.
Awalnya, tabel-tabel ini akan kosong, menunjukkan bahwa pengaturan database kami selesai
dan siap untuk langkah-langkah selanjutnya dalam mengimplementasikan passkey.

## 5. Mengimplementasikan Passkey: Langkah-langkah Integrasi Backend

Backend adalah inti dari setiap aplikasi passkey, bertindak sebagai pusat untuk memproses
permintaan autentikasi pengguna dari frontend. Ini berkomunikasi dengan pustaka server
WebAuthn untuk menangani permintaan pendaftaran (sign-up) dan autentikasi (login), dan
berinteraksi dengan database MySQL Anda untuk menyimpan dan mengambil kredensial pengguna.
Di bawah ini, kami akan memandu Anda melalui pengaturan backend Anda menggunakan
[Node.js](https://www.corbado.com/blog/nodejs-passkeys) (Express) dengan TypeScript yang akan mengekspos API
publik untuk menangani semua permintaan.

### 5.1 Inisialisasi Server Node.js (Express)

Pertama, buat direktori baru untuk proyek Anda dan navigasikan ke dalamnya menggunakan
terminal atau command prompt Anda.

Jalankan perintah

```bash
npx create-express-typescript-application passkeys-tutorial
```

Ini membuat kerangka kode dasar dari aplikasi Node.js (Express) yang ditulis dalam
TypeScript yang dapat kita gunakan untuk adaptasi lebih lanjut.

Proyek Anda memerlukan beberapa paket kunci yang perlu kita instal di atasnya:

- **@simplewebauthn/server:** Pustaka sisi server untuk memfasilitasi operasi WebAuthn,
  seperti pendaftaran pengguna (sign-up) dan autentikasi (login).
- **express-session:** Middleware untuk [Express.js](https://www.corbado.com/blog/nodejs-passkeys) untuk
  mengelola sesi, menyimpan data sesi sisi server dan menangani cookie.
- **uuid:** Utilitas untuk menghasilkan pengidentifikasi unik universal (UUID), yang biasa
  digunakan untuk membuat kunci atau pengidentifikasi unik dalam aplikasi.
- **mysql2:** Klien Node.js untuk MySQL, menyediakan kemampuan untuk terhubung dan
  menjalankan kueri terhadap database MySQL.

Pindah ke direktori baru dan instal dengan perintah berikut (kami juga menginstal tipe
TypeScript yang diperlukan):

```bash
cd passkeys-tutorial
npm install @simplewebauthn/server mysql2 uuid express-session
@types/express-session @types/uuid
```

Untuk mengonfirmasi bahwa semuanya terinstal dengan benar, jalankan

```bash
npm run dev:nodemon
```

Ini akan memulai server Node.js Anda dalam mode pengembangan dengan Nodemon, yang secara
otomatis me-restart server setiap kali ada perubahan file.

![Mulai Aplikasi Tutorial Passkey](https://www.corbado.com/website-assets/6572cd86e1fa2982352c7ca1_passkey_tutorial_start_app_b442392b4d.png)

**Tips pemecahan masalah:** Jika Anda mengalami kesalahan, coba perbarui `ts-node` ke
versi 10.8.1 di file `package.json` lalu jalankan `npm i` untuk menginstal pembaruan.

File `server.ts` Anda memiliki pengaturan dasar dan middleware untuk aplikasi Express.
Untuk mengintegrasikan fungsionalitas passkey, Anda perlu menambahkan:

- **Rute:** Tentukan rute baru untuk
  [pendaftaran passkey](https://www.corbado.com/id/blog/praktik-terbaik-pembuatan-passkey) (sign-up) dan
  autentikasi (login).
- **Controller:** Buat controller untuk menangani logika untuk rute-rute ini.
- **Middleware:** Integrasikan middleware untuk penanganan permintaan dan kesalahan.
- **Layanan:** Bangun layanan untuk mengambil dan menyimpan data di database.
- **Fungsi Utilitas:** Sertakan fungsi utilitas untuk operasi kode yang efisien.

Peningkatan ini adalah kunci untuk mengaktifkan
[autentikasi passkey](https://www.corbado.com/id/blog/penyedia-passkey-aaguid-adopsi) di backend aplikasi Anda.
Kita akan mengaturnya nanti.

### 5.2 Koneksi Database MySQL

Setelah kita membuat dan memulai database di [bagian 4](#4-pengaturan-database-mysql),
sekarang kita perlu memastikan bahwa backend kita dapat terhubung ke database MySQL. Oleh
karena itu, kita membuat file `database.ts` baru di folder `/src` dan menambahkan konten
berikut:

```ts filename="database.ts"
import mysql from "mysql2";

// Create a MySQL pool
const pool = mysql.createPool({
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    waitForConnections: true,
    connectionLimit: 10,
    queueLimit: 0,
});

// Promisify for Node.js async/await.
export const promisePool = pool.promise();
```

File ini nantinya akan digunakan oleh server kita untuk mengakses database.

### 5.3 Konfigurasi Server Aplikasi

Mari kita lihat sekilas `config.json` kita, di mana dua variabel sudah didefinisikan: port
tempat kita menjalankan aplikasi dan lingkungannya:

```json filename="config.json"
{
    "PORT": 8080,
    "NODE_ENV": "development"
}
```

`package.json` dapat dibiarkan apa adanya dan seharusnya terlihat seperti:

```json filename="package.json"
{
    "name": "passkeys-tutorial",
    "version": "0.0.1",
    "description": "passkeys-tutorial initialised with create-express-typescript-application.",
    "main": "src/index.ts",
    "scripts": {
        "build": "tsc",
        "start": "node ./build/src/index.js",
        "dev": "ts-node ./src/index.ts",
        "dev:nodemon": "nodemon -w src -e ts,json -x ts-node ./src/index.ts",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": ["express", "typescript"],
    "devDependencies": {
        "@types/cors": "^2.8.17",
        "@types/express": "^4.17.21",
        "@types/morgan": "^1.9.9",
        "@types/node": "^14.18.63",
        "@typescript-eslint/eslint-plugin": "^4.33.0",
        "@typescript-eslint/parser": "^4.33.0",
        "eslint": "^7.32.0",
        "nodemon": "^2.0.22",
        "ts-node": "^10.8.1",
        "typescript": "^4.9.5"
    },
    "dependencies": {
        "@simplewebauthn/server": "^8.3.5",
        "@types/express-session": "^1.17.10",
        "@types/uuid": "^9.0.7",
        "cors": "^2.8.5",
        "env-cmd": "^10.1.0",
        "express": "^4.18.2",
        "express-session": "^1.17.3",
        "fs": "^0.0.1-security",
        "helmet": "^4.6.0",
        "morgan": "^1.10.0",
        "mysql2": "^3.6.5",
        "uuid": "^9.0.1"
    }
}
```

`index.ts` terlihat seperti:

```ts filename="index.ts"
import app from "./server";
import config from "../config.json";

// Start the application by listening to specific port
const port = Number(process.env.PORT || config.PORT || 8080);
app.listen(port, () => {
    console.info("Express application started on port: " + port);
});
```

Di `server.ts`, kita perlu menyesuaikan beberapa hal lagi. Selain itu, cache sementara
dari beberapa jenis (misalnya redis, memcache atau express-session) diperlukan untuk
menyimpan tantangan sementara yang dapat digunakan pengguna untuk autentikasi. Kami
memutuskan untuk menggunakan `express-session` dan mendeklarasikan modul `express-session`
di atas agar berfungsi dengan `express-session`. Selain itu, kami merampingkan perutean
dan menghapus penanganan kesalahan untuk saat ini (ini akan ditambahkan ke middleware
nanti):

```ts filename="server.ts"
import express, { Express } from "express";
import morgan from "morgan";
import helmet from "helmet";
import cors from "cors";
import config from "../config.json";
import { router as passkeyRoutes } from "./routes/routes";
import session from "express-session";

const app: Express = express();

declare module "express-session" {
    interface SessionData {
        currentChallenge?: string;
        loggedInUserId?: string;
    }
}

/************************************************************************************
 *                              Basic Express Middlewares
 ***********************************************************************************/
app.set("json spaces", 4);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(
    session({
        // @ts-ignore
        secret: process.env.SESSION_SECRET,
        saveUninitialized: true,
        resave: false,
        cookie: {
            maxAge: 86400000,
            httpOnly: true, // Ensure to not expose session cookies to clientside scripts
        },
    }),
);

// Handle logs in console during development
if (process.env.NODE_ENV === "development" || config.NODE_ENV === "development") {
    app.use(morgan("dev"));
    app.use(cors());
}

// Handle security and origin in production
if (process.env.NODE_ENV === "production" || config.NODE_ENV === "production") {
    app.use(helmet());
}

/************************************************************************************
 *                               Register all routes
 ***********************************************************************************/
app.use("/api/passkey", passkeyRoutes);

app.use(express.static("src/public"));

export default app;
```

### 5.4 Layanan Kredensial & Layanan Pengguna

Untuk mengelola data secara efektif di dua tabel yang telah kita buat, kita akan
mengembangkan dua layanan berbeda di direktori `src/services` baru:
`authenticatorService.ts` dan `userService.ts`.

Setiap layanan akan mengenkapsulasi metode
[CRUD](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql) (Create, Read, Update, Delete),
memungkinkan kita untuk berinteraksi dengan database secara modular dan terorganisir.
Layanan-layanan ini akan memfasilitasi penyimpanan, pengambilan, dan pembaruan data di
tabel [authenticator](https://www.corbado.com/glossary/authenticator) dan pengguna. Berikut adalah bagaimana
struktur file-file yang diperlukan ini harus ditata:

`userService.ts` terlihat seperti ini:

```ts filename="userService.ts"
import { promisePool } from "../database"; // Adjust the import path as necessary
import { v4 as uuidv4 } from "uuid";

export const userService = {
    async getUserById(userId: string) {
        const [rows] = await promisePool.query("SELECT * FROM users WHERE id = ?", [
            userId,
        ]);
        // @ts-ignore
        return rows[0];
    },

    async getUserByUsername(username: string) {
        try {
            const [rows] = await promisePool.query(
                "SELECT * FROM users WHERE username = ?",
                [username],
            );
            // @ts-ignore
            return rows[0];
        } catch (error) {
            return null;
        }
    },

    async createUser(username: string) {
        const id = uuidv4();
        await promisePool.query("INSERT INTO users (id, username) VALUES (?, ?)", [
            id,
            username,
        ]);
        return { id, username };
    },
};
```

`credentialService.ts` terlihat sebagai berikut:

```ts filename="credentialService.ts"
import { promisePool } from "../database";
import type { AuthenticatorDevice } from "@simplewebauthn/typescript-types";

export const credentialService = {
    async saveNewCredential(
        userId: string,
        credentialId: string,
        publicKey: string,
        counter: number,
        transports: string,
    ) {
        try {
            await promisePool.query(
                "INSERT INTO credentials (user_id, credential_id, public_key, counter, transports) VALUES (?, ?, ?, ?, ?)",
                [userId, credentialId, publicKey, counter, transports],
            );
        } catch (error) {
            console.error("Error saving new credential:", error);
            throw error;
        }
    },

    async getCredentialByCredentialId(
        credentialId: string,
    ): Promise<AuthenticatorDevice | null> {
        try {
            const [rows] = await promisePool.query(
                "SELECT * FROM credentials WHERE credential_id = ? LIMIT 1",
                [credentialId],
            );
            // @ts-ignore
            if (rows.length === 0) return null;
            // @ts-ignore
            const row = rows[0];
            return {
                userID: row.user_id,
                credentialID: row.credential_id,
                credentialPublicKey: row.public_key,
                counter: row.counter,
                transports: row.transports ? row.transports.split(",") : [],
            } as AuthenticatorDevice;
        } catch (error) {
            console.error("Error retrieving credential:", error);
            throw error;
        }
    },

    async updateCredentialCounter(credentialId: string, newCounter: number) {
        try {
            await promisePool.query(
                "UPDATE credentials SET counter = ? WHERE credential_id = ?",
                [newCounter, credentialId],
            );
        } catch (error) {
            console.error("Error updating credential counter:", error);
            throw error;
        }
    },
};
```

### 5.5 Middleware

Untuk menangani kesalahan secara terpusat dan juga mempermudah debugging, kami menambahkan
file `errorHandler.ts`:

```ts filename="errorHandler.ts"
import { Request, Response, NextFunction } from "express";
import { CustomError } from "./customError";

interface ErrorWithStatus extends Error {
    statusCode?: number;
}

export const handleError = (
    err: CustomError,
    req: Request,
    res: Response,
    next: NextFunction,
) => {
    const statusCode = err.statusCode || 500;
    const message = err.message || "Internal Server Error";
    console.log(message);
    res.status(statusCode).send({ error: message });
};
```

Selain itu, kami menambahkan file `customError.ts` baru karena nanti kami ingin dapat
membuat kesalahan kustom untuk membantu kami menemukan bug lebih cepat:

```ts filename="customError.ts"
export class CustomError extends Error {
    statusCode: number;

    constructor(message: string, statusCode: number = 500) {
        super(message);
        this.statusCode = statusCode;
        Object.setPrototypeOf(this, CustomError.prototype);
    }
}
```

### 5.6 Utilitas

Di folder `utils`, kami membuat dua file `constants.ts` dan `utils.ts`.

`constant.ts` menyimpan beberapa informasi dasar server WebAuthn, seperti nama relying
party, ID [relying party](https://www.corbado.com/glossary/relying-party), dan origin:

```ts filename="constant.ts"
export const rpName: string = "Passkeys Tutorial";
export const rpID: string = "localhost";
export const origin: string = `http://${rpID}:8080`;
```

`utils.ts` menyimpan dua fungsi yang nantinya kita perlukan untuk mengkodekan dan
mendekodekan data:

```ts filename="utils.ts"
export const uint8ArrayToBase64 = (uint8Array: Uint8Array): string =>
    Buffer.from(uint8Array).toString("base64");

export const base64ToUint8Array = (base64: string): Uint8Array =>
    new Uint8Array(Buffer.from(base64, "base64"));
```

### 5.7 Controller Passkey dengan SimpleWebAuthn

Sekarang, kita sampai pada inti dari backend kita: controller. Kami membuat dua
controller, satu untuk membuat passkey baru (`registration.ts`) dan satu untuk login
dengan passkey (`authentication.ts`).

`registration.ts` terlihat seperti ini:

```ts filename="registration.ts"
import {
    generateRegistrationOptions,
    verifyRegistrationResponse,
} from "@simplewebauthn/server";
import { uint8ArrayToBase64 } from "../utils/utils";
import { rpName, rpID, origin } from "../utils/constants";
import { credentialService } from "../services/credentialService";
import { userService } from "../services/userService";
import { RegistrationResponseJSON } from "@simplewebauthn/typescript-types";
import { Request, Response, NextFunction } from "express";
import { CustomError } from "../middleware/customError";

export const handleRegisterStart = async (
    req: Request,
    res: Response,
    next: NextFunction,
) => {
    const { username } = req.body;

    if (!username) {
        return next(new CustomError("Username empty", 400));
    }

    try {
        let user = await userService.getUserByUsername(username);
        if (user) {
            return next(new CustomError("User already exists", 400));
        } else {
            user = await userService.createUser(username);
        }

        const options = await generateRegistrationOptions({
            rpName,
            rpID,
            userID: user.id,
            userName: user.username,
            timeout: 60000,
            attestationType: "direct",
            excludeCredentials: [],
            authenticatorSelection: {
                residentKey: "preferred",
            },
            // Support for the two most common algorithms: ES256, and RS256
            supportedAlgorithmIDs: [-7, -257],
        });
        req.session.loggedInUserId = user.id;
        req.session.currentChallenge = options.challenge;
        res.send(options);
    } catch (error) {
        next(
            error instanceof CustomError
                ? error
                : new CustomError("Internal Server Error", 500),
        );
    }
};

export const handleRegisterFinish = async (
    req: Request,
    res: Response,
    next: NextFunction,
) => {
    const { body } = req;
    const { currentChallenge, loggedInUserId } = req.session;

    if (!loggedInUserId) {
        return next(new CustomError("User ID is missing", 400));
    }

    if (!currentChallenge) {
        return next(new CustomError("Current challenge is missing", 400));
    }

    try {
        const verification = await verifyRegistrationResponse({
            response: body as RegistrationResponseJSON,
            expectedChallenge: currentChallenge,
            expectedOrigin: origin,
            expectedRPID: rpID,
            requireUserVerification: true,
        });

        if (verification.verified && verification.registrationInfo) {
            const { credentialPublicKey, credentialID, counter } =
                verification.registrationInfo;
            await credentialService.saveNewCredential(
                loggedInUserId,
                uint8ArrayToBase64(credentialID),
                uint8ArrayToBase64(credentialPublicKey),
                counter,
                body.response.transports,
            );
            res.send({ verified: true });
        } else {
            next(new CustomError("Verification failed", 400));
        }
    } catch (error) {
        next(
            error instanceof CustomError
                ? error
                : new CustomError("Internal Server Error", 500),
        );
    } finally {
        req.session.loggedInUserId = undefined;
        req.session.currentChallenge = undefined;
    }
};
```

Mari kita tinjau fungsionalitas controller kita, yang menangani dua titik akhir kunci
dalam proses pendaftaran (sign-up) WebAuthn. Di sinilah juga letak salah satu perbedaan
terbesar dengan autentikasi berbasis kata sandi: Untuk setiap upaya pendaftaran (sign-up)
atau autentikasi (login), diperlukan dua panggilan API backend, yang memerlukan konten
frontend spesifik di antaranya. Kata sandi biasanya hanya memerlukan satu titik akhir.

**1. Titik Akhir handleRegisterStart:**

Titik akhir ini dipicu oleh frontend, menerima nama pengguna untuk membuat passkey dan
akun baru. Dalam contoh ini, kami hanya mengizinkan pembuatan akun/passkey baru jika belum
ada akun yang ada. Dalam aplikasi dunia nyata, Anda perlu menangani ini dengan cara
memberi tahu pengguna bahwa passkey sudah ada dan menambahkan dari perangkat yang sama
tidak mungkin (tetapi pengguna dapat menambahkan passkey dari perangkat yang berbeda
setelah beberapa bentuk konfirmasi). Untuk kesederhanaan, kami mengabaikan ini dalam
tutorial ini.

`PublicKeyCredentialCreationOptions` disiapkan. `residentKey` diatur ke `preferred`, dan
`attestationType` ke `direct`, mengumpulkan lebih banyak data dari
[authenticator](https://www.corbado.com/glossary/authenticator) untuk potensi penyimpanan database.

Secara umum, `PublicKeyCredentialCreationOptions` terdiri dari data berikut:

```
dictionary PublicKeyCredentialCreationOptions {
    required PublicKeyCredentialRpEntity                rp;
    required PublicKeyCredentialUserEntity              user;

    required BufferSource                               challenge;
    required sequence<PublicKeyCredentialParameters>    pubKeyCredParams;

    unsigned long                                       timeout;
    sequence<PublicKeyCredentialDescriptor>             excludeCredentials = [];
    AuthenticatorSelectionCriteria                      authenticatorSelection;
    DOMString                                           attestation = "none";
    AuthenticationExtensionsClientInputs                extensions;
};
```

- **rp:** Mewakili informasi [relying party](https://www.corbado.com/glossary/relying-party) (situs web atau
  layanan), biasanya termasuk namanya (`rp.name`) dan domain (`rp.id`).
- **user:** Berisi detail akun pengguna seperti `user.name`, `user.id`, dan
  `user.displayName`.
- **challenge:** Nilai acak yang aman yang dibuat oleh server WebAuthn untuk mencegah
  serangan replay selama proses pendaftaran.
- **pubKeyCredParams:** Menentukan jenis kredensial kunci publik yang akan dibuat,
  termasuk algoritma kriptografi yang digunakan.
- **timeout:** Opsional, mengatur waktu dalam milidetik yang dimiliki pengguna untuk
  menyelesaikan interaksi.
- **excludeCredentials:** Daftar kredensial yang akan dikecualikan; digunakan untuk
  mencegah [pendaftaran passkey](https://www.corbado.com/id/blog/praktik-terbaik-pembuatan-passkey) untuk
  perangkat/[authenticator](https://www.corbado.com/glossary/authenticator) yang sama beberapa kali.
- **authenticatorSelection:** Kriteria untuk memilih authenticator, seperti apakah harus
  mendukung verifikasi pengguna atau bagaimana [kunci residen](https://www.corbado.com/id/glossary/ctap) harus
  didorong.
- **attestation:** Menentukan preferensi penyampaian [atestasi](https://www.corbado.com/id/glossary/ctap) yang
  diinginkan, seperti "none", "indirect", atau "direct".
- **extensions:** Opsional, memungkinkan ekstensi klien tambahan.

ID Pengguna dan challenge disimpan dalam objek sesi, menyederhanakan proses untuk tujuan
tutorial. Selain itu, sesi dibersihkan setelah setiap upaya pendaftaran (sign-up) atau
autentikasi (login).

**2. Titik Akhir handleRegisterFinish:**

Titik akhir ini mengambil ID pengguna dan challenge yang ditetapkan sebelumnya. Ini
memverifikasi `RegistrationResponse` dengan challenge. Jika valid, ia menyimpan kredensial
baru untuk pengguna. Setelah disimpan di database, ID pengguna dan challenge dihapus dari
sesi.

Tips: Saat men-debug aplikasi Anda, kami sangat menyarankan untuk menggunakan Chrome
sebagai peramban dan fitur bawaannya untuk meningkatkan pengalaman pengembang aplikasi
berbasis passkey, misalnya, authenticator WebAuthn virtual dan log perangkat (lihat tips
kami untuk pengembang di bawah untuk informasi lebih lanjut)

Selanjutnya, kita beralih ke `authentication.ts`, yang memiliki struktur dan
fungsionalitas serupa.

`authentication.ts` terlihat seperti ini:

```ts filename="authentication.ts"
import { Request, Response, NextFunction } from "express";
import {
    generateAuthenticationOptions,
    verifyAuthenticationResponse,
} from "@simplewebauthn/server";
import { uint8ArrayToBase64, base64ToUint8Array } from "../utils/utils";
import { rpID, origin } from "../utils/constants";
import { credentialService } from "../services/credentialService";
import { userService } from "../services/userService";
import { AuthenticatorDevice } from "@simplewebauthn/typescript-types";
import { isoBase64URL } from "@simplewebauthn/server/helpers";
import {
    VerifiedAuthenticationResponse,
    VerifyAuthenticationResponseOpts,
} from "@simplewebauthn/server/esm";
import { CustomError } from "../middleware/customError";

export const handleLoginStart = async (
    req: Request,
    res: Response,
    next: NextFunction,
) => {
    const { username } = req.body;
    try {
        const user = await userService.getUserByUsername(username);
        if (!user) {
            return next(new CustomError("User not found", 404));
        }

        req.session.loggedInUserId = user.id;

        // allowCredentials sengaja untuk demo ini dibiarkan kosong. Ini menyebabkan semua kredensial lokal yang ada
        // ditampilkan untuk layanan alih-alih hanya yang telah didaftarkan oleh nama pengguna.
        const options = await generateAuthenticationOptions({
            timeout: 60000,
            allowCredentials: [],
            userVerification: "required",
            rpID,
        });

        req.session.currentChallenge = options.challenge;
        res.send(options);
    } catch (error) {
        next(
            error instanceof CustomError
                ? error
                : new CustomError("Internal Server Error", 500),
        );
    }
};

export const handleLoginFinish = async (
    req: Request,
    res: Response,
    next: NextFunction,
) => {
    const { body } = req;
    const { currentChallenge, loggedInUserId } = req.session;

    if (!loggedInUserId) {
        return next(new CustomError("User ID is missing", 400));
    }

    if (!currentChallenge) {
        return next(new CustomError("Current challenge is missing", 400));
    }

    try {
        const credentialID = isoBase64URL.toBase64(body.rawId);
        const bodyCredIDBuffer = isoBase64URL.toBuffer(body.rawId);
        const dbCredential: AuthenticatorDevice | null =
            await credentialService.getCredentialByCredentialId(credentialID);
        if (!dbCredential) {
            return next(new CustomError("Credential not registered with this site", 404));
        }

        // @ts-ignore
        const user = await userService.getUserById(dbCredential.userID);
        if (!user) {
            return next(new CustomError("User not found", 404));
        }

        // @ts-ignore
        dbCredential.credentialID = base64ToUint8Array(dbCredential.credentialID);
        // @ts-ignore
        dbCredential.credentialPublicKey = base64ToUint8Array(
            dbCredential.credentialPublicKey,
        );

        let verification: VerifiedAuthenticationResponse;
        const opts: VerifyAuthenticationResponseOpts = {
            response: body,
            expectedChallenge: currentChallenge,
            expectedOrigin: origin,
            expectedRPID: rpID,
            authenticator: dbCredential,
        };
        verification = await verifyAuthenticationResponse(opts);
        const { verified, authenticationInfo } = verification;

        if (verified) {
            await credentialService.updateCredentialCounter(
                uint8ArrayToBase64(bodyCredIDBuffer),
                authenticationInfo.newCounter,
            );
            res.send({ verified: true });
        } else {
            next(new CustomError("Verification failed", 400));
        }
    } catch (error) {
        next(
            error instanceof CustomError
                ? error
                : new CustomError("Internal Server Error", 500),
        );
    } finally {
        req.session.currentChallenge = undefined;
        req.session.loggedInUserId = undefined;
    }
};
```

Proses autentikasi (login) kami melibatkan dua titik akhir:

**1. Titik Akhir handleLoginStart:**

Titik akhir ini diaktifkan ketika pengguna mencoba untuk login. Ini pertama-tama memeriksa
apakah nama pengguna ada di database, mengembalikan kesalahan jika tidak ditemukan. Dalam
skenario dunia nyata, Anda mungkin menawarkan untuk membuat akun baru sebagai gantinya.

Untuk pengguna yang sudah ada, ini mengambil ID pengguna dari database, menyimpannya di
sesi, dan menghasilkan opsi `PublicKeyCredentialRequestOptions`. `allowCredentials`
dibiarkan kosong untuk menghindari pembatasan penggunaan kredensial. Itulah mengapa semua
passkey yang tersedia untuk [relying party](https://www.corbado.com/glossary/relying-party) ini dapat dipilih di
modal passkey.

Tantangan yang dihasilkan juga disimpan di sesi dan `PublicKeyCredentialRequestOptions`
dikirim kembali ke frontend.

`PublicKeyCredentialRequestOptions` terdiri dari data berikut:

```
dictionary PublicKeyCredentialRequestOptions {
    required BufferSource                challenge;
    unsigned long                        timeout;
    USVString                            rpId;
    sequence<PublicKeyCredentialDescriptor> allowCredentials = [];
    DOMString                            userVerification = "preferred";
    AuthenticationExtensionsClientInputs extensions;
};
```

- **challenge:** Nilai acak yang aman dari server WebAuthn yang digunakan untuk mencegah
  serangan replay selama proses autentikasi.
- **timeout:** Opsional, mengatur waktu dalam milidetik yang dimiliki pengguna untuk
  merespons permintaan autentikasi.
- **rpId:** ID [pihak yang mengandalkan](https://www.corbado.com/id/glossary/relying-party), biasanya domain
  layanan.
- **allowCredentials:** Daftar deskriptor kredensial opsional, yang menentukan kredensial
  mana yang dapat digunakan untuk autentikasi (login) ini.
- **userVerification:** Menentukan persyaratan untuk verifikasi pengguna, seperti
  "required", "preferred", atau "discouraged".
- **extensions:** Opsional, memungkinkan ekstensi klien tambahan.

**2. Titik Akhir handleLoginFinish:**

Titik akhir ini mengambil `currentChallenge` dan `loggedInUserId` dari sesi.

Ini menanyakan database untuk kredensial yang tepat menggunakan ID kredensial dari body.
Jika kredensial ditemukan, ini berarti bahwa pengguna yang terkait dengan ID kredensial
ini sekarang dapat diautentikasi (login). Kemudian, kita dapat menanyakan pengguna dari
tabel pengguna melalui ID pengguna yang kita dapatkan dari kredensial dan memverifikasi
`authenticationResponse` menggunakan challenge dan body permintaan. Jika semuanya
berhasil, kita menampilkan pesan sukses login. Jika tidak ada kredensial yang cocok
ditemukan, kesalahan dikirim.

Selain itu, jika verifikasi berhasil, penghitung kredensial diperbarui, challenge yang
digunakan dan loggedInUserId dihapus dari sesi.

Di atas semua itu, kita dapat menghapus folder `src/app` dan `src/constant` bersama dengan
semua file di dalamnya.

Catatan: Manajemen sesi yang tepat dan perlindungan rute, yang krusial dalam aplikasi
kehidupan nyata, dihilangkan di sini untuk kesederhanaan dalam tutorial ini.

### 5.8 Rute Passkey

Terakhir, kita perlu memastikan bahwa controller kita dapat dijangkau dengan menambahkan
rute yang sesuai ke `routes.ts` yang berada di direktori baru `src/routes`:

```ts filename="routes.ts"
import express from "express";
import { handleError } from "../middleware/errorHandler";
import { handleRegisterStart, handleRegisterFinish } from "../controllers/registration";
import { handleLoginStart, handleLoginFinish } from "../controllers/authentication";

const router = express.Router();

router.post("/registerStart", handleRegisterStart);
router.post("/registerFinish", handleRegisterFinish);
router.post("/loginStart", handleLoginStart);
router.post("/loginFinish", handleLoginFinish);

router.use(handleError);

export { router };
```

## 6. Integrasikan Passkey ke Frontend

Bagian dari tutorial passkey ini berfokus pada cara mendukung passkey di frontend aplikasi
Anda. Kami memiliki frontend yang sangat dasar yang terdiri dari tiga file: `index.html`,
`styles.css`, dan `script.js`. Ketiga file tersebut berada di folder `src/public` baru.

File `index.html` berisi bidang input untuk nama pengguna dan dua tombol untuk mendaftar
dan login. Selain itu, kami mengimpor skrip `@simplewebauthn/browser` yang menyederhanakan
interaksi dengan API Web Authentication peramban di file `js/script.js`.

`index.html` terlihat seperti ini:

```html filename="index.html"
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Passkey Tutorial</title>
        <link rel="stylesheet" href="css/style.css" />
    </head>
    <body>
        <div class="container">
            <h1>Passkey Tutorial</h1>
            <div id="message"></div>

            <div class="input-group">
                <input type="text" id="username" placeholder="Enter username" />
                <button id="registerButton">Register</button>
                <button id="loginButton">Login</button>
            </div>
        </div>

        <script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.es5.umd.min.js"></script>
        <script src="js/script.js"></script>
    </body>
</html>
```

`script.js` terlihat sebagai berikut:

```javascript filename="script.js"
document.getElementById("registerButton").addEventListener("click", register);
document.getElementById("loginButton").addEventListener("click", login);

function showMessage(message, isError = false) {
    const messageElement = document.getElementById("message");
    messageElement.textContent = message;
    messageElement.style.color = isError ? "red" : "green";
}

async function register() {
    // Retrieve the username from the input field
    const username = document.getElementById("username").value;

    try {
        // Get registration options from your server. Here, we also receive the challenge.
        const response = await fetch("/api/passkey/registerStart", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ username: username }),
        });
        console.log(response);

        // Check if the registration options are ok.
        if (!response.ok) {
            throw new Error(
                "User already exists or failed to get registration options from server",
            );
        }

        // Convert the registration options to JSON.
        const options = await response.json();
        console.log(options);

        // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello).
        // A new attestation is created. This also means a new public-private-key pair is created.
        const attestationResponse =
            await SimpleWebAuthnBrowser.startRegistration(options);

        // Send attestationResponse back to server for verification and storage.
        const verificationResponse = await fetch("/api/passkey/registerFinish", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(attestationResponse),
        });

        if (verificationResponse.ok) {
            showMessage("Registration successful");
        } else {
            showMessage("Registration failed", true);
        }
    } catch (error) {
        showMessage("Error: " + error.message, true);
    }
}

async function login() {
    // Retrieve the username from the input field
    const username = document.getElementById("username").value;

    try {
        // Get login options from your server. Here, we also receive the challenge.
        const response = await fetch("/api/passkey/loginStart", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ username: username }),
        });
        // Check if the login options are ok.
        if (!response.ok) {
            throw new Error("Failed to get login options from server");
        }
        // Convert the login options to JSON.
        const options = await response.json();
        console.log(options);

        // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello).
        // A new assertionResponse is created. This also means that the challenge has been signed.
        const assertionResponse =
            await SimpleWebAuthnBrowser.startAuthentication(options);

        // Send assertionResponse back to server for verification.
        const verificationResponse = await fetch("/api/passkey/loginFinish", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(assertionResponse),
        });

        if (verificationResponse.ok) {
            showMessage("Login successful");
        } else {
            showMessage("Login failed", true);
        }
    } catch (error) {
        showMessage("Error: " + error.message, true);
    }
}
```

Di `script.js`, ada tiga fungsi utama:

**1. Fungsi showMessage:**

Ini adalah fungsi utilitas yang digunakan terutama untuk menampilkan pesan kesalahan,
membantu dalam debugging.

**2. Fungsi Register:**

Dipicu ketika pengguna mengklik "Register". Ini mengekstrak nama pengguna dari bidang
input dan mengirimkannya ke titik akhir passkeyRegisterStart. Responsnya mencakup
**`PublicKeyCredentialCreationOptions`**, yang dikonversi ke JSON dan diteruskan ke
`SimpleWebAuthnBrowser.startRegistration`. Panggilan ini mengaktifkan authenticator
perangkat (seperti [Face ID](https://www.corbado.com/faq/is-face-id-passkey) atau Touch ID). Setelah autentikasi
lokal berhasil, challenge yang ditandatangani dikirim kembali ke titik akhir
`passkeyRegisterFinish`, menyelesaikan proses
[pembuatan passkey](https://www.corbado.com/id/blog/praktik-terbaik-pembuatan-passkey).

Selama proses pendaftaran (sign-up), objek [atestasi](https://www.corbado.com/id/glossary/ctap) memainkan peran
penting, jadi mari kita lihat lebih dekat.

![Objek Atestasi Passkey](https://www.corbado.com/website-assets/6572cda3f3869adee2c38e05_passkey_attestation_object_5bfcce1977.png)

Objek [atestasi](https://www.corbado.com/id/glossary/ctap) terutama terdiri dari tiga komponen: `fmt`, `attStmt`,
dan `authData`. Elemen `fmt` menandakan format pernyataan atestasi, sementara `attStmt`
mewakili pernyataan atestasi itu sendiri. Dalam skenario di mana atestasi dianggap tidak
perlu, `fmt` akan ditetapkan sebagai "none," yang mengarah ke `attStmt` yang kosong.

Fokusnya adalah pada segmen `authData` dalam struktur ini. Segmen ini adalah kunci untuk
mengambil elemen penting seperti ID [pihak yang mengandalkan](https://www.corbado.com/id/glossary/relying-party),
flag, penghitung, dan data kredensial yang dibuktikan di server kami. Mengenai flag, yang
menarik adalah BS (Backup State) dan BE (Backup Eligibility) yang memberikan lebih banyak
informasi jika passkey disinkronkan (misalnya melalui
[iCloud Keychain](https://www.corbado.com/glossary/icloud-keychain) atau
[1Password](https://www.corbado.com/blog/1password-passkeys-best-practices-analysis)). Selain itu, UV (User
Verification) dan UP (User Presence) memberikan informasi yang lebih berguna.

![Data Authenticator Passkey](https://www.corbado.com/website-assets/6572cf3ce1fa2982352d6c88_passkey_authenticator_data_7f61aceaea.png)

Sangat penting untuk dicatat bahwa berbagai bagian dari objek atestasi, termasuk data
authenticator, ID [pihak yang mengandalkan](https://www.corbado.com/id/glossary/relying-party), dan pernyataan
atestasi, baik di-hash atau ditandatangani secara digital oleh authenticator menggunakan
kunci pribadinya. Proses ini merupakan bagian integral untuk menjaga integritas
keseluruhan objek atestasi.

**3. Fungsi Login:**

Diaktifkan ketika pengguna mengklik "Login". Mirip dengan fungsi register, ini mengekstrak
nama pengguna dan mengirimkannya ke titik akhir `passkeyLoginStart`. Responsnya, yang
berisi **`PublicKeyCredentialRequestOptions`**, dikonversi ke JSON dan digunakan dengan
`SimpleWebAuthnBrowser.startAuthentication`. Ini memicu autentikasi lokal pada perangkat.
Challenge yang ditandatangani kemudian dikirim kembali ke titik akhir
`passkeyLoginFinish`. Respons yang berhasil dari titik akhir ini menunjukkan bahwa
pengguna telah berhasil login ke aplikasi.

Selain itu, file CSS yang menyertainya menyediakan gaya sederhana untuk aplikasi:

```css
body {
    font-family: "Helvetica Neue", Arial, sans-serif;
    text-align: center;
    padding: 40px;
    background-color: #f3f4f6;
    color: #333;
}

.container {
    max-width: 400px;
    margin: auto;
    background: white;
    padding: 20px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    border-radius: 8px;
}

h1 {
    color: #007bff;
    font-size: 24px;
    margin-bottom: 20px;
}

.input-group {
    margin-bottom: 20px;
}

input[type="text"] {
    padding: 10px;
    margin-bottom: 10px;
    border: 1px solid #ced4da;
    border-radius: 4px;
    width: calc(100% - 22px);
}

button {
    width: calc(50% - 20px);
    padding: 10px 0;
    margin: 5px;
    font-size: 16px;
    cursor: pointer;
    border: none;
    border-radius: 4px;
    background-color: #007bff;
    color: white;
}

button:hover {
    background-color: #0056b3;
}

#message {
    color: #dc3545;
    margin: 20px;
}
```

## 7. Jalankan Aplikasi Contoh Passkey

Untuk melihat aplikasi Anda beraksi, kompilasi dan jalankan kode TypeScript Anda dengan:

```bash
npm run dev
```

Server Anda sekarang seharusnya sudah berjalan di
[http://localhost:8080](http://localhost:8080).

**Pertimbangan untuk Produksi:**

Ingat, apa yang telah kita bahas adalah garis besar dasar. Saat menerapkan aplikasi
passkey di lingkungan produksi, Anda perlu mendalami lebih lanjut tentang:

- **Tindakan Keamanan:** Terapkan praktik
  [keamanan](https://www.corbado.com/id/blog/cara-mengaktifkan-passkey-android) yang kuat untuk melindungi data
  pengguna.
- **Penanganan Kesalahan:** Pastikan aplikasi Anda menangani dan mencatat kesalahan dengan
  baik.
- **Manajemen Database:** Optimalkan operasi database untuk skalabilitas dan keandalan.

## 8. Integrasi DevOps Passkey

Kami telah menyiapkan kontainer Docker untuk database kami. Selanjutnya, kami akan
memperluas pengaturan Docker Compose kami untuk menyertakan server dengan backend dan
frontend. File `docker-compose.yml` Anda harus diperbarui sesuai.

Untuk mengemas aplikasi kami dalam kontainer, kami membuat Dockerfile baru yang menginstal
paket yang diperlukan dan memulai server pengembangan:

```docker filename="Docker"
# Use an official Node runtime as a parent image
FROM node:20-alpine

# Set the working directory in the container
WORKDIR /usr/src/app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install any needed packages
RUN npm install

# Bundle your app's source code inside the Docker image
COPY . .

# Make port 8080 available to the world outside this container
EXPOSE 8080

# Define the command to run your app
CMD ["npm", "run", "dev"]
```

Kemudian, kami juga memperluas file `docker-compose.yml` untuk memulai kontainer ini:

```yaml filename="docker-compose.yml"
version: "3.1"

services:
    db:
        image: mysql
        command: --default-authentication-plugin=mysql_native_password
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: my-secret-pw
            MYSQL_DATABASE: webauthn_db
        ports:
            - "3306:3306"
        volumes:
            - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql

    app:
        build: .
        ports:
            - "8080:8080"
        environment:
            - DB_HOST=db
            - DB_USER=root
            - DB_PASSWORD=my-secret-pw
            - DB_NAME=webauthn_db
            - SESSION_SECRET=secret123
        depends_on:
            - db
```

Jika Anda sekarang menjalankan `docker compose up` di terminal Anda dan mengakses
[http://localhost:8080](http://localhost:8080), Anda akan melihat versi kerja dari
[aplikasi web](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql) passkey Anda (di sini berjalan
di [Windows 11](https://www.corbado.com/blog/passkeys-windows-11) 23H2 + Chrome 119):

![Tutorial Passkey Windows Hello](https://www.corbado.com/website-assets/6572cf57f3869adee2c49ff5_passkey_tutorial_windows_hello_5ac0585957.png)

## 9. Tips Passkey Tambahan untuk Pengembang

Karena kami telah bekerja cukup lama dengan implementasi passkey, kami menemukan beberapa
tantangan jika Anda bekerja pada aplikasi passkey di dunia nyata:

- Kompatibilitas dan dukungan perangkat / platform
- Onboarding dan edukasi pengguna
- Menangani perangkat yang hilang atau berubah
- Autentikasi lintas platform
- Mekanisme fallback
- Kompleksitas pengkodean: Pengkodean seringkali merupakan bagian tersulit karena Anda
  harus berurusan dengan JSON, [CBOR](https://www.corbado.com/glossary/cbor), uint8arrays, buffer, blob, database
  yang berbeda, base64 dan base64url di mana banyak kesalahan dapat terjadi
- [Manajemen passkey](https://www.corbado.com/id/blog/penyedia-passkey-aaguid-adopsi) (misalnya untuk menambah,
  menghapus, atau mengganti nama passkey)

Selain itu, kami memiliki tips berikut untuk pengembang ketika datang ke bagian
implementasi:

**Manfaatkan Passkeys Debugger**

[**Passkeys debugger**](https://www.passkeys-debugger.io/) membantu menguji berbagai
pengaturan server WebAuthn dan respons klien. Selain itu, ini menyediakan parser yang
hebat untuk respons authenticator.

**Debug dengan Fitur Log Perangkat Chrome**

Gunakan log perangkat Chrome (dapat diakses melalui
[chrome://device-log/](chrome://device-log/)) untuk memantau panggilan FIDO/WebAuthn.
Fitur ini menyediakan log real-time dari proses autentikasi (login), memungkinkan Anda
untuk melihat data yang dipertukarkan dan memecahkan masalah apa pun yang muncul.

Jalan pintas lain yang sangat berguna untuk mendapatkan semua passkey Anda di Chrome
adalah dengan menggunakan [chrome://settings/passkeys](chrome://settings/passkeys).

**Gunakan Authenticator WebAuthn Virtual Chrome**

Untuk menghindari penggunaan prompt Touch ID, [Face ID](https://www.corbado.com/faq/is-face-id-passkey), atau
[Windows Hello](https://www.corbado.com/glossary/windows-hello) selama pengembangan, Chrome dilengkapi dengan
authenticator WebAuthn virtual yang sangat praktis yang meniru authenticator nyata. Kami
sangat merekomendasikan untuk menggunakannya untuk mempercepat pekerjaan. Temukan detail
lebih lanjut [di sini](https://developer.chrome.com/docs/devtools/webauthn/).

**Uji di Berbagai Platform dan Peramban**

Pastikan kompatibilitas dan fungsionalitas di berbagai peramban dan platform. WebAuthn
berperilaku berbeda di peramban yang berbeda, jadi pengujian menyeluruh adalah kuncinya.

**Uji di Perangkat yang Berbeda**

Di sini sangat berguna untuk bekerja dengan alat seperti
[ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok), di mana Anda dapat membuat
aplikasi lokal Anda dapat dijangkau di perangkat (seluler) lain.

**Atur Verifikasi Pengguna ke `preferred`**

Saat mendefinisikan properti untuk `userVerification` di
**`PublicKeyCredentialRequestOptions`**, pilih untuk mengaturnya ke `preferred` karena ini
adalah trade-off yang baik antara kegunaan dan
[keamanan](https://www.corbado.com/id/blog/cara-mengaktifkan-passkey-android). Ini berarti bahwa pemeriksaan
keamanan ada di perangkat yang sesuai sementara keramahan pengguna tetap terjaga di
perangkat tanpa kemampuan biometrik.

## 10. Kesimpulan: Tutorial Passkey

Kami harap tutorial passkey ini memberikan pemahaman yang jelas tentang cara
mengimplementasikan passkey secara efektif. Sepanjang tutorial, kami telah memandu Anda
melalui langkah-langkah penting untuk membuat aplikasi passkey, dengan fokus pada konsep
dasar dan implementasi praktis. Meskipun panduan ini berfungsi sebagai titik awal, masih
banyak lagi yang bisa dijelajahi dan disempurnakan di dunia WebAuthn.

Kami mendorong para pengembang untuk mendalami nuansa passkey (misalnya menambahkan
beberapa passkey, memeriksa kesiapan passkey pada perangkat, atau menawarkan solusi
pemulihan). Ini adalah perjalanan yang layak untuk dimulai, menawarkan tantangan dan
imbalan besar dalam meningkatkan autentikasi pengguna. Dengan passkey, Anda tidak hanya
membangun sebuah fitur; Anda berkontribusi pada dunia digital yang lebih aman dan ramah
pengguna.
