---
url: 'https://www.corbado.com/vi/blog/cach-xay-dung-verifiable-credential-issuer'
title: '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)'
description: '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'
lang: 'vi'
author: 'Amine'
date: '2025-08-20T15:39:08.129Z'
lastModified: '2026-03-25T10:08:33.487Z'
keywords: 'trình cấp thông tin xác thực kỹ thuật số, hướng dẫn xây dựng trình cấp, xây dựng trình cấp'
category: 'Digital Credentials'
---

# 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)

## 1. Giới thiệu

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](https://www.corbado.com/glossary/issuer) là một thực thể đáng tin cậy—chẳng hạn như một cơ quan
[chính phủ](https://www.corbado.com/passkeys-for-public-sector), 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](https://www.corbado.com/vi/blog/ung-dung-crud-react-express-mysql) 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](https://www.corbado.com/glossary/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](https://www.corbado.com/glossary/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](https://www.corbado.com/blog/nextjs-passkeys) hoạt động được, có
khả năng:

1. Chấp nhận dữ liệu người dùng thông qua một biểu mẫu web đơn giản.
2. Tạo một đề nghị cấp thông tin [xác thực an toàn](https://www.corbado.com/vi/glossary/open-id-4-vp), dùng một
   lần.
3. Hiển thị đề nghị dưới dạng [mã QR](https://www.corbado.com/vi/blog/phuong-phap-dang-nhap-xac-thuc-ma-qr) để
   người dùng quét bằng ví di động của họ.
4. Cấp một thông tin xác thực được ký mã hóa mà người dùng có thể lưu trữ và trình bày để
   xác minh.

### 1.1 Tìm hiểu thuật ngữ: Digital Credentials và Verifiable Credentials

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](https://www.corbado.com/vi/glossary/attestation). 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](https://www.corbado.com/vi/blog/cach-bat-passkey-tren-android) 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](https://www.corbado.com/glossary/microcredentials) 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ư:
    - Chữ ký mã hóa để đảm bảo tính xác thực và toàn vẹn
    - Mô hình dữ liệu và định dạng được tiêu chuẩn hóa
    - Cơ chế trình bày bảo vệ quyền riêng tư
    - Các giao thức xác minh có khả năng tương tác

**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](https://www.corbado.com/glossary/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](https://www.corbado.com/glossary/microcredentials).

### 1.2 Cách hoạt động

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

- **Issuer:** Một cơ quan có thẩm quyền đáng tin cậy (ví dụ: một cơ quan
  [chính phủ](https://www.corbado.com/passkeys-for-public-sector), trường đại học, hoặc ngân hàng) ký mã hóa và
  cấp một thông tin xác thực cho người dùng. **Đây là vai trò chúng ta đang xây dựng trong
  hướng dẫn này.**
- **Holder:** Người dùng, nhận thông tin xác thực và lưu trữ nó an toàn trong một ví kỹ
  thuật số cá nhân trên thiết bị của họ.
- **Verifier:** Một ứng dụng hoặc dịch vụ cần kiểm tra thông tin xác thực của người dùng.

![Hệ sinh thái W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model/diagrams/ecosystem.svg)

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.
![Biểu mẫu nhập dữ liệu người dùng](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_1_0733a9e1da.png)

**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](https://www.corbado.com/vi/glossary/open-id-4-vp), được hiển thị dưới dạng
[mã QR](https://www.corbado.com/vi/blog/phuong-phap-dang-nhap-xac-thuc-ma-qr) và một mã được ủy quyền trước.
![Mã QR đề nghị cấp thông tin xác thực](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_2_3f1881c473.png)

**Bước 3: Tương tác với Ví** Người dùng quét
[mã QR](https://www.corbado.com/vi/blog/phuong-phap-dang-nhap-xac-thuc-ma-qr) bằng một ví tương thích (ví dụ:
Sphereon [Wallet](https://www.corbado.com/blog/digital-wallet-assurance)) và nhập mã PIN để ủy quyền việc cấp
phát.
![Đề nghị cấp thông tin xác thực trên ví](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_3_b80d689dfe.png)
![Nhập mã PIN](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_4_ca8bad8d11.png)

**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.
![Xác nhận chi tiết thông tin xác thực](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_5_55b8150597.png)
![Thông tin xác thực đã được thêm](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_6_7f5ac5745d.png)

## 2. Các yêu cầu tiên quyết để xây dựng một Issuer

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](https://www.corbado.com/vi/blog/ung-dung-crud-react-express-mysql) 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.

### 2.1 Lựa chọn giao 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](https://www.corbado.com/vi/blog/ung-dung-crud-react-express-mysql) 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](https://www.w3.org/TR/vc-data-model/#json-web-token)** | **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](https://www.iso.org/standard/69084.html)**           | **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.                           |

#### 2.1.1 Luồng ủy quyền: Pre-Authorized và Authorization Code

[OpenID4VCI](https://www.corbado.com/glossary/openid4vci) hỗ trợ hai luồng ủy quyền chính để cấp thông tin xác
thực:

1. **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.

2. **Luồng Authorization Code:** Đây là luồng [OAuth 2.0](https://www.corbado.com/glossary/oauth2) 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.

### 2.2 Lựa chọn công nghệ

Để 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.

#### 2.2.1 Ngôn ngữ: TypeScript

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](https://www.corbado.com/vi/blog/cach-bat-passkey-tren-android) 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.

#### 2.2.2 Framework: Next.js

**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](https://www.corbado.com/vi/blog/ung-dung-crud-react-express-mysql).

- **Đối với Frontend:** Chúng ta sẽ sử dụng [Next.js](https://www.corbado.com/blog/nextjs-passkeys) với
  [React](https://www.corbado.com/blog/react-passkeys) để xây dựng giao diện người dùng nơi người dùng có thể
  nhập dữ liệu của họ để yêu cầu một thông tin xác thực.
- **Đối với Backend:** Chúng ta sẽ tận dụng **Next.js API Routes** để tạo các endpoint
  phía máy chủ xử lý luồng [OpenID4VCI](https://www.corbado.com/glossary/openid4vci), từ việc tạo các đề nghị cấp
  thông tin xác thực đến việc cấp thông tin xác thực đã được ký cuối cùng.

#### 2.2.3 Các thư viện chính

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

- **next**, **react**, và **react-dom**: Các thư viện cốt lõi cho ứng dụng
  [Next.js](https://www.corbado.com/blog/nextjs-passkeys) của chúng ta.
- **mysql2**: Một client [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) cho
  [Node.js](https://www.corbado.com/blog/nodejs-passkeys), được sử dụng để lưu trữ mã ủy quyền và dữ liệu phiên.
- **uuid**: Một thư viện để tạo các định danh duy nhất, chúng ta sẽ sử dụng nó để tạo các
  giá trị `pre-authorized_code`.
- **jose**: Một thư viện mạnh mẽ để xử lý JSON Web Signatures (JWS), chúng ta sẽ sử dụng
  nó để ký mã hóa các thông tin xác thực chúng ta cấp.

### 2.3 Lấy một ví thử nghiệm

Để 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](https://www.corbado.com/blog/how-to-enable-passkeys-android) và
[iOS](https://www.corbado.com/blog/how-to-enable-passkeys-ios).

**Cách cài đặt Sphereon Wallet:**

1. **Tải ví** từ
   [Google Play Store](https://play.google.com/store/apps/details?id=com.sphereon.ssi.wallet)
   hoặc [Apple App Store](https://apps.apple.com/us/app/sphereon-wallet/id1661096796).
2. Cài đặt ứng dụng trên thiết bị di động của bạn.
3. Sau khi cài đặt, ví đã sẵn sàng để nhận các đề nghị cấp thông tin xác thực bằng cách
   quét mã QR.

### 2.4 Kiến thức về mã hóa

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](https://www.corbado.com/vi/blog/cach-bat-passkey-tren-android), 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.

#### 2.4.1 Chữ ký số

Về cốt lõi, một [Verifiable Credential](https://www.corbado.com/glossary/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:

- **Tính xác thực:** Nó chứng minh rằng thông tin xác thực được tạo ra bởi một Issuer hợp
  pháp.
- **Tính toàn vẹn:** Nó chứng minh rằng thông tin xác thực không bị thay đổi kể từ khi nó
  được cấp.

#### 2.4.2 Mật mã khóa công khai/khóa riêng

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:

1. **Issuer có một cặp khóa:** một **khóa riêng**, được giữ bí mật và an toàn, và một
   **khóa công khai** tương ứng, được công khai.
2. **Ký:** Khi Issuer tạo một thông tin xác thực, nó sử dụng **khóa riêng** của mình để
   tạo ra một chữ ký số duy nhất cho dữ liệu thông tin xác thực.
3. **Xác minh:** Một Verifier sau đó có thể sử dụng **khóa công khai** của Issuer để kiểm
   tra chữ ký. Nếu việc kiểm tra thành công, Verifier biết rằng thông tin xác thực là xác
   thực và không bị thay đổi.

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.

## 3. Tổng quan về kiến trúc

Ứng dụng Issuer của chúng ta được xây dựng như một dự án Next.js
[full-stack](https://www.corbado.com/vi/blog/ung-dung-crud-react-express-mysql), 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 đủ.

- **Frontend (`src/app/issue/page.tsx`):** Một trang [React](https://www.corbado.com/blog/react-passkeys) 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.
- **Backend API Routes (`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](https://www.corbado.com/glossary/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ó.
- **Thư việ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:

![Luồng cấp phát thông tin xác thực kỹ thuật số](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Mermaid_Chart_Create_complex_visual_diagrams_with_text_A_smarter_way_of_creating_diagrams_2025_07_29_145228_d28fd13731.svg)

## 4. Xây dựng Issuer

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ộ.
>
> ```bash
> git clone https://github.com/corbado/digital-credentials-example.git
> ```

### 4.1 Thiết lập dự án

Đầ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.

#### 4.1.1 Khởi tạo ứng dụng Next.js

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.

```bash
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.

#### 4.1.2 Cài đặt các phụ thuộc

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.

```bash
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](https://www.corbado.com/blog/passkey-webauthn-database-guide) 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`.

#### 4.1.3 Khởi động cơ sở dữ liệu

Backend của chúng ta yêu cầu một cơ sở dữ liệu
[MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) để 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:

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

volumes:
    mysql_data:
```

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:

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

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

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

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

-- ISSUER TABLES

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

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

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

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

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:

```bash
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.

### 4.2 Triển khai các thư viện dùng chung

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.

#### 4.2.1 Thư viện cơ sở dữ liệu (`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:

```typescript
// 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](https://github.com/corbado/digital-credentials-example/blob/main/src/lib/database.ts).
> 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.

#### 4.2.2 Thư viện mã hóa (`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.

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

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

    const publicKeyJWK = await exportJWK(publicKey);
    publicKeyJWK.kid = keyId; // 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ý.

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

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

    const vcPayload = {
        // 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](https://www.corbado.com/glossary/microcredentials) 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.

### 4.2 Tổng quan kiến trúc của ứng dụng Next.js

Ứ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](https://www.corbado.com/blog/react-passkeys) 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/...`):**
    - **Khám phá (`.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.
    - **Cấp phát (`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.
    - **Schema (`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.

![Tổng quan kiến trúc của ứng dụng Next.js](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Mermaid_Chart_Create_complex_visual_diagrams_with_text_A_smarter_way_of_creating_diagrams_2025_07_29_151549_6a0aca6477.svg)

### 4.3 Xây dựng Frontend

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

- Ghi lại dữ liệu người dùng (tên, ngày sinh, v.v.).
- Gửi dữ liệu này đến backend của chúng ta để tạo một đề nghị cấp thông tin xác thực.
- Hiển thị mã QR và PIN kết quả để người dùng quét bằng ví của họ.

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.

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

1. **Xác thực dữ liệu biểu mẫu** để đảm bảo tất cả các trường bắt buộc đã được điền.
2. **Gửi một yêu cầu `POST`** đến endpoint `/api/issue/authorize` của chúng ta với dữ liệu
   của người dùng.
3. **Cập nhật trạng thái của component** với đề nghị cấp thông tin xác thực nhận được từ
   backend, điều này kích hoạt UI để hiển thị mã QR và mã giao dịch.

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](https://github.com/corbado/digital-credentials-example/blob/main/src/app/issue/page.tsx).

### 4.4 Thiết lập Môi trường và Khám phá

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

#### 4.4.1 Tạo tệp môi trường

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](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok) để public `localhost` của bạn.

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

#### 4.4.2 Triển khai các Endpoint khám phá

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

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

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

    const issuerMetadata = {
        // 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`:

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

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

    const openidConfiguration = {
        // 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`:

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

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

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

    const publicKeyJWK = JSON.parse(issuerKey.public_key);
    const didId = generateIssuerDid();
    const didDocument = {
        // 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.

#### 4.4.3 Triển khai Endpoint Schema Thông tin xác thực

Phần cuối cùng của [cơ sở hạ tầng](https://www.corbado.com/passkeys-for-critical-infrastructure) 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:

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

export async function GET() {
    const schema = {
        $schema: "https://json-schema.org/draft/2020-12/schema",
        $id: "https://example.com/schemas/pid", // 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](https://github.com/corbado/digital-credentials-example/blob/main/src/app/api/schemas/pid/route.ts).

### 4.5 Xây dựng các Endpoint Backend

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

#### 4.5.1 `/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:

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

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

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

1. **Xác thực dữ liệu:** Đầu tiên, nó đảm bảo dữ liệu người dùng bắt buộc có mặt.
2. **Tạo mã:** Nó tạo ra một `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.
3. **Lưu trữ dữ liệu:** `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ã.
4. **Xây dựng đề nghị:** Nó xây dựng đối tượng `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.
5. **Trả về URI:** Cuối cùng, nó tạo ra một URI deep link
   (`openid-credential-offer://...`) và trả về cho frontend, cùng với `tx_code` để người
   dùng xem.

#### 4.5.2 `/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](https://www.corbado.com/glossary/access-token) ngắn hạn.

Tạo tệp `src/app/api/issue/token/route.ts` với nội dung sau:

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

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

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

1. **Xác thực loại grant:** Nó đảm bảo ví đang sử dụng loại grant `pre-authorized_code`
   chính xác.
2. **Xác thực mã:** Nó kiểm tra xem `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.
3. **Xác thực PIN:** Nó so sánh `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.
4. **Tạo token:** Nó tạo ra một `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.
5. **Tạo phiên:** Nó tạo một bản ghi `issuance_sessions` mới trong cơ sở dữ liệu, liên kết
   [access token](https://www.corbado.com/glossary/access-token) với dữ liệu của người dùng.
6. **Đánh dấu mã đã sử dụng:** Để ngăn chặn cùng một đề nghị được sử dụng hai lần, nó đánh
   dấu `pre-authorized_code` là đã sử dụng.
7. **Trả về token:** Nó trả về `access_token` và `c_nonce` cho ví.

#### 4.5.3 `/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:

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

export async function POST(request: NextRequest) {
    try {
        // 1. 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:

1. **Xác thực token:** Nó kiểm tra một token `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.
2. **Truy xuất dữ liệu người dùng:** Nó truy xuất dữ liệu xác nhận của người dùng, đã được
   lưu trữ trong phiên khi token được tạo.
3. **Tải khóa của issuer:** Nó tải khóa ký đang hoạt động của issuer từ cơ sở dữ liệu.
   Trong một kịch bản thực tế, điều này sẽ được quản lý bởi một hệ thống quản lý khóa an
   toàn.
4. **Tạo thông tin xác thực:** Nó gọi hàm trợ giúp `createJWTVerifiableCredential` của
   chúng ta từ `src/lib/crypto.ts` để xây dựng và ký JWT-VC.
5. **Ghi lại việc cấp phát:** Nó lưu một bản ghi về thông tin xác thực đã cấp vào cơ sở dữ
   liệu cho mục đích kiểm toán và thu hồi.
6. **Trả về thông tin xác thực:** Nó trả về thông tin xác thực đã được ký cho ví trong một
   phản hồi JSON. Ví sau đó có trách nhiệm lưu trữ nó một cách an toàn.

## 5. Chạy Issuer và các bước tiếp theo

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.

### 5.1 Cách chạy ví dụ

1. **Sao chép Kho lưu trữ:**

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

2. **Cài đặt các phụ thuộc:**

    ```bash
    npm install
    ```

3. **Khởi động cơ sở dữ liệu:** Đảm bảo Docker đang chạy, sau đó khởi động container
   MySQL:

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

4. **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:**

    ```bash
    ngrok http 3000
    ```

    b. **Sao chép URL HTTPS** từ đầu ra của
    [ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-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>
    ```

5. **Chạy ứng dụng:**

    ```bash
    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](https://www.corbado.com/blog/multi-device-passkey-login-corbado-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.

### 5.2 Tầm quan trọng của HTTPS và `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.

### 5.3 Những gì nằm ngoài phạm vi của hướng dẫn này

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:

- **Bảo mật sẵn sàng cho sản xuất:** Issuer này chỉ dành cho mục đích giáo dục. Một hệ
  thống sản xuất sẽ yêu cầu một Hệ thống Quản lý Khóa (KMS) an toàn thay vì lưu trữ khóa
  trong cơ sở dữ liệu, xử lý lỗi mạnh mẽ, giới hạn tốc độ, và ghi nhật ký kiểm toán toàn
  diện.
- **Thu hồi thông tin xác thực:** Hướng dẫn này không triển khai cơ chế thu hồi các thông
  tin xác thực đã cấp. Mặc dù schema bao gồm một cờ `revoked` để sử dụng trong tương lai,
  nhưng không có logic thu hồi nào được cung cấp ở đây.
- **Luồng Authorization Code:** Chúng tôi tập trung hoàn toàn vào luồng
  `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](https://www.corbado.com/glossary/oauth2) phức tạp
  hơn.
- **Quản lý người dùng:** Hướng dẫn không bao gồm bất kỳ xác thực hoặc quản lý người dùng
  nào cho chính issuer. Giả định rằng người dùng đã được xác thực và được ủy quyền để nhận
  một thông tin xác thực.

## 6. Kết luậ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:

1. Cung cấp một frontend thân thiện với người dùng để yêu cầu thông tin xác thực.
2. Triển khai luồng OpenID4VCI `pre-authorized_code` đầy đủ.
3. Cung cấp tất cả các endpoint khám phá cần thiết cho khả năng tương tác của ví.
4. Tạo và ký một JWT-[Verifiable Credential](https://www.corbado.com/glossary/verifiable-credential) an toàn,
   tuân thủ tiêu chuẩn.

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](https://www.corbado.com/blog/digital-wallet-assurance) đượ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.

## 7. Tài nguyên

Đâ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:**
    - [Mã nguồn hoàn chỉnh trên GitHub](https://github.com/corbado/digital-credentials-example)

- **Các thông số kỹ thuật chính:**
    - [OpenID for Verifiable Credential Issuance (OpenID4VCI)](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html):
      Giao thức cấp phát cốt lõi.
    - [W3C Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/): Tiêu
      chuẩn nền tảng cho VCs.
    - [Phương thức `did:web`](https://w3c-ccg.github.io/did-method-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ụ:**
    - [Sphereon Wallet](https://sphereon.com/wallet/): Ví thử nghiệm được sử dụng trong
      hướng dẫn này.
    - ngrok: Để tạo một đường hầm an toàn đến môi trường phát triển cục bộ của bạn.

- **Thư viện:**
    - Next.js: Framework React để xây dựng frontend và backend.
    - [jose](https://github.com/panva/jose): Để tạo và ký JSON Web Tokens (JWTs).
    - [mysql2](https://github.com/sidorares/node-mysql2): Client MySQL cho
      [Node.js](https://www.corbado.com/blog/nodejs-passkeys).
