---
url: 'https://www.corbado.com/id/blog/cara-membangun-verifiable-credential-issuer'
title: 'Cara Membangun Issuer Kredensial Digital (Panduan Developer)'
description: 'Pelajari cara membangun issuer W3C Verifiable Credential menggunakan protokol OpenID4VCI. Panduan langkah demi langkah ini menunjukkan cara membuat aplikasi Next.js yang menerbitkan kredensial yang ditandatangani secara kriptografis dan kompatibel dengan '
lang: 'id'
author: 'Amine'
date: '2025-08-20T15:39:13.673Z'
lastModified: '2026-03-27T07:07:19.480Z'
keywords: 'issuer kredensial digital, tutorial issuer, membangun issuer'
category: 'Digital Credentials'
---

# Cara Membangun Issuer Kredensial Digital (Panduan Developer)

## 1. Pendahuluan

[Kredensial Digital](https://www.corbado.com/id/blog/digital-credentials-api) adalah cara yang andal untuk
membuktikan identitas dan klaim secara aman dan menjaga privasi. Tapi, bagaimana cara
pengguna mendapatkan kredensial ini? Di sinilah peran **Issuer** menjadi sangat penting.
[Issuer](https://www.corbado.com/glossary/issuer) adalah entitas tepercaya—seperti lembaga
[pemerintah](https://www.corbado.com/passkeys-for-public-sector), universitas, atau bank—yang bertanggung jawab
untuk membuat dan mendistribusikan kredensial yang ditandatangani secara digital kepada
pengguna.

Panduan ini menyediakan [tutorial](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql) langkah
demi langkah yang komprehensif untuk membangun sebuah [Issuer](https://www.corbado.com/glossary/issuer)
[Kredensial Digital](https://www.corbado.com/id/blog/digital-credentials-api). Kita akan fokus pada protokol
**OpenID for Verifiable Credential Issuance (OpenID4VCI)**, sebuah standar modern yang
mendefinisikan cara pengguna dapat memperoleh kredensial dari [Issuer](https://www.corbado.com/glossary/issuer)
dan menyimpannya dengan aman di [wallet](https://www.corbado.com/blog/digital-wallet-assurance) digital mereka.

Hasil akhirnya adalah sebuah aplikasi [Next.js](https://www.corbado.com/blog/nextjs-passkeys) fungsional yang
dapat:

1. Menerima data pengguna melalui formulir web sederhana.
2. Menghasilkan penawaran kredensial sekali pakai yang aman.
3. Menampilkan penawaran tersebut sebagai [kode QR](https://www.corbado.com/id/blog/kode-qr-login-autentikasi)
   agar pengguna dapat memindainya dengan [wallet](https://www.corbado.com/blog/digital-wallet-assurance) seluler
   mereka.
4. Menerbitkan kredensial yang ditandatangani secara kriptografis yang dapat disimpan dan
   ditunjukkan pengguna untuk verifikasi.

### 1.1 Memahami Terminologi: Kredensial Digital vs. Verifiable Credentials

Sebelum kita lanjut, penting untuk memperjelas perbedaan antara dua konsep yang saling
terkait namun berbeda:

- **Kredensial Digital (Istilah Umum):** Ini adalah kategori luas yang mencakup segala
  bentuk kredensial, sertifikat, atau [atestasi](https://www.corbado.com/id/glossary/ctap) digital. Ini bisa
  termasuk sertifikat digital sederhana, lencana digital dasar, atau kredensial apa pun
  yang disimpan secara elektronik yang mungkin memiliki atau tidak memiliki fitur
  [keamanan](https://www.corbado.com/id/blog/cara-mengaktifkan-passkey-android) kriptografis.

- **Verifiable Credentials (VCs - Standar W3C):** Ini adalah jenis
  [kredensial digital](https://www.corbado.com/id/blog/digital-credentials-api) spesifik yang mengikuti standar
  W3C [Verifiable Credentials](https://www.corbado.com/glossary/microcredentials) Data Model. Verifiable
  Credentials adalah kredensial yang ditandatangani secara kriptografis, tahan terhadap
  manipulasi, dan menghargai privasi yang dapat diverifikasi secara independen. Mereka
  mencakup persyaratan teknis khusus seperti:
    - Tanda tangan kriptografis untuk otentisitas dan integritas
    - Model dan format data yang terstandardisasi
    - Mekanisme presentasi yang menjaga privasi
    - Protokol verifikasi yang dapat dioperasikan

**Dalam panduan ini, kita secara spesifik akan membangun sebuah issuer Verifiable
Credential** yang mengikuti standar W3C, bukan sekadar sistem kredensial digital biasa.
Protokol [OpenID4VCI](https://www.corbado.com/glossary/openid4vci) yang kita gunakan dirancang khusus untuk
menerbitkan Verifiable Credentials, dan format JWT-VC yang akan kita implementasikan
adalah format yang sesuai dengan W3C untuk
[Verifiable Credentials](https://www.corbado.com/glossary/microcredentials).

### 1.2 Cara Kerjanya

Kunci di balik kredensial digital terletak pada model **"segitiga kepercayaan" (trust
triangle)** yang sederhana namun kuat, yang melibatkan tiga pemain utama:

- **Issuer:** Otoritas tepercaya (misalnya, lembaga
  [pemerintah](https://www.corbado.com/passkeys-for-public-sector), universitas, atau bank) yang menandatangani
  dan menerbitkan kredensial secara kriptografis kepada pengguna. **Inilah peran yang
  sedang kita bangun dalam panduan ini.**
- **Holder:** Pengguna, yang menerima kredensial dan menyimpannya dengan aman di
  [wallet](https://www.corbado.com/blog/digital-wallet-assurance) digital pribadi di perangkat mereka.
- **Verifier:** Aplikasi atau layanan yang perlu memeriksa kredensial pengguna.

![Ekosistem W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model/diagrams/ecosystem.svg)

Alur penerbitan adalah langkah pertama dalam ekosistem ini. Issuer memvalidasi informasi
pengguna dan memberikan mereka kredensial. Setelah Holder memiliki kredensial ini di
wallet mereka, mereka dapat menunjukkannya kepada Verifier untuk membuktikan identitas
atau klaim mereka, melengkapi segitiga tersebut.

Berikut adalah gambaran singkat aplikasi akhir saat beraksi:

**Langkah 1: Input Data Pengguna** Pengguna mengisi formulir dengan informasi pribadi
mereka untuk meminta kredensial baru.
![Formulir Input Data Pengguna](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_1_0733a9e1da.png)

**Langkah 2: Pembuatan Penawaran Kredensial** Aplikasi menghasilkan penawaran kredensial
yang aman, ditampilkan sebagai [kode QR](https://www.corbado.com/id/blog/kode-qr-login-autentikasi) dan kode
pra-otorisasi.
![Kode QR Penawaran Kredensial](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_2_3f1881c473.png)

**Langkah 3: Interaksi Wallet** Pengguna memindai
[kode QR](https://www.corbado.com/id/blog/kode-qr-login-autentikasi) dengan wallet yang kompatibel (misalnya,
Sphereon Wallet) dan memasukkan PIN untuk mengotorisasi penerbitan.
![Penawaran Kredensial di wallet](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_3_b80d689dfe.png)
![Penyisipan Kode PIN](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_4_ca8bad8d11.png)

**Langkah 4: Kredensial Diterbitkan** Wallet menerima dan menyimpan kredensial digital
yang baru diterbitkan, siap untuk digunakan di masa mendatang.
![Mengonfirmasi detail kredensial](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_5_55b8150597.png)
![Kredensial ditambahkan](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_6_7f5ac5745d.png)

## 2. Prasyarat untuk Membangun Issuer

Sebelum kita masuk ke kode, mari kita bahas pengetahuan dasar dan alat yang kita perlukan.
Panduan ini mengasumsikan kita memiliki pemahaman dasar tentang konsep pengembangan web,
tetapi prasyarat berikut sangat penting untuk membangun issuer kredensial.

### 2.1 Pilihan Protokol

Issuer kita dibangun di atas serangkaian standar terbuka yang memastikan interoperabilitas
antara wallet dan layanan penerbitan. Untuk
[tutorial](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql) ini, kita akan fokus pada hal
berikut:

| Standar / Protokol                                                | Deskripsi                                                                                                                                                                                                                                              |
| :---------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **OpenID4VCI**                                                    | **OpenID for Verifiable Credential Issuance.** Ini adalah protokol inti yang akan kita gunakan. Protokol ini mendefinisikan alur standar tentang bagaimana pengguna (melalui wallet mereka) dapat meminta dan menerima kredensial dari seorang Issuer. |
| **[JWT-VC](https://www.w3.org/TR/vc-data-model/#json-web-token)** | **JWT-based Verifiable Credentials.** Format untuk kredensial yang akan kita terbitkan. Ini adalah standar W3C yang mengkodekan verifiable credentials sebagai JSON Web Tokens (JWTs), menjadikannya ringkas dan ramah-web.                            |
| **[ISO mDoc](https://www.corbado.com/id/blog/digital-credentials-api)**                  | **ISO/IEC 18013-5.** Standar internasional untuk Surat Izin Mengemudi seluler (mDLs). Meskipun kita menerbitkan JWT-VC, _klaim_ di dalamnya disusun agar kompatibel dengan model data mDoc (misalnya, `eu.europa.ec.eudi.pid.1`).                      |
| **OAuth 2.0**                                                     | Kerangka kerja otorisasi yang mendasari yang digunakan oleh OpenID4VCI. Kita akan mengimplementasikan alur `pre-authorized_code`, yang merupakan jenis grant spesifik yang dirancang untuk penerbitan kredensial yang aman dan ramah pengguna.         |

#### 2.1.1 Alur Otorisasi: Kode Pra-Otorisasi vs. Kode Otorisasi

[OpenID4VCI](https://www.corbado.com/glossary/openid4vci) mendukung dua alur otorisasi utama untuk menerbitkan
kredensial:

1. **Alur Kode Pra-Otorisasi:** Dalam alur ini, Issuer menghasilkan kode sekali pakai yang
   berumur pendek (`pre-authorized_code`) yang langsung tersedia untuk pengguna. Wallet
   pengguna kemudian dapat menukar kode ini secara langsung dengan kredensial. Alur ini
   ideal untuk skenario di mana pengguna sudah diautentikasi dan hadir di situs web
   Issuer, karena memberikan pengalaman penerbitan yang mulus dan instan tanpa pengalihan.

2. **Alur Kode Otorisasi:** Ini adalah alur standar [OAuth 2.0](https://www.corbado.com/glossary/oauth2), di mana
   pengguna dialihkan ke server otorisasi untuk memberikan persetujuan. Setelah disetujui,
   server mengirimkan `authorization_code` kembali ke `redirect_uri` yang terdaftar. Alur
   ini lebih cocok untuk aplikasi pihak ketiga yang memulai proses penerbitan atas nama
   pengguna.

**Untuk tutorial ini, kita akan menggunakan alur `pre-authorized_code`.** Kita memilih
pendekatan ini karena lebih sederhana dan memberikan pengalaman pengguna yang lebih
langsung untuk kasus penggunaan spesifik kita: pengguna yang langsung meminta kredensial
dari situs web Issuer itu sendiri. Ini menghilangkan kebutuhan untuk pengalihan yang
kompleks dan pendaftaran klien, membuat logika penerbitan inti lebih mudah dipahami dan
diimplementasikan.

Kombinasi standar ini memungkinkan kita untuk membangun issuer yang kompatibel dengan
berbagai macam wallet digital dan memastikan proses yang aman dan terstandardisasi bagi
pengguna.

### 2.2 Pilihan Tech Stack

Untuk membangun issuer kita, kita akan menggunakan tech stack modern dan kuat yang sama
dengan yang kita gunakan untuk verifier, memastikan pengalaman developer yang konsisten
dan berkualitas tinggi.

#### 2.2.1 Bahasa: TypeScript

Kita akan menggunakan **TypeScript** untuk kode frontend dan backend kita. Pengetikan
statisnya sangat berharga dalam aplikasi yang kritis terhadap
[keamanan](https://www.corbado.com/id/blog/cara-mengaktifkan-passkey-android) seperti issuer, karena membantu
mencegah kesalahan umum dan meningkatkan kualitas serta kemudahan pemeliharaan kode secara
keseluruhan.

#### 2.2.2 Framework: Next.js

**Next.js** adalah framework pilihan kita karena menyediakan pengalaman yang mulus dan
terintegrasi untuk membangun aplikasi
[full-stack](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql).

- **Untuk Frontend:** Kita akan menggunakan [Next.js](https://www.corbado.com/blog/nextjs-passkeys) dengan
  [React](https://www.corbado.com/blog/react-passkeys) untuk membangun antarmuka pengguna di mana pengguna dapat
  memasukkan data mereka untuk meminta kredensial.
- **Untuk Backend:** Kita akan memanfaatkan **Next.js API Routes** untuk membuat endpoint
  sisi server yang menangani alur [OpenID4VCI](https://www.corbado.com/glossary/openid4vci), mulai dari
  menghasilkan penawaran kredensial hingga menerbitkan kredensial akhir yang
  ditandatangani.

#### 2.2.3 Library Kunci

Implementasi kita akan mengandalkan beberapa library kunci untuk menangani tugas-tugas
spesifik:

- **next**, **react**, dan **react-dom**: Library inti untuk aplikasi
  [Next.js](https://www.corbado.com/blog/nextjs-passkeys) kita.
- **mysql2**: Klien [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) untuk
  [Node.js](https://www.corbado.com/blog/nodejs-passkeys), digunakan untuk menyimpan kode otorisasi dan data
  sesi.
- **uuid**: Library untuk menghasilkan pengidentifikasi unik, yang akan kita gunakan untuk
  membuat nilai `pre-authorized_code`.
- **jose**: Library yang kuat untuk menangani JSON Web Signatures (JWS), yang akan kita
  gunakan untuk menandatangani kredensial yang kita terbitkan secara kriptografis.

### 2.3 Dapatkan Wallet Uji Coba

Untuk menguji issuer Anda, Anda akan memerlukan wallet seluler yang mendukung protokol
OpenID4VCI. Untuk [tutorial](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql) ini, kami
merekomendasikan **Sphereon Wallet**, yang tersedia untuk
[Android](https://www.corbado.com/blog/how-to-enable-passkeys-android) dan
[iOS](https://www.corbado.com/blog/how-to-enable-passkeys-ios).

**Cara Menginstal Sphereon Wallet:**

1. **Unduh wallet** dari
   [Google Play Store](https://play.google.com/store/apps/details?id=com.sphereon.ssi.wallet)
   atau [Apple App Store](https://apps.apple.com/us/app/sphereon-wallet/id1661096796).
2. Instal aplikasi di perangkat seluler Anda.
3. Setelah terinstal, wallet siap menerima penawaran kredensial dengan memindai kode QR.

### 2.4 Pengetahuan Kriptografi

Menerbitkan kredensial adalah operasi yang sangat penting bagi
[keamanan](https://www.corbado.com/id/blog/cara-mengaktifkan-passkey-android) yang bergantung pada konsep
kriptografi dasar untuk memastikan kepercayaan dan keaslian.

#### 2.4.1 Tanda Tangan Digital

Pada intinya, [Verifiable Credential](https://www.corbado.com/glossary/verifiable-credential) adalah sekumpulan
klaim yang telah **ditandatangani secara digital** oleh Issuer. Tanda tangan ini
memberikan dua jaminan:

- **Keaslian:** Ini membuktikan bahwa kredensial tersebut dibuat oleh Issuer yang sah.
- **Integritas:** Ini membuktikan bahwa kredensial tersebut belum diubah sejak
  diterbitkan.

#### 2.4.2 Kriptografi Kunci Publik/Privat

Tanda tangan digital dibuat menggunakan
[kriptografi kunci publik](https://www.corbado.com/id/blog/webauthn-pubkeycredparams-credentialpublickey)/privat.
Begini cara kerjanya:

1. **Issuer memiliki sepasang kunci:** **kunci privat**, yang dirahasiakan dan diamankan,
   dan **kunci publik** yang sesuai, yang tersedia untuk umum.
2. **Penandatanganan:** Saat Issuer membuat kredensial, ia menggunakan **kunci privatnya**
   untuk menghasilkan tanda tangan digital unik untuk data kredensial tersebut.
3. **Verifikasi:** Verifier nantinya dapat menggunakan **kunci publik** Issuer untuk
   memeriksa tanda tangan. Jika pemeriksaan berhasil, Verifier tahu bahwa kredensial
   tersebut asli dan belum diubah.

Dalam implementasi kita, kita akan menghasilkan sepasang kunci Elliptic Curve (EC) dan
menggunakan algoritma `ES256` untuk menandatangani JWT-VC. Kunci publik disematkan dalam
DID Issuer (`did:web`), memungkinkan setiap Verifier untuk menemukannya dan memvalidasi
tanda tangan kredensial. **Catatan:** Klaim `aud` (audience) sengaja dihilangkan dalam JWT
kita, karena kredensial ini dirancang untuk tujuan umum dan tidak terikat pada wallet
tertentu. Jika Anda ingin membatasi penggunaan untuk audiens tertentu, sertakan klaim
`aud` dan atur nilainya sesuai.

## 3. Gambaran Arsitektur

Apilikasi Issuer kita dibangun sebagai proyek Next.js
[full-stack](https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql), dengan pemisahan yang jelas
antara logika frontend dan backend. Arsitektur ini memungkinkan kita untuk menciptakan
pengalaman pengguna yang mulus sambil menangani semua operasi penting keamanan di server.
**Penting:** Tabel `verification_sessions` dan `verified_credentials` yang disertakan
dalam SQL tidak diperlukan untuk issuer ini tetapi disertakan untuk kelengkapan.

- **Frontend (`src/app/issue/page.tsx`):** Sebuah halaman [React](https://www.corbado.com/blog/react-passkeys)
  tunggal yang memungkinkan pengguna memasukkan data mereka untuk meminta kredensial.
  Halaman ini melakukan panggilan API ke backend kita untuk memulai proses penerbitan.
- **Backend API Routes (`src/app/api/issue/...`):** Serangkaian endpoint sisi server yang
  mengimplementasikan protokol OpenID4VCI.
    - `/.well-known/openid-credential-issuer`: Endpoint metadata publik. Ini adalah URL
      pertama yang akan diperiksa oleh wallet untuk menemukan kapabilitas issuer, termasuk
      server otorisasi, endpoint token, endpoint kredensial, dan jenis kredensial yang
      ditawarkannya.
    - `/.well-known/openid-configuration`: Endpoint penemuan OpenID Connect standar.
      Meskipun terkait erat dengan yang di atas, endpoint ini menyajikan konfigurasi
      terkait OIDC yang lebih luas dan seringkali diperlukan untuk interoperabilitas
      dengan klien OpenID standar.
    - `/.well-known/did.json`: Dokumen DID untuk issuer kita. Saat menggunakan metode
      `did:web`, file ini digunakan untuk mempublikasikan kunci publik issuer, yang dapat
      digunakan verifier untuk memvalidasi tanda tangan dari kredensial yang
      diterbitkannya.
    - `authorize/route.ts`: Membuat `pre-authorized_code` dan penawaran kredensial.
    - `token/route.ts`: Menukarkan `pre-authorized_code` dengan
      [access token](https://www.corbado.com/glossary/access-token).
    - `credential/route.ts`: Menerbitkan JWT-VC akhir yang ditandatangani secara
      kriptografis.
    - `schemas/pid/route.ts`: Mengekspos skema JSON untuk kredensial PID. Ini memungkinkan
      setiap konsumen kredensial untuk memahami struktur dan tipe datanya.
- **Library (`src/lib/`):**
    - `database.ts`: Mengelola semua interaksi database, seperti menyimpan kode otorisasi
      dan kunci issuer.
    - `crypto.ts`: Menangani semua operasi kriptografis, termasuk pembuatan kunci dan
      penandatanganan JWT.

Berikut adalah diagram yang mengilustrasikan alur penerbitan:

![Alur Penerbitan Kredensial Digital](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Mermaid_Chart_Create_complex_visual_diagrams_with_text_A_smarter_way_of_creating_diagrams_2025_07_29_145228_d28fd13731.svg)

## 4. Membangun Issuer

Sekarang setelah kita memiliki pemahaman yang kuat tentang standar, protokol, dan
arsitektur, kita bisa mulai membangun issuer kita.

> **Ikuti Langkah-Langkahnya atau Gunakan Kode Final**
>
> Kita sekarang akan membahas penyiapan dan implementasi kode langkah demi langkah. Jika
> Anda lebih suka langsung ke produk jadi, Anda dapat mengkloning proyek lengkap dari
> repositori GitHub kami dan menjalankannya secara lokal.
>
> ```bash
> git clone https://github.com/corbado/digital-credentials-example.git
> ```

### 4.1 Menyiapkan Proyek

Pertama, kita akan menginisialisasi proyek Next.js baru, menginstal dependensi yang
diperlukan, dan memulai database kita.

#### 4.1.1 Menginisialisasi Aplikasi Next.js

Buka terminal Anda, navigasikan ke direktori tempat Anda ingin membuat proyek, dan
jalankan perintah berikut. Kita menggunakan App Router, TypeScript, dan Tailwind CSS untuk
proyek ini.

```bash
npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm
```

Perintah ini membuat kerangka aplikasi Next.js baru di direktori Anda saat ini.

#### 4.1.2 Menginstal Dependensi

Selanjutnya, kita perlu menginstal library yang akan menangani JWT, koneksi database, dan
pembuatan UUID.

```bash
npm install jose mysql2 uuid @types/uuid
```

Perintah ini menginstal:

- `jose`: Untuk menandatangani dan memverifikasi JSON Web Tokens (JWTs).
- `mysql2`: Klien [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) untuk database kita.
- `uuid`: Untuk menghasilkan string tantangan unik.
- `@types/uuid`: Tipe TypeScript untuk library `uuid`.

#### 4.1.3 Memulai Database

Backend kita memerlukan database [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) untuk
menyimpan kode otorisasi, sesi penerbitan, dan kunci issuer. Kami telah menyertakan file
`docker-compose.yml` untuk mempermudah ini.

Jika Anda telah mengkloning repositori, Anda cukup menjalankan `docker-compose up -d`.
Jika Anda membangun dari awal, buat file bernama `docker-compose.yml` dengan konten
berikut:

```yaml
services:
    mysql:
        image: mysql:8.0
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: rootpassword
            MYSQL_DATABASE: digital_credentials
            MYSQL_USER: app_user
            MYSQL_PASSWORD: app_password
        ports:
            - "3306:3306"
        volumes:
            - mysql_data:/var/lib/mysql
            - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql
        healthcheck:
            test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
            timeout: 20s
            retries: 10

volumes:
    mysql_data:
```

Pengaturan Docker Compose ini juga memerlukan skrip inisialisasi SQL. Buat direktori
bernama `sql` dan di dalamnya, file bernama `init.sql` dengan konten berikut untuk
menyiapkan tabel yang diperlukan baik untuk verifier maupun issuer:

```sql
-- Create database if not exists
CREATE DATABASE IF NOT EXISTS digital_credentials;
USE digital_credentials;

-- Table for storing challenges
CREATE TABLE IF NOT EXISTS challenges (
    id VARCHAR(36) PRIMARY KEY,
    challenge VARCHAR(255) NOT NULL UNIQUE,
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    used BOOLEAN DEFAULT FALSE,
    INDEX idx_challenge (challenge),
    INDEX idx_expires_at (expires_at)
);

-- Table for storing verification sessions
CREATE TABLE IF NOT EXISTS verification_sessions (
    id VARCHAR(36) PRIMARY KEY,
    challenge_id VARCHAR(36),
    status ENUM('pending', 'verified', 'failed', 'expired') DEFAULT 'pending',
    presentation_data JSON,
    verified_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE,
    INDEX idx_challenge_id (challenge_id),
    INDEX idx_status (status)
);

-- Table for storing verified credentials data (optional)
CREATE TABLE IF NOT EXISTS verified_credentials (
    id VARCHAR(36) PRIMARY KEY,
    session_id VARCHAR(36),
    credential_type VARCHAR(255),
    issuer VARCHAR(255),
    subject VARCHAR(255),
    claims JSON,
    verified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (session_id) REFERENCES verification_sessions(id) ON DELETE CASCADE,
    INDEX idx_session_id (session_id),
    INDEX idx_credential_type (credential_type)
);

-- ISSUER TABLES

-- Table for storing authorization codes in OpenID4VCI flow
CREATE TABLE IF NOT EXISTS authorization_codes (
    id VARCHAR(36) PRIMARY KEY,
    code VARCHAR(255) NOT NULL UNIQUE,
    client_id VARCHAR(255),
    scope VARCHAR(255),
    code_challenge VARCHAR(255),
    code_challenge_method VARCHAR(50),
    redirect_uri TEXT,
    user_pin VARCHAR(10),
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    used BOOLEAN DEFAULT FALSE,
    INDEX idx_code (code),
    INDEX idx_expires_at (expires_at)
);

-- Table for storing issuance sessions
CREATE TABLE IF NOT EXISTS issuance_sessions (
    id VARCHAR(36) PRIMARY KEY,
    authorization_code_id VARCHAR(36),
    access_token VARCHAR(255),
    token_type VARCHAR(50) DEFAULT 'Bearer',
    expires_in INT DEFAULT 3600,
    c_nonce VARCHAR(255),
    c_nonce_expires_at TIMESTAMP,
    status ENUM('pending', 'authorized', 'credential_issued', 'expired', 'failed') DEFAULT 'pending',
    user_data JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (authorization_code_id) REFERENCES authorization_codes(id) ON DELETE CASCADE,
    INDEX idx_access_token (access_token),
    INDEX idx_c_nonce (c_nonce),
    INDEX idx_status (status)
);

-- Table for storing issued credentials
CREATE TABLE IF NOT EXISTS issued_credentials (
    id VARCHAR(36) PRIMARY KEY,
    session_id VARCHAR(36),
    credential_id VARCHAR(255),
    credential_type VARCHAR(255) DEFAULT 'jwt_vc',
    doctype VARCHAR(255) DEFAULT 'eu.europa.ec.eudi.pid.1',
    credential_data LONGTEXT, -- Base64 encoded mDoc
    credential_claims JSON,
    issuer_did VARCHAR(255),
    subject_id VARCHAR(255),
    issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP,
    revoked BOOLEAN DEFAULT FALSE,
    revoked_at TIMESTAMP NULL,
    FOREIGN KEY (session_id) REFERENCES issuance_sessions(id) ON DELETE CASCADE,
    INDEX idx_credential_id (credential_id),
    INDEX idx_session_id (session_id),
    INDEX idx_doctype (doctype),
    INDEX idx_subject_id (subject_id),
    INDEX idx_issued_at (issued_at)
);

-- Table for storing issuer keys (simplified for demo)
CREATE TABLE IF NOT EXISTS issuer_keys (
    id VARCHAR(36) PRIMARY KEY,
    key_id VARCHAR(255) NOT NULL UNIQUE,
    key_type VARCHAR(50) NOT NULL, -- 'EC', 'RSA'
    algorithm VARCHAR(50) NOT NULL, -- 'ES256', 'RS256', etc.
    public_key TEXT NOT NULL, -- JWK format
    private_key TEXT NOT NULL, -- JWK format (encrypted in production)
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_key_id (key_id),
    INDEX idx_is_active (is_active)
);
```

Setelah kedua file tersebut ada, buka terminal Anda di root proyek dan jalankan:

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

Perintah ini akan memulai kontainer MySQL di latar belakang, siap digunakan oleh aplikasi
kita.

### 4.2 Mengimplementasikan Library Bersama

Sebelum kita membangun endpoint API, mari kita buat library bersama yang akan menangani
logika bisnis inti. Pendekatan ini menjaga rute API kita tetap bersih dan fokus pada
penanganan permintaan HTTP, sementara pekerjaan kompleks didelegasikan ke modul-modul ini.

#### 4.2.1 Library Database (`src/lib/database.ts`)

File ini adalah satu-satunya sumber kebenaran untuk semua interaksi database. File ini
menggunakan library `mysql2` untuk terhubung ke kontainer MySQL kita dan menyediakan
serangkaian fungsi yang diekspor untuk membuat, membaca, dan memperbarui catatan di tabel
kita. Lapisan abstraksi ini membuat kode kita lebih modular dan lebih mudah dipelihara.

Buat file `src/lib/database.ts` dengan konten berikut:

```typescript
// src/lib/database.ts
import mysql from "mysql2/promise";

// Konfigurasi koneksi database
const dbConfig = {
    host: process.env.DATABASE_HOST || "localhost",
    port: parseInt(process.env.DATABASE_PORT || "3306"),
    user: process.env.DATABASE_USER || "app_user",
    password: process.env.DATABASE_PASSWORD || "app_password",
    database: process.env.DATABASE_NAME || "digital_credentials",
    timezone: "+00:00",
};

let connection: mysql.Connection | null = null;

export async function getConnection(): Promise<mysql.Connection> {
    if (!connection) {
        connection = await mysql.createConnection(dbConfig);
    }
    return connection;
}

// Fungsi Data-Access-Object (DAO) untuk setiap tabel
// ... (misalnya, createChallenge, getChallenge, createAuthorizationCode, dll.)
```

> **Catatan:** Untuk keringkasan, daftar lengkap fungsi DAO telah dihilangkan. Anda dapat
> menemukan kode lengkap di
> [repositori proyek](https://github.com/corbado/digital-credentials-example/blob/main/src/lib/database.ts).
> File ini mencakup fungsi untuk mengelola tantangan, sesi verifikasi, kode otorisasi,
> sesi penerbitan, dan kunci issuer.

#### 4.2.2 Library Kripto (`src/lib/crypto.ts`)

File ini menangani semua operasi kriptografi yang kritis terhadap keamanan. Ini
menggunakan library `jose` untuk menghasilkan pasangan kunci dan menandatangani JSON Web
Tokens (JWTs).

**Pembuatan Kunci** Fungsi `generateIssuerKeyPair` membuat pasangan kunci Elliptic Curve
baru yang akan digunakan untuk menandatangani kredensial. Kunci publik diekspor dalam
format JSON Web Key (JWK) sehingga dapat dipublikasikan di dokumen `did.json` kita.

```typescript
// src/lib/crypto.ts
import { generateKeyPair, exportJWK, SignJWT } from "jose";

export async function generateIssuerKeyPair(keyId: string, issuerDid: string) {
    const { publicKey, privateKey } = await generateKeyPair("ES256", {
        crv: "P-256",
        extractable: true,
    });

    const publicKeyJWK = await exportJWK(publicKey);
    publicKeyJWK.kid = keyId; // Menetapkan ID kunci yang unik

    // ... (ekspor kunci privat dan pengaturan lainnya)

    return { publicKey, privateKey, publicKeyJWK /* ... */ };
}
```

**Pembuatan Kredensial JWT** Fungsi `createJWTVerifiableCredential` adalah inti dari
proses penerbitan. Fungsi ini mengambil klaim pengguna, pasangan kunci issuer, dan
metadata lainnya, dan menggunakannya untuk membuat JWT-VC yang ditandatangani.

```typescript
// src/lib/crypto.ts

export async function createJWTVerifiableCredential(
    claims: MDocClaims,
    issuerKeyPair: IssuerKeyPair,
    subjectId: string,
    audience: string,
): Promise<string> {
    const now = Math.floor(Date.now() / 1000);
    const oneYear = 365 * 24 * 60 * 60;

    const vcPayload = {
        // DID issuer
        iss: issuerKeyPair.issuerDid,
        // DID subjek (pemegang)
        sub: subjectId,
        // Waktu kredensial diterbitkan (iat) dan kapan kedaluwarsa (exp)
        iat: now,
        exp: now + oneYear,
        // Model data Verifiable Credential
        vc: {
            "@context": [
                "https://www.w3.org/2018/credentials/v1",
                "https://europa.eu/eudi/pid/v1",
            ],
            type: ["VerifiableCredential", "eu.europa.ec.eudi.pid.1"],
            issuer: issuerKeyPair.issuerDid,
            issuanceDate: new Date(now * 1000).toISOString(),
            credentialSubject: {
                id: subjectId,
                ...claims,
            },
        },
    };

    // Menandatangani payload dengan kunci privat issuer
    return await new SignJWT(vcPayload)
        .setProtectedHeader({
            alg: issuerKeyPair.algorithm,
            kid: issuerKeyPair.keyId,
            typ: "JWT",
        })
        .sign(issuerKeyPair.privateKey);
}
```

Fungsi ini menyusun payload JWT sesuai dengan W3C
[Verifiable Credentials](https://www.corbado.com/glossary/microcredentials) Data Model dan menandatanganinya
dengan kunci privat issuer, menghasilkan
[verifiable credential](https://www.corbado.com/glossary/verifiable-credential) yang aman.

### 4.2 Tinjauan Arsitektur Aplikasi Next.js

Aplikasi Next.js kami disusun untuk memisahkan urusan antara frontend dan backend,
meskipun mereka adalah bagian dari proyek yang sama. Ini dicapai dengan memanfaatkan App
Router untuk halaman UI dan endpoint API.

- **Frontend (`src/app/issue/page.tsx`):** Satu komponen halaman
  [React](https://www.corbado.com/blog/react-passkeys) yang mendefinisikan UI untuk rute `/issue`. Komponen ini
  menangani input pengguna dan berkomunikasi dengan API backend kami.

- **Backend API Routes (`src/app/api/...`):**
    - **Discovery (`.well-known/.../route.ts`):** Rute ini mengekspos endpoint metadata
      publik yang memungkinkan wallet dan klien lain untuk menemukan kapabilitas dan kunci
      publik issuer.
    - **Issuance (`issue/.../route.ts`):** Endpoint ini mengimplementasikan logika inti
      OpenID4VCI, termasuk membuat penawaran kredensial, menerbitkan token, dan
      menandatangani kredensial akhir.
    - **Schema (`schemas/pid/route.ts`):** Rute ini menyajikan skema JSON untuk
      kredensial, mendefinisikan strukturnya.

- **Library (`src/lib/`):** Direktori ini berisi logika yang dapat digunakan kembali yang
  dibagikan di seluruh backend.
    - `database.ts`: Mengelola semua interaksi database, mengabstraksikan kueri SQL.
    - `crypto.ts`: Menangani semua operasi kriptografi, seperti pembuatan kunci dan
      penandatanganan JWT.

Pemisahan yang jelas ini membuat aplikasi menjadi modular dan lebih mudah untuk
dipelihara.

**Catatan:** Fungsi `generateIssuerDid()` harus mengembalikan `did:web` yang valid yang
cocok dengan domain issuer Anda. Saat di-deploy, `.well-known/did.json` harus disajikan
melalui HTTPS di domain tersebut agar verifier dapat memvalidasi kredensial.

![Gambaran umum arsitektur Aplikasi Next.js](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Mermaid_Chart_Create_complex_visual_diagrams_with_text_A_smarter_way_of_creating_diagrams_2025_07_29_151549_6a0aca6477.svg)

### 4.3 Membangun Frontend

Frontend kita adalah satu halaman React yang menyediakan formulir sederhana bagi pengguna
untuk meminta kredensial digital baru. Tanggung jawabnya adalah untuk:

- Menangkap data pengguna (nama, tanggal lahir, dll.).
- Mengirim data ini ke backend kita untuk membuat penawaran kredensial.
- Menampilkan kode QR dan PIN yang dihasilkan agar pengguna dapat memindainya dengan
  wallet mereka.

Logika inti ditangani dalam fungsi `handleSubmit`, yang dipicu saat pengguna mengirimkan
formulir.

```typescript
// src/app/issue/page.tsx

const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    setCredentialOffer(null);

    try {
        // 1. Validasi bidang yang diperlukan
        if (!userData.given_name || !userData.family_name || !userData.birth_date) {
            throw new Error("Harap isi semua bidang yang diperlukan");
        }

        // 2. Minta penawaran kredensial dari backend
        const response = await fetch("/api/issue/authorize", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                user_data: userData,
            }),
        });

        if (!response.ok) {
            const errorData = await response.json();
            throw new Error(
                errorData.error_description || "Gagal membuat penawaran kredensial",
            );
        }

        // 3. Atur penawaran kredensial di state untuk menampilkan kode QR
        const result = await response.json();
        setCredentialOffer(result);
    } catch (err) {
        const errorMessage =
            (err as Error).message || "Terjadi kesalahan yang tidak diketahui";
        setError(errorMessage);
    } finally {
        setLoading(false);
    }
};
```

Fungsi ini melakukan tiga tindakan utama:

1. **Memvalidasi data formulir** untuk memastikan semua bidang yang diperlukan terisi.
2. **Mengirim permintaan `POST`** ke endpoint `/api/issue/authorize` kita dengan data
   pengguna.
3. **Memperbarui state komponen** dengan penawaran kredensial yang diterima dari backend,
   yang memicu UI untuk menampilkan kode QR dan kode transaksi.

Sisa file berisi kode React standar untuk merender formulir dan tampilan kode QR. Anda
dapat melihat file lengkap di
[repositori proyek](https://github.com/corbado/digital-credentials-example/blob/main/src/app/issue/page.tsx).

### 4.4 Menyiapkan Lingkungan dan Discovery

Sebelum kita membangun API backend, kita perlu mengonfigurasi lingkungan kita dan
menyiapkan endpoint discovery. File `.well-known` ini sangat penting agar wallet dapat
menemukan issuer kita dan memahami cara berinteraksi dengannya.

#### 4.4.1 Membuat File Lingkungan

Buat file bernama `.env.local` di root proyek Anda dan tambahkan baris berikut. URL ini
harus dapat diakses secara publik agar wallet seluler dapat mencapainya. Untuk
pengembangan lokal, Anda dapat menggunakan layanan tunneling seperti
[ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok) untuk mengekspos `localhost` Anda.

```
NEXT_PUBLIC_BASE_URL=http://localhost:3000
```

#### 4.4.2 Mengimplementasikan Endpoint Discovery

Wallet menemukan kapabilitas issuer dengan melakukan query ke URL `.well-known` standar.
Kita perlu membuat tiga endpoint ini.

**1. Metadata Issuer (`/.well-known/openid-credential-issuer`)**

Ini adalah file discovery utama untuk OpenID4VCI. File ini memberi tahu wallet semua yang
perlu diketahuinya tentang issuer, termasuk endpoint-nya, jenis kredensial yang
ditawarkannya, dan algoritma kriptografi yang didukung.

Buat file `src/app/.well-known/openid-credential-issuer/route.ts`:

```typescript
// src/app/.well-known/openid-credential-issuer/route.ts
import { NextResponse } from "next/server";

export async function GET() {
    const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";

    const issuerMetadata = {
        // Pengidentifikasi unik issuer.
        issuer: baseUrl,
        // URL server otorisasi. Untuk kesederhanaan, issuer kami adalah server otorisasi itu sendiri.
        authorization_servers: [baseUrl],
        // URL dari credential issuer.
        credential_issuer: baseUrl,
        // Endpoint tempat wallet akan melakukan POST untuk menerima kredensial yang sebenarnya.
        credential_endpoint: `${baseUrl}/api/issue/credential`,
        // Endpoint tempat wallet menukar kode otorisasi dengan token akses.
        token_endpoint: `${baseUrl}/api/issue/token`,
        // Endpoint untuk alur otorisasi (tidak digunakan dalam alur pra-otorisasi kami, tetapi praktik yang baik untuk disertakan).
        authorization_endpoint: `${baseUrl}/api/issue/authorize`,
        // Menunjukkan dukungan untuk alur kode pra-otorisasi tanpa memerlukan otentikasi klien.
        pre_authorized_grant_anonymous_access_supported: true,
        // Informasi yang dapat dibaca manusia tentang issuer.
        display: [
            {
                name: "Corbado Credentials Issuer",
                locale: "en-US",
            },
        ],
        // Daftar jenis kredensial yang dapat diterbitkan oleh issuer ini.
        credential_configurations_supported: {
            "eu.europa.ec.eudi.pid.1": {
                // Format kredensial (misalnya, jwt_vc, mso_mdoc).
                format: "jwt_vc",
                // Jenis dokumen spesifik, sesuai dengan standar ISO mDoc.
                doctype: "eu.europa.ec.eudi.pid.1",
                // Cakupan OAuth 2.0 yang terkait dengan jenis kredensial ini.
                scope: "eu.europa.ec.eudi.pid.1",
                // Metode yang dapat digunakan wallet untuk membuktikan kepemilikan kuncinya.
                cryptographic_binding_methods_supported: ["jwk"],
                // Algoritma penandatanganan yang didukung issuer untuk kredensial ini.
                credential_signing_alg_values_supported: ["ES256"],
                // Jenis proof-of-possession yang dapat digunakan wallet.
                proof_types_supported: {
                    jwt: {
                        proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"],
                    },
                },
                // Properti tampilan untuk kredensial.
                display: [
                    {
                        name: "Corbado Credential Issuer",
                        locale: "en-US",
                        logo: {
                            uri: `${baseUrl}/logo.png`,
                            alt_text: "EU Digital Identity",
                        },
                        background_color: "#003399",
                        text_color: "#FFFFFF",
                    },
                ],
                // Daftar klaim (atribut) dalam kredensial.
                claims: {
                    "eu.europa.ec.eudi.pid.1": {
                        given_name: {
                            mandatory: true,
                            display: [{ name: "Given Name", locale: "en-US" }],
                        },
                        family_name: {
                            mandatory: true,
                            display: [{ name: "Family Name", locale: "en-US" }],
                        },
                        birth_date: {
                            mandatory: true,
                            display: [{ name: "Date of Birth", locale: "en-US" }],
                        },
                    },
                },
            },
        },
        // Metode otentikasi yang didukung oleh endpoint token. 'none' berarti klien publik.
        token_endpoint_auth_methods_supported: ["none"],
        // Metode tantangan kode PKCE yang didukung.
        code_challenge_methods_supported: ["S256"],
        // Jenis grant OAuth 2.0 yang didukung issuer.
        grant_types_supported: [
            "authorization_code",
            "urn:ietf:params:oauth:grant-type:pre-authorized_code",
        ],
    };

    return NextResponse.json(issuerMetadata, {
        headers: {
            "Content-Type": "application/json",
            "Cache-Control": "no-cache, no-store, must-revalidate",
            Pragma: "no-cache",
            Expires: "0",
        },
    });
}
```

**2. Konfigurasi OpenID (`/.well-known/openid-configuration`)**

Ini adalah dokumen discovery OIDC standar yang menyediakan serangkaian detail konfigurasi
yang lebih luas.

Buat file `src/app/.well-known/openid-configuration/route.ts`:

```typescript
// src/app/.well-known/openid-configuration/route.ts
import { NextResponse } from "next/server";

export async function GET() {
    const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";

    const openidConfiguration = {
        // Pengidentifikasi unik issuer.
        credential_issuer: baseUrl,
        // Endpoint tempat wallet akan melakukan POST untuk menerima kredensial yang sebenarnya.
        credential_endpoint: `${baseUrl}/api/issue/credential`,
        // Endpoint untuk alur otorisasi.
        authorization_endpoint: `${baseUrl}/api/issue/authorize`,
        // Endpoint tempat wallet menukar kode otorisasi dengan token akses.
        token_endpoint: `${baseUrl}/api/issue/token`,
        // Daftar jenis kredensial yang dapat diterbitkan oleh issuer ini.
        credential_configurations_supported: {
            "eu.europa.ec.eudi.pid.1": {
                format: "jwt_vc",
                scope: "eu.europa.ec.eudi.pid.1",
                cryptographic_binding_methods_supported: ["jwk"],
                credential_signing_alg_values_supported: ["ES256", "ES384", "ES512"],
                proof_types_supported: {
                    jwt: {
                        proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"],
                    },
                },
            },
        },
        // Jenis grant OAuth 2.0 yang didukung issuer.
        grant_types_supported: [
            "authorization_code",
            "urn:ietf:params:oauth:grant-type:pre-authorized_code",
        ],
        // Menunjukkan dukungan untuk alur kode pra-otorisasi.
        pre_authorized_grant_anonymous_access_supported: true,
        // Metode tantangan kode PKCE yang didukung.
        code_challenge_methods_supported: ["S256"],
        // Metode otentikasi yang didukung oleh endpoint token.
        token_endpoint_auth_methods_supported: ["none"],
        // Cakupan OAuth 2.0 yang didukung issuer.
        scopes_supported: ["eu.europa.ec.eudi.pid.1"],
    };

    return NextResponse.json(openidConfiguration, {
        headers: {
            "Content-Type": "application/json",
            "Cache-Control": "no-cache, no-store, must-revalidate",
            Pragma: "no-cache",
            Expires: "0",
        },
    });
}
```

**3. Dokumen DID (`/.well-known/did.json`)**

File ini mempublikasikan kunci publik issuer menggunakan metode `did:web`, memungkinkan
siapa saja untuk memverifikasi tanda tangan kredensial yang diterbitkannya.

Buat file `src/app/.well-known/did.json/route.ts`:

```typescript
// src/app/.well-known/did.json/route.ts
import { NextResponse } from "next/server";
import { getActiveIssuerKey } from "../../../lib/database";
import { generateIssuerDid } from "../../../lib/crypto";

export async function GET() {
    const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
    const issuerKey = await getActiveIssuerKey();

    if (!issuerKey) {
        return NextResponse.json(
            { error: "No active issuer key found" },
            { status: 404 },
        );
    }

    const publicKeyJWK = JSON.parse(issuerKey.public_key);
    const didId = generateIssuerDid();
    const didDocument = {
        // Konteks mendefinisikan kosakata yang digunakan dalam dokumen.
        "@context": [
            "https://www.w3.org/ns/did/v1",
            "https://w3id.org/security/suites/jws-2020/v1",
        ],
        // URI DID, yang merupakan pengidentifikasi unik untuk issuer.
        id: didId,
        // Pengontrol DID, yang merupakan entitas yang mengontrol DID. Di sini, itu adalah issuer itu sendiri.
        controller: didId,
        // Daftar kunci publik yang dapat digunakan untuk memverifikasi tanda tangan dari issuer.
        verificationMethod: [
            {
                // Pengidentifikasi unik untuk kunci, yang terlingkup ke DID.
                id: `${didId}#${issuerKey.key_id}`,
                // Jenis kunci.
                type: "JsonWebKey2020",
                // DID dari pengontrol kunci.
                controller: didId,
                // Kunci publik dalam format JWK.
                publicKeyJwk: publicKeyJWK,
            },
        ],
        // Menentukan kunci mana yang dapat digunakan untuk otentikasi (membuktikan kontrol atas DID).
        authentication: [`${didId}#${issuerKey.key_id}`],
        // Menentukan kunci mana yang dapat digunakan untuk membuat kredensial yang dapat diverifikasi.
        assertionMethod: [`${didId}#${issuerKey.key_id}`],
        // Daftar layanan yang disediakan oleh subjek DID, seperti endpoint issuer.
        service: [
            {
                id: `${didId}#openid-credential-issuer`,
                type: "OpenIDCredentialIssuer",
                serviceEndpoint: `${baseUrl}/.well-known/openid-credential-issuer`,
            },
        ],
    };

    return NextResponse.json(didDocument, {
        headers: {
            "Content-Type": "application/did+json",
            "Cache-Control": "no-cache, no-store, must-revalidate",
            Pragma: "no-cache",
            Expires: "0",
        },
    });
}
```

> **Mengapa Tanpa Caching?** Anda akan melihat bahwa ketiga endpoint ini mengembalikan
> header yang secara agresif mencegah caching (`Cache-Control: no-cache`,
> `Pragma: no-cache`, `Expires: 0`). Ini adalah praktik keamanan penting untuk dokumen
> discovery. Konfigurasi issuer dapat berubah—misalnya, kunci kriptografi mungkin
> dirotasi. Jika wallet atau klien menyimpan versi lama dari file `did.json` atau
> `openid-credential-issuer`, ia akan gagal memvalidasi kredensial baru atau berinteraksi
> dengan endpoint yang diperbarui. Dengan memaksa klien untuk mengambil salinan baru pada
> setiap permintaan, kami memastikan mereka selalu memiliki informasi yang paling
> mutakhir.

#### 4.4.3 Mengimplementasikan Endpoint Skema Kredensial

Bagian terakhir dari [infrastruktur](https://www.corbado.com/passkeys-for-critical-infrastructure) publik kita
adalah endpoint skema kredensial. Rute ini menyajikan Skema JSON yang secara formal
mendefinisikan struktur, tipe data, dan batasan dari kredensial PID yang kita terbitkan.
Wallet dan verifier dapat menggunakan skema ini untuk memvalidasi isi kredensial.

Buat file `src/app/api/schemas/pid/route.ts` dengan konten berikut:

```typescript
// src/app/api/schemas/pid/route.ts
import { NextResponse } from "next/server";

export async function GET() {
    const schema = {
        $schema: "https://json-schema.org/draft/2020-12/schema",
        $id: "https://example.com/schemas/pid", // Ganti dengan domain Anda yang sebenarnya
        title: "PID Credential",
        description:
            "Skema untuk Verifiable Credential yang mewakili Dokumen Identifikasi Pribadi (PID).",
        type: "object",
        properties: {
            credentialSubject: {
                type: "object",
                properties: {
                    given_name: { type: "string" },
                    family_name: { type: "string" },
                    birth_date: { type: "string", format: "date" },
                    // ... properti lain dari subjek kredensial
                },
                required: ["given_name", "family_name", "birth_date"],
            },
            // ... properti tingkat atas lainnya dari Verifiable Credential
        },
    };

    return NextResponse.json(schema, {
        headers: {
            "Content-Type": "application/schema+json",
            "Access-Control-Allow-Origin": "*", // Izinkan permintaan lintas-asal
        },
    });
}
```

> **Catatan:** Skema JSON untuk kredensial PID bisa cukup besar dan detail. Untuk
> keringkasan, skema lengkap telah dipotong. Anda dapat menemukan file lengkap di
> [repositori proyek](https://github.com/corbado/digital-credentials-example/blob/main/src/app/api/schemas/pid/route.ts).

### 4.5 Membangun Endpoint Backend

Dengan frontend yang sudah ada, sekarang kita memerlukan logika sisi server untuk
menangani alur OpenID4VCI. Kita akan mulai dengan endpoint pertama yang dipanggil oleh
frontend: `/api/issue/authorize`.

#### 4.5.1 `/api/issue/authorize`: Membuat Penawaran Kredensial

Endpoint ini bertanggung jawab untuk mengambil data pengguna, menghasilkan kode sekali
pakai yang aman, dan membuat `credential_offer` yang dapat dipahami oleh wallet pengguna.

Berikut adalah logika intinya:

```typescript
// src/app/api/issue/authorize/route.ts
import { NextRequest, NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
import { createAuthorizationCode } from "@/lib/database";

export async function POST(request: NextRequest) {
    try {
        const body = await request.json();
        const { user_data } = body;

        // 1. Validasi data pengguna
        if (
            !user_data ||
            !user_data.given_name ||
            !user_data.family_name ||
            !user_data.birth_date
        ) {
            return NextResponse.json({ error: "missing_user_data" }, { status: 400 });
        }

        // 2. Buat kode pra-otorisasi dan PIN
        const code = uuidv4();
        const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 menit
        const txCode = Math.floor(1000 + Math.random() * 9000).toString(); // PIN 4 digit

        // 3. Simpan kode dan data pengguna
        await createAuthorizationCode(uuidv4(), code, expiresAt);
        // Catatan: Ini menggunakan penyimpanan dalam memori hanya untuk tujuan demo.
        // Di produksi, simpan data dengan aman di database dengan kedaluwarsa yang tepat.
        if (!(global as any).userDataStore) (global as any).userDataStore = new Map();
        (global as any).userDataStore.set(code, user_data);
        if (!(global as any).txCodeStore) (global as any).txCodeStore = new Map();
        (global as any).txCodeStore.set(code, txCode);

        // 4. Buat objek penawaran kredensial
        const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
        const credentialOffer = {
            // Pengidentifikasi issuer, yang merupakan URL dasarnya.
            credential_issuer: baseUrl,
            // Array jenis kredensial yang ditawarkan oleh issuer.
            credential_configuration_ids: ["eu.europa.ec.eudi.pid.1"],
            // Menentukan jenis grant yang dapat digunakan oleh wallet.
            grants: {
                // Kita menggunakan alur kode pra-otorisasi.
                "urn:ietf:params:oauth:grant-type:pre-authorized_code": {
                    // Kode sekali pakai yang akan ditukar oleh wallet dengan token.
                    "pre-authorized_code": code,
                    // Menunjukkan bahwa pengguna harus memasukkan PIN (tx_code) untuk menukarkan kode.
                    user_pin_required: true,
                },
            },
        };

        // 5. Buat URI penawaran kredensial lengkap (deep link untuk wallet)
        const credentialOfferUri = `openid-credential-offer://?credential_offer=${encodeURIComponent(
            JSON.stringify(credentialOffer),
        )}`;

        // Respons akhir ke frontend.
        return NextResponse.json({
            // Deep link untuk kode QR.
            credential_offer_uri: credentialOfferUri,
            // Kode pra-otorisasi mentah, untuk ditampilkan atau entri manual.
            pre_authorized_code: code,
            // PIN 4 digit yang harus dimasukkan pengguna di wallet mereka.
            tx_code: txCode,
        });
    } catch (error) {
        console.error("Authorization error:", error);
        return NextResponse.json({ error: "server_error" }, { status: 500 });
    }
}
```

Langkah-langkah kunci di endpoint ini:

1. **Validasi Data:** Pertama, memastikan data pengguna yang diperlukan ada.
2. **Buat Kode:** Membuat `pre-authorized_code` unik (sebuah UUID) dan `tx_code` 4 digit
   (PIN) untuk lapisan keamanan tambahan.
3. **Simpan Data:** `pre-authorized_code` disimpan di database dengan waktu kedaluwarsa
   yang singkat. Data pengguna dan PIN disimpan di memori, terkait dengan kode tersebut.
4. **Bangun Penawaran:** Membangun objek `credential_offer` sesuai dengan spesifikasi
   OpenID4VCI. Objek ini memberi tahu wallet di mana issuer berada, kredensial apa yang
   ditawarkannya, dan kode yang diperlukan untuk mendapatkannya.
5. **Kembalikan URI:** Akhirnya, membuat URI deep link (`openid-credential-offer://...`)
   dan mengembalikannya ke frontend, bersama dengan `tx_code` agar dapat dilihat oleh
   pengguna.

#### 4.5.2 `/api/issue/token`: Menukarkan Kode dengan Token

Setelah pengguna memindai kode QR dan memasukkan PIN mereka, wallet membuat permintaan
`POST` ke endpoint ini. Tugasnya adalah memvalidasi `pre-authorized_code` dan `user_pin`
(PIN), dan jika valid, menerbitkan [access token](https://www.corbado.com/glossary/access-token) yang berumur
pendek.

Buat file `src/app/api/issue/token/route.ts` dengan konten berikut:

```typescript
// src/app/api/issue/token/route.ts
import { NextRequest, NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
import {
    getAuthorizationCode,
    markAuthorizationCodeAsUsed,
    createIssuanceSession,
} from "@/lib/database";

export async function POST(request: NextRequest) {
    try {
        const formData = await request.formData();
        const grant_type = formData.get("grant_type") as string;
        const code = formData.get("pre-authorized_code") as string;
        const user_pin = formData.get("user_pin") as string;

        // 1. Validasi jenis grant
        if (grant_type !== "urn:ietf:params:oauth:grant-type:pre-authorized_code") {
            return NextResponse.json(
                { error: "unsupported_grant_type" },
                { status: 400 },
            );
        }

        // 2. Validasi kode pra-otorisasi
        const authCode = await getAuthorizationCode(code);
        if (!authCode) {
            return NextResponse.json(
                {
                    error: "invalid_grant",
                    error_description: "Kode tidak valid atau kedaluwarsa",
                },
                { status: 400 },
            );
        }

        // 3. Validasi PIN (tx_code)
        const expectedTxCode = (global as any).txCodeStore?.get(code);
        if (expectedTxCode !== user_pin) {
            return NextResponse.json(
                { error: "invalid_grant", error_description: "PIN tidak valid" },
                { status: 400 },
            );
        }

        // 4. Buat token akses dan c_nonce
        const accessToken = uuidv4();
        const cNonce = uuidv4();
        const cNonceExpiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 menit

        // 5. Buat sesi penerbitan baru
        const userData = (global as any).userDataStore?.get(code);
        await createIssuanceSession(
            uuidv4(),
            authCode.id,
            accessToken,
            cNonce,
            cNonceExpiresAt,
            userData,
        );

        // 6. Tandai kode sebagai sudah digunakan dan bersihkan data sementara
        await markAuthorizationCodeAsUsed(code);
        (global as any).txCodeStore?.delete(code);
        (global as any).userDataStore?.delete(code);

        // 7. Kembalikan respons token akses
        return NextResponse.json({
            access_token: accessToken,
            token_type: "Bearer",
            expires_in: 3600, // 1 jam
            c_nonce: cNonce,
            c_nonce_expires_in: 300, // 5 menit
        });
    } catch (error) {
        console.error("Token endpoint error:", error);
        return NextResponse.json({ error: "server_error" }, { status: 500 });
    }
}
```

Langkah-langkah kunci di endpoint ini:

1. **Validasi Jenis Grant:** Memastikan wallet menggunakan jenis grant
   `pre-authorized_code` yang benar.
2. **Validasi Kode:** Memeriksa bahwa `pre-authorized_code` ada di database, belum
   kedaluwarsa, dan belum pernah digunakan sebelumnya.
3. **Validasi PIN:** Membandingkan `user_pin` dari wallet dengan `tx_code` yang kita
   simpan sebelumnya untuk memastikan pengguna mengotorisasi transaksi.
4. **Buat Token:** Membuat `access_token` yang aman dan `c_nonce` (credential nonce), yang
   merupakan nilai sekali pakai untuk mencegah serangan replay pada endpoint kredensial.
5. **Buat Sesi:** Membuat catatan `issuance_sessions` baru di database, menghubungkan
   [access token](https://www.corbado.com/glossary/access-token) dengan data pengguna.
6. **Tandai Kode sebagai Telah Digunakan:** Untuk mencegah penawaran yang sama digunakan
   dua kali, ini menandai `pre-authorized_code` sebagai telah digunakan.
7. **Kembalikan Token:** Mengembalikan `access_token` dan `c_nonce` ke wallet.

#### 4.5.3 `/api/issue/credential`: Menerbitkan Kredensial yang Ditandatangani

Ini adalah endpoint terakhir dan terpenting. Wallet menggunakan token akses yang
diterimanya dari endpoint `/token` untuk membuat permintaan `POST` yang diautentikasi ke
rute ini. Tugas endpoint ini adalah melakukan validasi akhir, membuat kredensial yang
ditandatangani secara kriptografis, dan mengembalikannya ke wallet.

Buat file `src/app/api/issue/credential/route.ts` dengan konten berikut:

```typescript
// src/app/api/issue/credential/route.ts
import { NextRequest, NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
import {
    getIssuanceSessionByToken,
    updateIssuanceSession,
    createIssuedCredential,
    getActiveIssuerKey,
} from "@/lib/database";
import {
    createJWTVerifiableCredential,
    importIssuerKeyPair,
    generateIssuerDid,
} from "@/lib/crypto";

export async function POST(request: NextRequest) {
    try {
        // 1. Validasi token Bearer
        const authHeader = request.headers.get("authorization");
        const accessToken = authHeader?.substring(7);
        const session = await getIssuanceSessionByToken(accessToken);

        if (!session) {
            return NextResponse.json({ error: "invalid_token" }, { status: 401 });
        }

        // 2. Dapatkan data pengguna dari sesi
        const userData = session.user_data;
        if (!userData) {
            return NextResponse.json({ error: "missing_user_data" }, { status: 400 });
        }

        // 3. Dapatkan kunci issuer yang aktif
        const issuerKey = await getActiveIssuerKey();
        if (!issuerKey) {
            // Dalam aplikasi nyata, Anda akan memiliki sistem manajemen kunci yang lebih kuat.
            // Untuk demo ini, kita dapat membuat kunci secara on-the-fly jika tidak ada.
            // Bagian ini dihilangkan untuk keringkasan tetapi ada di repositori.
            return NextResponse.json(
                {
                    error: "server_error",
                    error_description: "Gagal mendapatkan kunci issuer",
                },
                { status: 500 },
            );
        }

        // 4. Buat JWT-VC
        const issuerDid = generateIssuerDid();
        const keyPair = await importIssuerKeyPair(
            issuerKey.key_id,
            issuerKey.public_key,
            issuerKey.private_key,
            issuerDid,
        );
        const subjectId = `did:example:${uuidv4()}`;
        const credentialData = await createJWTVerifiableCredential(
            userData,
            keyPair,
            subjectId,
            process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000",
        );

        // 5. Simpan kredensial yang diterbitkan di database
        await createIssuedCredential(/* ... detail kredensial ... */);
        await updateIssuanceSession(session.id, "credential_issued");

        // 6. Kembalikan kredensial yang ditandatangani
        return NextResponse.json({
            format: "jwt_vc",
            credential: credentialData,
            c_nonce: uuidv4(), // Nonce baru untuk permintaan berikutnya
            c_nonce_expires_in: 300,
        });
    } catch (error) {
        console.error("Credential endpoint error:", error);
        return NextResponse.json({ error: "server_error" }, { status: 500 });
    }
}
```

Langkah-langkah kunci di endpoint ini:

1. **Validasi Token:** Memeriksa token `Bearer` yang valid di header `Authorization` dan
   menggunakannya untuk mencari sesi penerbitan yang aktif.
2. **Ambil Data Pengguna:** Mengambil data klaim pengguna, yang disimpan di sesi saat
   token dibuat.
3. **Muat Kunci Issuer:** Memuat kunci penandatanganan aktif issuer dari database. Dalam
   skenario dunia nyata, ini akan dikelola oleh sistem manajemen kunci yang aman.
4. **Buat Kredensial:** Memanggil helper `createJWTVerifiableCredential` kita dari
   `src/lib/crypto.ts` untuk membangun dan menandatangani JWT-VC.
5. **Catat Penerbitan:** Menyimpan catatan kredensial yang diterbitkan di database untuk
   tujuan audit dan pencabutan.
6. **Kembalikan Kredensial:** Mengembalikan kredensial yang ditandatangani ke wallet dalam
   respons JSON. Wallet kemudian bertanggung jawab untuk menyimpannya dengan aman.

## 5. Menjalankan Issuer dan Langkah Selanjutnya

Anda sekarang memiliki implementasi issuer kredensial digital yang lengkap dari ujung ke
ujung. Berikut cara menjalankannya secara lokal dan apa yang perlu Anda pertimbangkan
untuk membawanya dari bukti konsep menjadi aplikasi siap produksi.

### 5.1 Cara Menjalankan Contoh

1. **Kloning Repositori:**

    ```bash
    git clone https://github.com/corbado/digital-credentials-example.git
    cd digital-credentials-example
    ```

2. **Instal Dependensi:**

    ```bash
    npm install
    ```

3. **Mulai Database:** Pastikan Docker berjalan, lalu mulai kontainer MySQL:

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

4. **Konfigurasi Lingkungan & Jalankan Tunnel:** Ini adalah langkah paling penting untuk
   pengujian lokal. Karena wallet seluler Anda perlu terhubung ke mesin pengembangan Anda
   melalui internet, Anda harus mengekspos server lokal Anda dengan URL HTTPS publik. Kita
   akan menggunakan `ngrok` untuk ini.

    a. **Mulai ngrok:**

    ```bash
    ngrok http 3000
    ```

    b. **Salin URL HTTPS** dari output
    [ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok) (misalnya,
    `https://random-string.ngrok.io`). c. **Buat file `.env.local`** dan atur URL:

    ```
    NEXT_PUBLIC_BASE_URL=https://<your-ngrok-url>
    ```

5. **Jalankan Aplikasi:**

    ```bash
    npm run dev
    ```

    Buka browser Anda ke `http://localhost:3000/issue`. Anda sekarang dapat mengisi
    formulir, dan kode QR yang dihasilkan akan menunjuk dengan benar ke URL
    [ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok) publik Anda, memungkinkan
    wallet seluler Anda untuk terhubung dan menerima kredensial.

### 5.2 Pentingnya HTTPS dan `ngrok`

Protokol kredensial digital dibangun dengan keamanan sebagai prioritas utama. Karena
alasan ini, wallet hampir selalu menolak untuk terhubung ke issuer melalui koneksi yang
tidak aman (`http://`). Seluruh proses bergantung pada koneksi **HTTPS** yang aman, yang
diaktifkan oleh **sertifikat SSL**.

Layanan terowongan seperti `ngrok` menyelesaikan kedua masalah ini dengan membuat URL
HTTPS yang aman dan menghadap ke publik (dengan sertifikat SSL yang valid) yang meneruskan
semua lalu lintas ke server pengembangan lokal Anda. Wallet memerlukan HTTPS dan akan
menolak untuk terhubung ke endpoint yang tidak aman (`http://`). Ini adalah alat penting
untuk menguji layanan web apa pun yang perlu berinteraksi dengan perangkat seluler atau
webhook eksternal.

### 5.3 Apa yang di Luar Cakupan Tutorial Ini

Contoh ini sengaja difokuskan pada alur penerbitan inti agar mudah dipahami. Topik-topik
berikut dianggap di luar cakupan:

- **Keamanan Siap Produksi:** Issuer ini untuk tujuan pendidikan. Sistem produksi akan
  memerlukan Sistem Manajemen Kunci (KMS) yang aman alih-alih menyimpan kunci di database,
  penanganan kesalahan yang kuat, pembatasan laju, dan pencatatan audit yang komprehensif.
- **Pencabutan Kredensial:** Panduan ini tidak mengimplementasikan mekanisme untuk
  mencabut kredensial yang diterbitkan. Meskipun skema menyertakan flag `revoked` untuk
  penggunaan di masa mendatang, tidak ada logika pencabutan yang disediakan di sini.
- **Alur Kode Otorisasi:** Kami fokus secara eksklusif pada alur `pre-authorized_code`.
  Implementasi penuh dari alur `authorization_code` akan memerlukan layar persetujuan
  pengguna dan logika [OAuth 2.0](https://www.corbado.com/glossary/oauth2) yang lebih kompleks.
- **Manajemen Pengguna:** Panduan ini tidak menyertakan otentikasi atau manajemen pengguna
  apa pun untuk issuer itu sendiri. Diasumsikan bahwa pengguna sudah diautentikasi dan
  diizinkan untuk menerima kredensial.

## 6. Kesimpulan

Selesai! Dengan beberapa halaman kode, kita sekarang memiliki issuer kredensial digital
yang lengkap dari ujung ke ujung yang:

1. Menyediakan frontend yang ramah pengguna untuk meminta kredensial.
2. Mengimplementasikan alur `pre-authorized_code` OpenID4VCI penuh.
3. Mengekspos semua endpoint discovery yang diperlukan untuk interoperabilitas wallet.
4. Menghasilkan dan menandatangani
   JWT-[Verifiable Credential](https://www.corbado.com/glossary/verifiable-credential) yang aman dan sesuai
   standar.

Meskipun panduan ini memberikan dasar yang kuat, issuer yang siap produksi akan memerlukan
fitur tambahan seperti manajemen kunci yang kuat, penyimpanan persisten alih-alih
penyimpanan dalam memori, pencabutan kredensial, dan pengerasan keamanan yang
komprehensif. Kompatibilitas wallet juga bervariasi; Sphereon Wallet direkomendasikan
untuk pengujian, tetapi wallet lain mungkin tidak mendukung alur pra-otorisasi seperti
yang diimplementasikan di sini. Namun, blok bangunan inti dan alur interaksi akan tetap
sama. Dengan mengikuti pola-pola ini, Anda dapat membangun issuer yang aman dan dapat
dioperasikan untuk semua jenis kredensial digital.

## 7. Sumber Daya

Berikut adalah beberapa sumber daya utama, spesifikasi, dan alat yang digunakan atau
dirujuk dalam tutorial ini:

- **Repositori Proyek:**
    - [Kode Sumber Lengkap di GitHub](https://github.com/corbado/digital-credentials-example)

- **Spesifikasi Kunci:**
    - [OpenID for Verifiable Credential Issuance (OpenID4VCI)](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html):
      Protokol penerbitan inti.
    - [W3C Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/):
      Standar dasar untuk VC.
    - [Metode `did:web`](https://w3c-ccg.github.io/did-method-web/): Metode DID yang
      digunakan untuk kunci publik issuer kami.

- **Alat:**
    - [Sphereon Wallet](https://sphereon.com/wallet/): Wallet uji coba yang digunakan
      dalam panduan ini.
    - ngrok: Untuk membuat terowongan aman ke lingkungan pengembangan lokal Anda.

- **Library:**
    - Next.js: Framework React untuk membangun frontend dan backend.
    - [jose](https://github.com/panva/jose): Untuk membuat dan menandatangani JSON Web
      Tokens (JWTs).
    - [mysql2](https://github.com/sidorares/node-mysql2): Klien MySQL untuk
      [Node.js](https://www.corbado.com/blog/nodejs-passkeys).
