Tìm hiểu cách xây dựng trình cấp Thông tin xác thực có thể xác minh (Verifiable Credential) theo chuẩn W3C bằng giao thức OpenID4VCI. Hướng dẫn từng bước này chỉ cho bạn cách tạo một ứng dụng Next.js có khả năng cấp các thông tin xác thực được ký mã hóa v
Amine
Created: August 20, 2025
Updated: August 21, 2025
See the original blog version in English here.
Thông tin xác thực kỹ thuật số là một cách mạnh mẽ để chứng minh danh tính và các xác nhận một cách an toàn và bảo vệ quyền riêng tư. Nhưng làm thế nào để người dùng có được những thông tin xác thực này? Đây là lúc vai trò của Issuer (Trình cấp) trở nên quan trọng. Một Issuer là một thực thể đáng tin cậy—chẳng hạn như một cơ quan chính phủ, một trường đại học, hoặc một ngân hàng—chịu trách nhiệm tạo và phân phối các thông tin xác thực được ký điện tử cho người dùng.
Hướng dẫn này cung cấp một bài học toàn diện, từng bước để xây dựng một Issuer Thông tin xác thực kỹ thuật số. Chúng ta sẽ tập trung vào giao thức OpenID for Verifiable Credential Issuance (OpenID4VCI), một tiêu chuẩn hiện đại định nghĩa cách người dùng có thể nhận thông tin xác thực từ một Issuer và lưu trữ chúng an toàn trong ví kỹ thuật số của họ.
Kết quả cuối cùng sẽ là một ứng dụng Next.js hoạt động được, có khả năng:
Recent Articles
📝
Cách xây dựng Trình xác minh thông tin xác thực kỹ thuật số (Hướng dẫn cho nhà phát triển)
📝
Hướng dẫn xây dựng Trình cấp Thông tin xác thực kỹ thuật số (Dành cho Lập trình viên)
📖
Khóa Thường Trú WebAuthn: Thông Tin Xác Thực Có Thể Khám Phá Dưới Dạng Passkey
🔑
Truy Cập Bằng Thẻ Vật Lý & Passkeys: Hướng Dẫn Kỹ Thuật
🔑
Bắt buộc MFA & Hướng tới Passkeys: Các phương pháp hay nhất
Trước khi tiếp tục, điều quan trọng là phải làm rõ sự khác biệt giữa hai khái niệm liên quan nhưng khác nhau:
Digital Credentials (Thông tin xác thực kỹ thuật số - Thuật ngữ chung): Đây là một danh mục rộng bao gồm mọi hình thức kỹ thuật số của thông tin xác thực, chứng chỉ, hoặc chứng thực. Chúng có thể bao gồm các chứng chỉ kỹ thuật số đơn giản, huy hiệu kỹ thuật số cơ bản, hoặc bất kỳ thông tin xác thực nào được lưu trữ điện tử mà có thể có hoặc không có các tính năng bảo mật mã hóa.
Verifiable Credentials (VCs - Tiêu chuẩn W3C): Đây là một loại thông tin xác thực kỹ thuật số cụ thể tuân theo tiêu chuẩn Mô hình Dữ liệu Thông tin xác thực có thể xác minh của W3C. Verifiable Credentials là các thông tin xác thực được ký mã hóa, chống giả mạo và tôn trọng quyền riêng tư, có thể được xác minh một cách độc lập. Chúng bao gồm các yêu cầu kỹ thuật cụ thể như:
Trong hướng dẫn này, chúng ta sẽ xây dựng một trình cấp Verifiable Credential tuân thủ tiêu chuẩn W3C, chứ không chỉ là một hệ thống thông tin xác thực kỹ thuật số bất kỳ. Giao thức OpenID4VCI mà chúng ta đang sử dụng được thiết kế đặc biệt để cấp Verifiable Credentials, và định dạng JWT-VC chúng ta sẽ triển khai là một định dạng tuân thủ W3C cho Verifiable Credentials.
Điều kỳ diệu đằng sau thông tin xác thực kỹ thuật số nằm ở mô hình "tam giác tin cậy" đơn giản nhưng mạnh mẽ, liên quan đến ba bên chính:
Luồng cấp phát là bước đầu tiên trong hệ sinh thái này. Issuer xác thực thông tin của người dùng và cung cấp cho họ một thông tin xác thực. Một khi Holder có thông tin xác thực này trong ví của họ, họ có thể trình bày nó cho một Verifier để chứng minh danh tính hoặc các xác nhận của mình, hoàn thành tam giác.
Đây là cái nhìn nhanh về ứng dụng cuối cùng khi hoạt động:
Bước 1: Nhập dữ liệu người dùng Người dùng điền vào một biểu mẫu với thông tin cá nhân của họ để yêu cầu một thông tin xác thực mới.
Bước 2: Tạo đề nghị cấp thông tin xác thực Ứng dụng tạo ra một đề nghị cấp thông tin xác thực an toàn, được hiển thị dưới dạng mã QR và một mã được ủy quyền trước.
Bước 3: Tương tác với Ví Người dùng quét mã QR bằng một ví tương thích (ví dụ: Sphereon Wallet) và nhập mã PIN để ủy quyền việc cấp phát.
Bước 4: Thông tin xác thực được cấp Ví nhận và lưu trữ thông tin xác thực kỹ thuật số mới được cấp, sẵn sàng cho việc sử dụng trong tương lai.
Trước khi đi sâu vào code, hãy cùng tìm hiểu các kiến thức nền tảng và công cụ bạn sẽ cần. Hướng dẫn này giả định bạn đã quen thuộc cơ bản với các khái niệm phát triển web, nhưng các yêu cầu tiên quyết sau đây là cần thiết để xây dựng một trình cấp thông tin xác thực.
Issuer của chúng ta được xây dựng trên một tập hợp các tiêu chuẩn mở đảm bảo khả năng tương tác giữa các ví và dịch vụ cấp phát. Trong bài hướng dẫn này, chúng ta sẽ tập trung vào các tiêu chuẩn sau:
Tiêu chuẩn / Giao thức | Mô tả |
---|---|
OpenID4VCI | OpenID for Verifiable Credential Issuance. Đây là giao thức cốt lõi chúng ta sẽ sử dụng. Nó định nghĩa một luồng chuẩn về cách người dùng (thông qua ví của họ) có thể yêu cầu và nhận một thông tin xác thực từ một Issuer. |
JWT-VC | JWT-based Verifiable Credentials. Định dạng cho thông tin xác thực chúng ta sẽ cấp. Đây là một tiêu chuẩn W3C mã hóa các thông tin xác thực có thể xác minh dưới dạng JSON Web Tokens (JWTs), giúp chúng nhỏ gọn và thân thiện với web. |
ISO mDoc | ISO/IEC 18013-5. Tiêu chuẩn quốc tế cho Bằng lái xe di động (mDLs). Mặc dù chúng ta cấp một JWT-VC, các claims bên trong nó được cấu trúc để tương thích với mô hình dữ liệu mDoc (ví dụ: eu.europa.ec.eudi.pid.1 ). |
OAuth 2.0 | Framework ủy quyền nền tảng được OpenID4VCI sử dụng. Chúng ta sẽ triển khai luồng pre-authorized_code , một loại grant type cụ thể được thiết kế để cấp thông tin xác thực an toàn và thân thiện với người dùng. |
OpenID4VCI hỗ trợ hai luồng ủy quyền chính để cấp thông tin xác thực:
Luồng Pre-Authorized Code: Trong luồng này, Issuer tạo ra một mã ngắn hạn, sử dụng
một lần (pre-authorized_code
) và cung cấp ngay cho người dùng. Ví của người dùng sau
đó có thể đổi mã này trực tiếp để lấy thông tin xác thực. Luồng này lý tưởng cho các
tình huống mà người dùng đã được xác thực và đang có mặt trên trang web của
Issuer, vì nó mang lại trải nghiệm cấp phát liền mạch, tức thì mà
không cần chuyển hướng.
Luồng Authorization Code: Đây là luồng OAuth 2.0 tiêu chuẩn,
trong đó người dùng được chuyển hướng đến một máy chủ ủy quyền để cấp quyền. Sau khi
phê duyệt, máy chủ gửi một authorization_code
trở lại một redirect_uri
đã đăng ký.
Luồng này phù hợp hơn cho các ứng dụng của bên thứ ba khởi tạo quy trình cấp phát thay
mặt người dùng.
Trong bài hướng dẫn này, chúng ta sẽ sử dụng luồng pre-authorized_code
. Chúng tôi
chọn cách tiếp cận này vì nó đơn giản hơn và cung cấp trải nghiệm người dùng trực tiếp hơn
cho trường hợp sử dụng cụ thể của chúng ta: một người dùng trực tiếp yêu cầu một thông tin
xác thực từ trang web của chính Issuer. Nó loại bỏ sự cần thiết của
các chuyển hướng phức tạp và đăng ký client, làm cho logic cấp phát cốt lõi dễ hiểu và
triển khai hơn.
Sự kết hợp các tiêu chuẩn này cho phép chúng ta xây dựng một issuer tương thích với nhiều loại ví kỹ thuật số và đảm bảo một quy trình an toàn, được tiêu chuẩn hóa cho người dùng.
Để xây dựng issuer của mình, chúng ta sẽ sử dụng cùng một bộ công nghệ mạnh mẽ và hiện đại mà chúng ta đã sử dụng cho verifier, đảm bảo một trải nghiệm phát triển nhất quán và chất lượng cao.
Chúng ta sẽ sử dụng TypeScript cho cả code frontend và backend. Kiểu tĩnh của nó là vô giá trong một ứng dụng quan trọng về bảo mật như một issuer, vì nó giúp ngăn ngừa các lỗi phổ biến và cải thiện chất lượng tổng thể cũng như khả năng bảo trì của code.
Next.js là framework chúng ta lựa chọn vì nó cung cấp một trải nghiệm tích hợp, liền mạch để xây dựng các ứng dụng full-stack.
Việc triển khai của chúng ta sẽ dựa vào một vài thư viện chính để xử lý các tác vụ cụ thể:
pre-authorized_code
.Để kiểm tra issuer của bạn, bạn sẽ cần một ví di động hỗ trợ giao thức OpenID4VCI. Đối với bài hướng dẫn này, chúng tôi khuyên dùng Sphereon Wallet, có sẵn cho cả Android và iOS.
Cách cài đặt Sphereon Wallet:
Cấp một thông tin xác thực là một hoạt động quan trọng về bảo mật, dựa trên các khái niệm mã hóa cơ bản để đảm bảo sự tin cậy và tính xác thực.
Về cốt lõi, một Verifiable Credential là một tập hợp các xác nhận đã được ký điện tử bởi Issuer. Chữ ký này cung cấp hai sự đảm bảo:
Chữ ký số được tạo ra bằng cách sử dụng mật mã khóa công khai/khóa riêng. Đây là cách nó hoạt động:
Trong quá trình triển khai của chúng ta, chúng ta sẽ tạo một cặp khóa Elliptic Curve (EC)
và sử dụng thuật toán ES256
để ký JWT-VC. Khóa công khai được nhúng trong DID của Issuer
(did:web
), cho phép bất kỳ Verifier nào khám phá nó và xác thực chữ ký của thông tin xác
thực. Lưu ý: Claim aud
(audience) được cố ý bỏ qua trong các JWT của chúng ta, vì
thông tin xác thực được thiết kế để sử dụng chung và không bị ràng buộc với một ví cụ thể.
Nếu bạn muốn hạn chế việc sử dụng cho một đối tượng cụ thể, hãy bao gồm một claim aud
và
đặt nó tương ứng.
Ứng dụng Issuer của chúng ta được xây dựng như một dự án Next.js
full-stack, với sự phân tách rõ ràng giữa
logic frontend và backend. Kiến trúc này cho phép chúng ta tạo ra một trải nghiệm người
dùng liền mạch trong khi xử lý tất cả các hoạt động quan trọng về bảo mật trên máy chủ.
Quan trọng: Các bảng verification_sessions
và verified_credentials
được bao gồm
trong SQL không bắt buộc cho issuer này nhưng được đưa vào để đầy đủ.
src/app/issue/page.tsx
): Một trang React duy
nhất cho phép người dùng nhập dữ liệu của họ để yêu cầu một thông tin xác thực. Nó thực
hiện các lệnh gọi API đến backend của chúng ta để khởi tạo quy trình cấp phát.src/app/api/issue/...
): Một tập hợp các endpoint phía máy chủ
triển khai giao thức OpenID4VCI.
/.well-known/openid-credential-issuer
: Một endpoint siêu dữ liệu công khai. Đây là
URL đầu tiên mà một ví sẽ kiểm tra để khám phá các khả năng của issuer, bao gồm máy
chủ ủy quyền, endpoint token, endpoint thông tin xác thực, và các loại thông tin xác
thực mà nó cung cấp./.well-known/openid-configuration
: Một endpoint khám phá OpenID Connect tiêu
chuẩn. Mặc dù liên quan chặt chẽ đến endpoint trên, endpoint này phục vụ cấu hình
liên quan đến OIDC rộng hơn và thường được yêu cầu để tương tác với các client
OpenID tiêu chuẩn./.well-known/did.json
: DID Document cho issuer của chúng ta. Khi sử dụng phương
thức did:web
, tệp này được sử dụng để công bố các khóa công khai của issuer, mà
các verifier có thể sử dụng để xác thực chữ ký của các thông tin xác thực nó cấp.authorize/route.ts
: Tạo một pre-authorized_code
và một đề nghị cấp thông tin xác
thực.token/route.ts
: Đổi pre-authorized_code
để lấy một
access token.credential/route.ts
: Cấp JWT-VC cuối cùng, được ký mã hóa.schemas/pid/route.ts
: Cung cấp JSON schema cho thông tin xác thực PID. Điều này
cho phép bất kỳ người tiêu dùng nào của thông tin xác thực hiểu được cấu trúc và các
kiểu dữ liệu của nó.src/lib/
):
database.ts
: Quản lý tất cả các tương tác cơ sở dữ liệu, chẳng hạn như lưu trữ mã
ủy quyền và khóa của issuer.crypto.ts
: Xử lý tất cả các hoạt động mã hóa, bao gồm tạo khóa và ký JWT.Đây là sơ đồ minh họa luồng cấp phát:
Bây giờ chúng ta đã có một sự hiểu biết vững chắc về các tiêu chuẩn, giao thức và kiến trúc, chúng ta có thể bắt đầu xây dựng issuer của mình.
Làm theo hoặc sử dụng mã nguồn cuối cùng
Bây giờ chúng ta sẽ đi qua từng bước thiết lập và triển khai mã nguồn. Nếu bạn muốn chuyển thẳng đến sản phẩm hoàn chỉnh, bạn có thể sao chép dự án hoàn chỉnh từ kho lưu trữ GitHub của chúng tôi và chạy nó cục bộ.
git clone https://github.com/corbado/digital-credentials-example.git
Đầu tiên, chúng ta sẽ khởi tạo một dự án Next.js mới, cài đặt các phụ thuộc cần thiết và khởi động cơ sở dữ liệu của chúng ta.
Mở terminal của bạn, điều hướng đến thư mục bạn muốn tạo dự án và chạy lệnh sau. Chúng ta đang sử dụng App Router, TypeScript, và Tailwind CSS cho dự án này.
npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm
Lệnh này tạo ra một ứng dụng Next.js mới trong thư mục hiện tại của bạn.
Tiếp theo, chúng ta cần cài đặt các thư viện sẽ xử lý JWT, kết nối cơ sở dữ liệu và tạo UUID.
npm install jose mysql2 uuid @types/uuid
Lệnh này cài đặt:
jose
: Để ký và xác minh JSON Web Tokens (JWTs).mysql2
: Client MySQL cho cơ sở dữ liệu của
chúng ta.uuid
: Để tạo các chuỗi thách thức duy nhất.@types/uuid
: Các kiểu TypeScript cho thư viện uuid
.Backend của chúng ta yêu cầu một cơ sở dữ liệu
MySQL để lưu trữ mã ủy quyền, các phiên cấp phát
và khóa của issuer. Chúng tôi đã bao gồm một tệp docker-compose.yml
để làm điều này trở
nên dễ dàng.
Nếu bạn đã sao chép kho lưu trữ, bạn chỉ cần chạy docker-compose up -d
. Nếu bạn đang xây
dựng từ đầu, hãy tạo một tệp có tên docker-compose.yml
với nội dung sau:
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:
Thiết lập Docker Compose này cũng yêu cầu một kịch bản khởi tạo SQL. Tạo một thư mục có
tên sql
và bên trong nó, một tệp có tên init.sql
với nội dung sau để thiết lập các
bảng cần thiết cho cả verifier và 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) );
Khi cả hai tệp đã sẵn sàng, mở terminal của bạn ở thư mục gốc của dự án và chạy:
docker-compose up -d
Lệnh này sẽ khởi động một container MySQL trong nền, sẵn sàng để ứng dụng của chúng ta sử dụng.
Trước khi chúng ta xây dựng các API endpoint, hãy tạo các thư viện dùng chung sẽ xử lý logic nghiệp vụ cốt lõi. Cách tiếp cận này giúp các API route của chúng ta gọn gàng và tập trung vào việc xử lý các yêu cầu HTTP, trong khi công việc phức tạp được ủy thác cho các mô-đun này.
src/lib/database.ts
)#Tệp này là nguồn chân lý duy nhất cho tất cả các tương tác cơ sở dữ liệu. Nó sử dụng thư
viện mysql2
để kết nối với container MySQL của chúng ta và cung cấp một tập hợp các hàm
được xuất ra để tạo, đọc và cập nhật các bản ghi trong các bảng của chúng ta. Lớp trừu
tượng này làm cho code của chúng ta trở nên modular và dễ bảo trì hơn.
Tạo tệp src/lib/database.ts
với nội dung sau:
// src/lib/database.ts import mysql from "mysql2/promise"; // Database connection configuration 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; } // Data-Access-Object (DAO) functions for each table // ... (e.g., createChallenge, getChallenge, createAuthorizationCode, etc.)
Lưu ý: Để ngắn gọn, danh sách đầy đủ các hàm DAO đã được bỏ qua. Bạn có thể tìm thấy mã nguồn hoàn chỉnh trong kho lưu trữ của dự án. Tệp này bao gồm các hàm để quản lý các thách thức, các phiên xác minh, mã ủy quyền, các phiên cấp phát, và khóa của issuer.
src/lib/crypto.ts
)#Tệp này xử lý tất cả các hoạt động mã hóa quan trọng về bảo mật. Nó sử dụng thư viện
jose
để tạo cặp khóa và ký JSON Web Tokens (JWTs).
Tạo khóa Hàm generateIssuerKeyPair
tạo ra một cặp khóa Elliptic Curve mới sẽ được sử
dụng để ký các thông tin xác thực. Khóa công khai được xuất ra ở định dạng JSON Web Key
(JWK) để nó có thể được công bố trong tài liệu did.json
của chúng ta.
// 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; // Assign a unique key ID // ... (private key export and other setup) return { publicKey, privateKey, publicKeyJWK /* ... */ }; }
Tạo thông tin xác thực JWT Hàm createJWTVerifiableCredential
là cốt lõi của quy
trình cấp phát. Nó lấy các xác nhận của người dùng, cặp khóa của issuer, và các siêu dữ
liệu khác, và sử dụng chúng để tạo ra một JWT-VC đã được ký.
// 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 = { // The issuer's DID iss: issuerKeyPair.issuerDid, // The subject's (holder's) DID sub: subjectId, // The time the credential was issued (iat) and when it expires (exp) iat: now, exp: now + oneYear, // The Verifiable Credential data model 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, }, }, }; // Sign the payload with the issuer's private key return await new SignJWT(vcPayload) .setProtectedHeader({ alg: issuerKeyPair.algorithm, kid: issuerKeyPair.keyId, typ: "JWT", }) .sign(issuerKeyPair.privateKey); }
Hàm này xây dựng payload JWT theo Mô hình Dữ liệu Verifiable Credentials của W3C và ký nó bằng khóa riêng của issuer, tạo ra một thông tin xác thực có thể xác minh an toàn.
Ứng dụng Next.js của chúng ta được cấu trúc để tách biệt các mối quan tâm giữa frontend và backend, mặc dù chúng là một phần của cùng một dự án. Điều này được thực hiện bằng cách tận dụng App Router cho cả các trang UI và các API endpoint.
Frontend (src/app/issue/page.tsx
): Một component trang
React duy nhất định nghĩa UI cho route /issue
. Nó xử lý đầu
vào của người dùng và giao tiếp với API backend của chúng ta.
Backend API Routes (src/app/api/...
):
.well-known/.../route.ts
): Các route này cung cấp các endpoint siêu
dữ liệu công khai cho phép ví và các client khác khám phá các khả năng và khóa công
khai của issuer.issue/.../route.ts
): Các endpoint này triển khai logic OpenID4VCI
cốt lõi, bao gồm tạo các đề nghị cấp thông tin xác thực, cấp token, và ký thông tin
xác thực cuối cùng.schemas/pid/route.ts
): Route này phục vụ JSON schema cho thông tin xác
thực, định nghĩa cấu trúc của nó.Thư viện (src/lib/
): Thư mục này chứa logic có thể tái sử dụng được chia sẻ trên
toàn bộ backend.
database.ts
: Quản lý tất cả các tương tác cơ sở dữ liệu, trừu tượng hóa các truy
vấn SQL.crypto.ts
: Xử lý tất cả các hoạt động mã hóa, chẳng hạn như tạo khóa và ký JWT.Sự phân tách rõ ràng này làm cho ứng dụng trở nên modular và dễ bảo trì hơn.
Lưu ý: Hàm generateIssuerDid()
phải trả về một did:web
hợp lệ khớp với miền issuer
của bạn. Khi được triển khai, .well-known/did.json
phải được phục vụ qua HTTPS tại miền
đó để các verifier có thể xác thực các thông tin xác thực.
Frontend của chúng ta là một trang React duy nhất cung cấp một biểu mẫu đơn giản cho người dùng yêu cầu một thông tin xác thực kỹ thuật số mới. Trách nhiệm của nó là:
Logic cốt lõi được xử lý trong hàm handleSubmit
, được kích hoạt khi người dùng gửi biểu
mẫu.
// src/app/issue/page.tsx const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(null); setCredentialOffer(null); try { // 1. Validate required fields if (!userData.given_name || !userData.family_name || !userData.birth_date) { throw new Error("Please fill in all required fields"); } // 2. Request a credential offer from the 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 || "Failed to create credential offer", ); } // 3. Set the credential offer in state to display the QR code const result = await response.json(); setCredentialOffer(result); } catch (err) { const errorMessage = (err as Error).message || "Unknown error occurred"; setError(errorMessage); } finally { setLoading(false); } };
Hàm này thực hiện ba hành động chính:
POST
đến endpoint /api/issue/authorize
của chúng ta với dữ liệu
của người dùng.Phần còn lại của tệp chứa code React tiêu chuẩn để hiển thị biểu mẫu và màn hình hiển thị mã QR. Bạn có thể xem tệp hoàn chỉnh trong kho lưu trữ của dự án.
Trước khi chúng ta xây dựng API backend, chúng ta cần cấu hình môi trường của mình và
thiết lập các endpoint khám phá. Các tệp .well-known
này rất quan trọng để ví có thể tìm
thấy issuer của chúng ta và hiểu cách tương tác với nó.
Tạo một tệp có tên .env.local
trong thư mục gốc của dự án và thêm dòng sau. URL này phải
có thể truy cập công khai để một ví di động có thể tiếp cận nó. Để phát triển cục bộ, bạn
có thể sử dụng một dịch vụ đường hầm như
ngrok để public localhost
của bạn.
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Các ví khám phá các khả năng của một issuer bằng cách truy vấn các URL .well-known
tiêu
chuẩn. Chúng ta cần tạo ba trong số các endpoint này.
1. Siêu dữ liệu Issuer (/.well-known/openid-credential-issuer
)
Đây là tệp khám phá chính cho OpenID4VCI. Nó cho ví biết mọi thứ cần thiết về issuer, bao gồm các endpoint của nó, các loại thông tin xác thực nó cung cấp, và các thuật toán mã hóa được hỗ trợ.
Tạo tệp 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 = { // The issuer's unique identifier. issuer: baseUrl, // The URL of the authorization server. For simplicity, our issuer is its own authorization server. authorization_servers: [baseUrl], // The URL of the credential issuer. credential_issuer: baseUrl, // The endpoint where the wallet will POST to receive the actual credential. credential_endpoint: `${baseUrl}/api/issue/credential`, // The endpoint where the wallet exchanges an authorization code for an access token. token_endpoint: `${baseUrl}/api/issue/token`, // The endpoint for the authorization flow (not used in our pre-authorized flow, but good practice to include). authorization_endpoint: `${baseUrl}/api/issue/authorize`, // Indicates support for the pre-authorized code flow without requiring client authentication. pre_authorized_grant_anonymous_access_supported: true, // Human-readable information about the issuer. display: [ { name: "Corbado Credentials Issuer", locale: "en-US", }, ], // A list of the credential types this issuer can issue. credential_configurations_supported: { "eu.europa.ec.eudi.pid.1": { // The format of the credential (e.g., jwt_vc, mso_mdoc). format: "jwt_vc", // The specific document type, conforming to ISO mDoc standards. doctype: "eu.europa.ec.eudi.pid.1", // The OAuth 2.0 scope associated with this credential type. scope: "eu.europa.ec.eudi.pid.1", // Methods the wallet can use to prove possession of its key. cryptographic_binding_methods_supported: ["jwk"], // Signing algorithms the issuer supports for this credential. credential_signing_alg_values_supported: ["ES256"], // Proof-of-possession types the wallet can use. proof_types_supported: { jwt: { proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"], }, }, // Display properties for the credential. display: [ { name: "Corbado Credential Issuer", locale: "en-US", logo: { uri: `${baseUrl}/logo.png`, alt_text: "EU Digital Identity", }, background_color: "#003399", text_color: "#FFFFFF", }, ], // A list of the claims (attributes) in the credential. 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" }], }, }, }, }, }, // Authentication methods supported by the token endpoint. 'none' means public client. token_endpoint_auth_methods_supported: ["none"], // PKCE code challenge methods supported. code_challenge_methods_supported: ["S256"], // OAuth 2.0 grant types the issuer supports. 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. Cấu hình OpenID (/.well-known/openid-configuration
)
Đây là một tài liệu khám phá OIDC tiêu chuẩn cung cấp một tập hợp các chi tiết cấu hình rộng hơn.
Tạo tệp 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 = { // The issuer's unique identifier. credential_issuer: baseUrl, // The endpoint where the wallet will POST to receive the actual credential. credential_endpoint: `${baseUrl}/api/issue/credential`, // The endpoint for the authorization flow. authorization_endpoint: `${baseUrl}/api/issue/authorize`, // The endpoint where the wallet exchanges an authorization code for an access token. token_endpoint: `${baseUrl}/api/issue/token`, // A list of the credential types this issuer can issue. 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"], }, }, }, }, // OAuth 2.0 grant types the issuer supports. grant_types_supported: [ "authorization_code", "urn:ietf:params:oauth:grant-type:pre-authorized_code", ], // Indicates support for the pre-authorized code flow. pre_authorized_grant_anonymous_access_supported: true, // PKCE code challenge methods supported. code_challenge_methods_supported: ["S256"], // Authentication methods supported by the token endpoint. token_endpoint_auth_methods_supported: ["none"], // OAuth 2.0 scopes the issuer supports. 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. Tài liệu DID (/.well-known/did.json
)
Tệp này công bố khóa công khai của issuer bằng phương thức did:web
, cho phép bất kỳ ai
xác minh chữ ký của các thông tin xác thực do nó cấp.
Tạo tệp 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 = { // The context defines the vocabulary used in the document. "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1", ], // The DID URI, which is the unique identifier for the issuer. id: didId, // The DID controller, which is the entity that controls the DID. Here, it's the issuer itself. controller: didId, // A list of public keys that can be used to verify signatures from the issuer. verificationMethod: [ { // A unique identifier for the key, scoped to the DID. id: `${didId}#${issuerKey.key_id}`, // The type of the key. type: "JsonWebKey2020", // The DID of the key's controller. controller: didId, // The public key in JWK format. publicKeyJwk: publicKeyJWK, }, ], // Specifies which keys can be used for authentication (proving control of the DID). authentication: [`${didId}#${issuerKey.key_id}`], // Specifies which keys can be used for creating verifiable credentials. assertionMethod: [`${didId}#${issuerKey.key_id}`], // A list of services provided by the DID subject, such as the issuer endpoint. 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", }, }); }
Tại sao không cache? Bạn sẽ nhận thấy rằng cả ba endpoint này đều trả về các header
ngăn chặn việc cache một cách quyết liệt (Cache-Control: no-cache
, Pragma: no-cache
,
Expires: 0
). Đây là một thực hành bảo mật quan trọng đối với các tài liệu khám phá.
Cấu hình của Issuer có thể thay đổi—ví dụ, một khóa mã hóa có thể được xoay vòng. Nếu
một ví hoặc client lưu vào bộ đệm một phiên bản cũ của tệp did.json
hoặc
openid-credential-issuer
, nó sẽ không thể xác thực các thông tin xác thực mới hoặc
tương tác với các endpoint đã cập nhật. Bằng cách buộc các client phải lấy một bản sao
mới trong mỗi yêu cầu, chúng tôi đảm bảo họ luôn có thông tin cập nhật nhất.
Phần cuối cùng của cơ sở hạ tầng công khai của chúng ta là endpoint schema thông tin xác thực. Route này phục vụ một JSON Schema định nghĩa chính thức cấu trúc, các kiểu dữ liệu và các ràng buộc của thông tin xác thực PID mà chúng ta đang cấp. Ví và verifier có thể sử dụng schema này để xác thực nội dung của thông tin xác thực.
Tạo tệp src/app/api/schemas/pid/route.ts
với nội dung sau:
// 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", // Replace with your actual domain title: "PID Credential", description: "A schema for a Verifiable Credential representing a Personal Identification Document (PID).", type: "object", properties: { credentialSubject: { type: "object", properties: { given_name: { type: "string" }, family_name: { type: "string" }, birth_date: { type: "string", format: "date" }, // ... other properties of the credential subject }, required: ["given_name", "family_name", "birth_date"], }, // ... other top-level properties of a Verifiable Credential }, }; return NextResponse.json(schema, { headers: { "Content-Type": "application/schema+json", "Access-Control-Allow-Origin": "*", // Allow cross-origin requests }, }); }
Lưu ý: JSON Schema cho một thông tin xác thực PID có thể khá lớn và chi tiết. Để ngắn gọn, schema đầy đủ đã được cắt bớt. Bạn có thể tìm thấy tệp hoàn chỉnh trong kho lưu trữ của dự án.
Với frontend đã sẵn sàng, bây giờ chúng ta cần logic phía máy chủ để xử lý luồng
OpenID4VCI. Chúng ta sẽ bắt đầu với endpoint đầu tiên mà frontend gọi:
/api/issue/authorize
.
/api/issue/authorize
: Tạo đề nghị cấp thông tin xác thực#Endpoint này chịu trách nhiệm lấy dữ liệu của người dùng, tạo ra một mã sử dụng một lần an
toàn, và xây dựng một credential_offer
mà ví của người dùng có thể hiểu được.
Đây là logic cốt lõi:
// 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. Validate user data 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. Generate a pre-authorized code and a PIN const code = uuidv4(); const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes const txCode = Math.floor(1000 + Math.random() * 9000).toString(); // 4-digit PIN // 3. Store the code and user data await createAuthorizationCode(uuidv4(), code, expiresAt); // Note: This uses an in-memory store for demo purposes only. // In production, persist data securely in a database with proper expiry. 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. Create the credential offer object const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const credentialOffer = { // The issuer's identifier, which is its base URL. credential_issuer: baseUrl, // An array of credential types the issuer is offering. credential_configuration_ids: ["eu.europa.ec.eudi.pid.1"], // Specifies the grant types the wallet can use. grants: { // We are using the pre-authorized code flow. "urn:ietf:params:oauth:grant-type:pre-authorized_code": { // The one-time code the wallet will exchange for a token. "pre-authorized_code": code, // Indicates that the user must enter a PIN (tx_code) to redeem the code. user_pin_required: true, }, }, }; // 5. Create the full credential offer URI (a deep link for wallets) const credentialOfferUri = `openid-credential-offer://?credential_offer=${encodeURIComponent( JSON.stringify(credentialOffer), )}`; // The final response to the frontend. return NextResponse.json({ // The deep link for the QR code. credential_offer_uri: credentialOfferUri, // The raw pre-authorized code, for display or manual entry. pre_authorized_code: code, // The 4-digit PIN the user must enter in their wallet. tx_code: txCode, }); } catch (error) { console.error("Authorization error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }
Các bước chính trong endpoint này:
pre-authorized_code
duy nhất (một UUID) và một tx_code
4
chữ số (PIN) để tăng thêm một lớp bảo mật.pre-authorized_code
được lưu trữ trong cơ sở dữ liệu với thời
gian hết hạn ngắn. Dữ liệu của người dùng và mã PIN được lưu trữ trong bộ nhớ, liên kết
với mã.credential_offer
theo đặc tả của
OpenID4VCI. Đối tượng này cho ví biết issuer ở đâu, nó cung cấp thông tin xác thực nào,
và mã cần thiết để nhận chúng.openid-credential-offer://...
) và trả về cho frontend, cùng với tx_code
để người
dùng xem./api/issue/token
: Đổi mã lấy token#Khi người dùng quét mã QR và nhập mã PIN, ví sẽ thực hiện một yêu cầu POST
đến endpoint
này. Công việc của nó là xác thực pre-authorized_code
và user_pin
(PIN), và nếu chúng
hợp lệ, cấp một access token ngắn hạn.
Tạo tệp src/app/api/issue/token/route.ts
với nội dung sau:
// 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. Validate the grant type if (grant_type !== "urn:ietf:params:oauth:grant-type:pre-authorized_code") { return NextResponse.json( { error: "unsupported_grant_type" }, { status: 400 }, ); } // 2. Validate the pre-authorized code const authCode = await getAuthorizationCode(code); if (!authCode) { return NextResponse.json( { error: "invalid_grant", error_description: "Invalid or expired code", }, { status: 400 }, ); } // 3. Validate the PIN (tx_code) const expectedTxCode = (global as any).txCodeStore?.get(code); if (expectedTxCode !== user_pin) { return NextResponse.json( { error: "invalid_grant", error_description: "Invalid PIN" }, { status: 400 }, ); } // 4. Generate access token and c_nonce const accessToken = uuidv4(); const cNonce = uuidv4(); const cNonceExpiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes // 5. Create a new issuance session const userData = (global as any).userDataStore?.get(code); await createIssuanceSession( uuidv4(), authCode.id, accessToken, cNonce, cNonceExpiresAt, userData, ); // 6. Mark the code as used and clean up temporary data await markAuthorizationCodeAsUsed(code); (global as any).txCodeStore?.delete(code); (global as any).userDataStore?.delete(code); // 7. Return the access token response return NextResponse.json({ access_token: accessToken, token_type: "Bearer", expires_in: 3600, // 1 hour c_nonce: cNonce, c_nonce_expires_in: 300, // 5 minutes }); } catch (error) { console.error("Token endpoint error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }
Các bước chính trong endpoint này:
pre-authorized_code
chính xác.pre-authorized_code
có tồn tại trong cơ sở dữ liệu,
chưa hết hạn và chưa được sử dụng.user_pin
từ ví với tx_code
mà chúng ta đã lưu trữ
trước đó để đảm bảo người dùng đã ủy quyền giao dịch.access_token
an toàn và một c_nonce
(credential
nonce), là một giá trị sử dụng một lần để ngăn chặn các cuộc tấn công phát lại trên
endpoint thông tin xác thực.issuance_sessions
mới trong cơ sở dữ liệu, liên kết
access token với dữ liệu của người dùng.pre-authorized_code
là đã sử dụng.access_token
và c_nonce
cho ví./api/issue/credential
: Cấp thông tin xác thực đã được ký#Đây là endpoint cuối cùng và quan trọng nhất. Ví sử dụng access token nó nhận được từ
endpoint /token
để thực hiện một yêu cầu POST
đã được xác thực đến route này. Công
việc của endpoint này là thực hiện xác thực cuối cùng, tạo ra thông tin xác thực được ký
mã hóa, và trả về cho ví.
Tạo tệp src/app/api/issue/credential/route.ts
với nội dung sau:
// 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. Validate the Bearer token 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. Get the user data from the session const userData = session.user_data; if (!userData) { return NextResponse.json({ error: "missing_user_data" }, { status: 400 }); } // 3. Get the active issuer key const issuerKey = await getActiveIssuerKey(); if (!issuerKey) { // In a real application, you would have a more robust key management system. // For this demo, we can generate a key on the fly if one doesn't exist. // This part is omitted for brevity but is in the repository. return NextResponse.json( { error: "server_error", error_description: "Failed to get issuer key", }, { status: 500 }, ); } // 4. Create the 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. Store the issued credential in the database await createIssuedCredential(/* ... credential details ... */); await updateIssuanceSession(session.id, "credential_issued"); // 6. Return the signed credential return NextResponse.json({ format: "jwt_vc", credential: credentialData, c_nonce: uuidv4(), // A new nonce for subsequent requests c_nonce_expires_in: 300, }); } catch (error) { console.error("Credential endpoint error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }
Các bước chính trong endpoint này:
Bearer
hợp lệ trong header Authorization
và sử dụng nó để tra cứu phiên cấp phát đang hoạt động.createJWTVerifiableCredential
của
chúng ta từ src/lib/crypto.ts
để xây dựng và ký JWT-VC.Bây giờ bạn đã có một triển khai hoàn chỉnh, từ đầu đến cuối của một issuer thông tin xác thực kỹ thuật số. Đây là cách chạy nó cục bộ và những gì bạn cần xem xét để đưa nó từ một bằng chứng khái niệm thành một ứng dụng sẵn sàng cho sản xuất.
Sao chép Kho lưu trữ:
git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
Cài đặt các phụ thuộc:
npm install
Khởi động cơ sở dữ liệu: Đảm bảo Docker đang chạy, sau đó khởi động container MySQL:
docker-compose up -d
Cấu hình Môi trường & Chạy Tunnel: Đây là bước quan trọng nhất để kiểm tra cục bộ.
Vì ví di động của bạn cần kết nối với máy phát triển của bạn qua internet, bạn phải
public máy chủ cục bộ của mình bằng một URL HTTPS công khai. Chúng ta sẽ sử dụng
ngrok
cho việc này.
a. Khởi động ngrok:
ngrok http 3000
b. Sao chép URL HTTPS từ đầu ra của
ngrok (ví dụ:
https://random-string.ngrok.io
). c. Tạo một tệp .env.local
và đặt URL:
NEXT_PUBLIC_BASE_URL=https://<your-ngrok-url>
Chạy ứng dụng:
npm run dev
Mở trình duyệt của bạn đến http://localhost:3000/issue
. Bây giờ bạn có thể điền vào
biểu mẫu, và mã QR được tạo ra sẽ trỏ đúng đến URL ngrok công khai của bạn, cho phép
ví di động của bạn kết nối và nhận thông tin xác thực.
ngrok
#Các giao thức thông tin xác thực kỹ thuật số được xây dựng với ưu tiên hàng đầu là bảo
mật. Vì lý do này, các ví gần như luôn từ chối kết nối với một issuer qua một kết nối
không an toàn (http://
). Toàn bộ quá trình dựa trên một kết nối HTTPS an toàn, được
kích hoạt bởi một chứng chỉ SSL.
Một dịch vụ tunnel như ngrok
giải quyết cả hai vấn đề bằng cách tạo ra một URL HTTPS
công khai, an toàn (với một chứng chỉ SSL hợp lệ) chuyển tiếp tất cả lưu lượng truy cập
đến máy chủ phát triển cục bộ của bạn. Các ví yêu cầu HTTPS và sẽ từ chối kết nối với các
endpoint không an toàn (http://
). Đây là một công cụ thiết yếu để kiểm tra bất kỳ dịch
vụ web nào cần tương tác với các thiết bị di động hoặc webhook bên ngoài.
Ví dụ này được cố ý tập trung vào luồng cấp phát cốt lõi để dễ hiểu. Các chủ đề sau được coi là nằm ngoài phạm vi:
revoked
để sử dụng trong tương lai,
nhưng không có logic thu hồi nào được cung cấp ở đây.pre-authorized_code
. Một triển khai đầy đủ của luồng authorization_code
sẽ yêu cầu
một màn hình chấp thuận của người dùng và logic OAuth 2.0 phức tạp
hơn.Thế là xong! Với một vài trang code, bây giờ chúng ta đã có một issuer thông tin xác thực kỹ thuật số hoàn chỉnh, từ đầu đến cuối, có khả năng:
pre-authorized_code
đầy đủ.Mặc dù hướng dẫn này cung cấp một nền tảng vững chắc, một issuer sẵn sàng cho sản xuất sẽ yêu cầu các tính năng bổ sung như quản lý khóa mạnh mẽ, lưu trữ bền vững thay vì lưu trữ trong bộ nhớ, thu hồi thông tin xác thực, và tăng cường bảo mật toàn diện. Khả năng tương thích của ví cũng khác nhau; Sphereon Wallet được khuyến nghị để kiểm tra, nhưng các ví khác có thể không hỗ trợ luồng pre-authorized như đã triển khai ở đây. Tuy nhiên, các khối xây dựng cốt lõi và luồng tương tác sẽ vẫn giữ nguyên. Bằng cách tuân theo các mẫu này, bạn có thể xây dựng một issuer an toàn và có khả năng tương tác cho bất kỳ loại thông tin xác thực kỹ thuật số nào.
Đây là một số tài nguyên, thông số kỹ thuật và công cụ chính được sử dụng hoặc tham chiếu trong hướng dẫn này:
Kho lưu trữ dự án:
Các thông số kỹ thuật chính:
did:web
: Phương thức DID
được sử dụng cho khóa công khai của issuer của chúng ta.Công cụ:
Thư viện:
Related Articles
Table of Contents