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
See the original blog version in English here.
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:
Recent Articles
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:
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.
Kunci di balik kredensial digital terletak pada model "segitiga kepercayaan" (trust triangle) yang sederhana namun kuat, yang melibatkan tiga pemain utama:
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.
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.
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 / 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 | 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 | 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. |
OpenID4VCI mendukung dua alur otorisasi utama untuk menerbitkan kredensial:
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.
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.
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.
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.
Next.js adalah framework pilihan kita karena menyediakan pengalaman yang mulus dan terintegrasi untuk membangun aplikasi full-stack.
Implementasi kita akan mengandalkan beberapa library kunci untuk menangani tugas-tugas spesifik:
pre-authorized_code
.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:
Menerbitkan kredensial adalah operasi yang sangat penting bagi keamanan yang bergantung pada konsep kriptografi dasar untuk memastikan kepercayaan dan keaslian.
Pada intinya, Verifiable Credential adalah sekumpulan klaim yang telah ditandatangani secara digital oleh Issuer. Tanda tangan ini memberikan dua jaminan:
Tanda tangan digital dibuat menggunakan kriptografi kunci publik/privat. Begini cara kerjanya:
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.
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.
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.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.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:
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
Pertama, kita akan menginisialisasi proyek Next.js baru, menginstal dependensi yang diperlukan, dan memulai database kita.
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.
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
.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.
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.
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.
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.
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/...
):
.well-known/.../route.ts
): Rute ini mengekspos endpoint metadata
publik yang memungkinkan wallet dan klien lain untuk menemukan kapabilitas dan kunci
publik issuer.issue/.../route.ts
): Endpoint ini mengimplementasikan logika inti
OpenID4VCI, termasuk membuat penawaran kredensial, menerbitkan token, dan
menandatangani kredensial akhir.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.
Frontend kita adalah satu halaman React yang menyediakan formulir sederhana bagi pengguna untuk meminta kredensial digital baru. Tanggung jawabnya adalah untuk:
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:
POST
ke endpoint /api/issue/authorize
kita dengan data
pengguna.Sisa file berisi kode React standar untuk merender formulir dan tampilan kode QR. Anda dapat melihat file lengkap di repositori proyek.
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.
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
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.
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.
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
.
/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:
pre-authorized_code
unik (sebuah UUID) dan tx_code
4 digit
(PIN) untuk lapisan keamanan tambahan.pre-authorized_code
disimpan di database dengan waktu kedaluwarsa
yang singkat. Data pengguna dan PIN disimpan di memori, terkait dengan kode tersebut.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.openid-credential-offer://...
)
dan mengembalikannya ke frontend, bersama dengan tx_code
agar dapat dilihat oleh
pengguna./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:
pre-authorized_code
yang benar.pre-authorized_code
ada di database, belum
kedaluwarsa, dan belum pernah digunakan sebelumnya.user_pin
dari wallet dengan tx_code
yang kita
simpan sebelumnya untuk memastikan pengguna mengotorisasi transaksi.access_token
yang aman dan c_nonce
(credential nonce), yang
merupakan nilai sekali pakai untuk mencegah serangan replay pada endpoint kredensial.issuance_sessions
baru di database, menghubungkan
access token dengan data pengguna.pre-authorized_code
sebagai telah digunakan.access_token
dan c_nonce
ke wallet./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:
Bearer
yang valid di header Authorization
dan
menggunakannya untuk mencari sesi penerbitan yang aktif.createJWTVerifiableCredential
kita dari
src/lib/crypto.ts
untuk membangun dan menandatangani JWT-VC.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.
Kloning Repositori:
git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
Instal Dependensi:
npm install
Mulai Database: Pastikan Docker berjalan, lalu mulai kontainer MySQL:
docker-compose up -d
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>
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.
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.
Contoh ini sengaja difokuskan pada alur penerbitan inti agar mudah dipahami. Topik-topik berikut dianggap di luar cakupan:
revoked
untuk
penggunaan di masa mendatang, tidak ada logika pencabutan yang disediakan di sini.pre-authorized_code
.
Implementasi penuh dari alur authorization_code
akan memerlukan layar persetujuan
pengguna dan logika OAuth 2.0 yang lebih kompleks.Selesai! Dengan beberapa halaman kode, kita sekarang memiliki issuer kredensial digital yang lengkap dari ujung ke ujung yang:
pre-authorized_code
OpenID4VCI penuh.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.
Berikut adalah beberapa sumber daya utama, spesifikasi, dan alat yang digunakan atau dirujuk dalam tutorial ini:
Repositori Proyek:
Spesifikasi Kunci:
did:web
: Metode DID yang
digunakan untuk kunci publik issuer kami.Alat:
Library:
Related Articles
Table of Contents