Get your free and exclusive 80-page Banking Passkey Report
Back to Overview

Cara Membangun Issuer Kredensial Digital (Panduan Developer)

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

Amine

Created: August 20, 2025

Updated: August 21, 2025

Blog-Post-Header-Image

See the original blog version in English here.

DigitalCredentialsDemo Icon

Want to experience digital credentials in action?

Try Digital Credentials

1. Pendahuluan#

Kredensial Digital 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 adalah entitas tepercaya—seperti lembaga pemerintah, universitas, atau bank—yang bertanggung jawab untuk membuat dan mendistribusikan kredensial yang ditandatangani secara digital kepada pengguna.

Panduan ini menyediakan tutorial langkah demi langkah yang komprehensif untuk membangun sebuah Issuer Kredensial Digital. Kita akan fokus pada protokol OpenID for Verifiable Credential Issuance (OpenID4VCI), sebuah standar modern yang mendefinisikan cara pengguna dapat memperoleh kredensial dari Issuer dan menyimpannya dengan aman di wallet digital mereka.

Hasil akhirnya adalah sebuah aplikasi Next.js 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 agar pengguna dapat memindainya dengan wallet 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 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 kriptografis.

  • Verifiable Credentials (VCs - Standar W3C): Ini adalah jenis kredensial digital spesifik yang mengikuti standar W3C Verifiable Credentials 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 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.

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, 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 digital pribadi di perangkat mereka.
  • Verifier: Aplikasi atau layanan yang perlu memeriksa kredensial pengguna.

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.

Langkah 2: Pembuatan Penawaran Kredensial Aplikasi menghasilkan penawaran kredensial yang aman, ditampilkan sebagai kode QR dan kode pra-otorisasi.

Langkah 3: Interaksi Wallet Pengguna memindai kode QR dengan wallet yang kompatibel (misalnya, Sphereon Wallet) dan memasukkan PIN untuk mengotorisasi penerbitan.

Langkah 4: Kredensial Diterbitkan Wallet menerima dan menyimpan kredensial digital yang baru diterbitkan, siap untuk digunakan di masa mendatang.

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 ini, kita akan fokus pada hal berikut:

Standar / ProtokolDeskripsi
OpenID4VCIOpenID 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-VCJWT-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 mDocISO/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.0Kerangka 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 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, 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 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.

  • Untuk Frontend: Kita akan menggunakan Next.js dengan React 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, 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 kita.
  • mysql2: Klien MySQL untuk Node.js, 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 ini, kami merekomendasikan Sphereon Wallet, yang tersedia untuk Android dan iOS.

Cara Menginstal Sphereon Wallet:

  1. Unduh wallet dari Google Play Store atau Apple App Store.
  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 yang bergantung pada konsep kriptografi dasar untuk memastikan kepercayaan dan keaslian.

2.4.1 Tanda Tangan Digital#

Pada intinya, 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/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, 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 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.
    • 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:

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.

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.

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.

npm install jose mysql2 uuid @types/uuid

Perintah ini menginstal:

  • jose: Untuk menandatangani dan memverifikasi JSON Web Tokens (JWTs).
  • mysql2: Klien MySQL 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 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:

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:

-- 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:

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:

// 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. 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.

// 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.

// 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 Data Model dan menandatanganinya dengan kunci privat issuer, menghasilkan 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 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.

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.

// 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.

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 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:

// 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:

// 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:

// 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 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:

// 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.

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:

// 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 yang berumur pendek.

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

// 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 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:

// 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:

    git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
  2. Instal Dependensi:

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

    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:

    ngrok http 3000

    b. Salin URL HTTPS dari output 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:

    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 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 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 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:

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start Free Trial

Share this article


LinkedInTwitterFacebook