Tìm hiểu cách xây dựng một trình xác minh thông tin xác thực kỹ thuật số từ đầu bằng Next.js, OpenID4VP và ISO mDoc. Hướng dẫn chi tiết này sẽ chỉ cho bạn cách tạo một trình xác minh có thể yêu cầu, nhận và xác thực giấy phép lái xe di động cùng các thông
Amine
Created: August 20, 2025
Updated: August 21, 2025
See the original blog version in English here.
Chứng minh danh tính trực tuyến là một thách thức không ngừng, dẫn đến sự phụ thuộc vào mật khẩu và việc chia sẻ các tài liệu nhạy cảm qua các kênh không an toàn. Điều này đã làm cho quy trình xác minh danh tính của doanh nghiệp trở nên chậm chạp, tốn kém và dễ bị gian lận. Thông tin xác thực kỹ thuật số (Digital Credentials) mang đến một cách tiếp cận mới, trao lại quyền kiểm soát dữ liệu cho người dùng. Chúng là phiên bản kỹ thuật số của một wallet vật lý, chứa mọi thứ từ giấy phép lái xe đến bằng đại học, nhưng có thêm lợi ích là an toàn về mặt mật mã, bảo vệ quyền riêng tư và có thể xác minh ngay lập tức.
Guide này cung cấp cho các nhà phát triển một hướng dẫn thực tế, từng bước để xây dựng một trình xác minh cho Digital Credentials. Mặc dù các tiêu chuẩn đã tồn tại, nhưng có rất ít hướng dẫn về việc triển khai chúng. Hướng dẫn này lấp đầy khoảng trống đó, chỉ cho bạn cách xây dựng một trình xác minh sử dụng API Digital Credential gốc của trình duyệt, OpenID4VP cho giao thức trình bày và ISO mDoc (ví dụ: giấy phép lái xe di động) làm định dạng thông tin xác thực.
Kết quả cuối cùng sẽ là một ứng dụng Next.js đơn giản nhưng đầy đủ chức năng, có thể yêu cầu, nhận và xác minh một Digital Credential từ một wallet di động tương thích.
Dưới đây là cái nhìn nhanh về ứng dụng cuối cùng khi hoạt động. Quá trình này bao gồm bốn bước chính:
Bước 1: Trang ban đầu Người dùng truy cập trang ban đầu và nhấp vào "Xác minh với Digital Identity" để bắt đầu quá trình.
Bước 2: Lời nhắc tin cậy Trình duyệt yêu cầu người dùng xác nhận tin cậy. Người dùng nhấp vào "Tiếp tục" để tiến hành.
Bước 3: Quét mã QR Một mã QR được hiển thị, người dùng quét mã này bằng ứng dụng wallet tương thích của họ.
Bước 4: Thông tin xác thực đã được giải mã Sau khi xác minh thành công, ứng dụng hiển thị dữ liệu thông tin xác thực đã được giải mã.
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
Đ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:
Khi người dùng muốn truy cập một dịch vụ, họ trình bày thông tin xác thực từ wallet của mình. Bên xác minh sau đó có thể kiểm tra ngay lập tức tính xác thực của nó mà không cần liên hệ trực tiếp với bên phát hành ban đầu.
Để hệ sinh thái danh tính phi tập trung này phát triển mạnh, vai trò của trình xác minh (verifier) là cực kỳ quan trọng. Họ là những người gác cổng của cơ sở hạ tầng tin cậy mới này, là những người sử dụng thông tin xác thực và làm cho chúng trở nên hữu ích trong thế giới thực. Như sơ đồ dưới đây minh họa, một trình xác minh hoàn thành tam giác tin cậy bằng cách yêu cầu, nhận và xác thực thông tin xác thực từ người nắm giữ.
Nếu bạn là một nhà phát triển, việc xây dựng một dịch vụ để thực hiện việc xác minh này là một kỹ năng nền tảng cho thế hệ ứng dụng an toàn và lấy người dùng làm trung tâm tiếp theo. Guide này được thiết kế để hướng dẫn bạn qua chính xác quy trình đó. Chúng ta sẽ đề cập đến mọi thứ bạn cần biết để xây dựng trình xác minh thông tin xác thực có thể xác minh của riêng bạn, từ các khái niệm và tiêu chuẩn cốt lõi đến các chi tiết triển khai từng bước về việc xác thực chữ ký và kiểm tra trạng thái thông tin xác thực.
Bạn muốn bỏ qua phần trước? Bạn có thể tìm thấy dự án hoàn chỉnh cho hướng dẫn này trên GitHub. Hãy thoải mái sao chép nó và tự mình thử nghiệm: https://github.com/corbado/digital-credentials-example
Hãy bắt đầu nào.
Trước khi bắt đầu, hãy đảm bảo bạn có:
Bây giờ chúng ta sẽ đi qua từng điều kiện tiên quyết này một cách chi tiết, bắt đầu với các tiêu chuẩn và giao thức làm nền tảng cho trình xác minh dựa trên mdoc này.
Trình xác minh của chúng ta được xây dựng cho các tiêu chuẩn/giao thức sau:
Tiêu chuẩn / Giao thức | Mô tả |
---|---|
W3C VC | Mô hình Dữ liệu Thông tin xác thực có thể xác minh của W3C. Nó định nghĩa cấu trúc tiêu chuẩn cho thông tin xác thực kỹ thuật số, bao gồm các claim, metadata và bằng chứng. |
SD-JWT | Tiết lộ có chọn lọc cho JWT. Một định dạng cho VC dựa trên JSON Web Tokens cho phép người nắm giữ chỉ tiết lộ có chọn lọc các claim cụ thể từ một thông tin xác thực, tăng cường quyền riêng tư. |
ISO mDoc | ISO/IEC 18013-5. Tiêu chuẩn quốc tế cho Giấy phép lái xe di động (mDLs) và các loại ID di động khác, định nghĩa cấu trúc dữ liệu và giao thức truyền thông cho việc sử dụng ngoại tuyến và trực tuyến. |
OpenID4VP | OpenID for Verifiable Presentations. Một giao thức trình bày có khả năng tương tác được xây dựng trên OAuth 2.0. Nó định nghĩa cách một trình xác minh yêu cầu thông tin xác thực và wallet của người nắm giữ trình bày chúng. |
Đối với hướng dẫn này, chúng ta đặc biệt sử dụng:
Lưu ý về phạm vi: Mặc dù chúng ta giới thiệu ngắn gọn về W3C VC và SD-JWT để cung cấp bối cảnh rộng hơn, hướng dẫn này chỉ triển khai thông tin xác thực ISO mDoc qua OpenID4VP. Các VC dựa trên W3C nằm ngoài phạm vi của ví dụ này.
Tiêu chuẩn ISO/IEC 18013-5 mDoc định nghĩa cấu trúc và mã hóa cho các tài liệu di động như giấy phép lái xe di động (mDLs). Thông tin xác thực mDoc được mã hóa bằng CBOR, được ký bằng mật mã và có thể được trình bày kỹ thuật số để xác minh. Trình xác minh của chúng ta sẽ tập trung vào việc giải mã và xác thực các thông tin xác thực mdoc này.
OpenID4VP là một giao thức có khả năng tương tác để yêu cầu và trình bày thông tin xác thực kỹ thuật số, được xây dựng trên OAuth 2.0 và OpenID Connect. Trong triển khai này, OpenID4VP được sử dụng để:
Bây giờ chúng ta đã hiểu rõ về các tiêu chuẩn và giao thức, chúng ta cần chọn tech stack phù hợp để xây dựng trình xác minh của mình. Các lựa chọn của chúng ta được thiết kế để đảm bảo sự mạnh mẽ, trải nghiệm nhà phát triển tốt và khả năng tương thích với hệ sinh thái web hiện đại.
Chúng ta sẽ sử dụng TypeScript cho cả mã frontend và backend. Là một tập hợp con mở rộng của JavaScript, nó thêm vào kiểu tĩnh, giúp phát hiện lỗi sớm, cải thiện chất lượng mã và làm cho các ứng dụng phức tạp dễ quản lý hơn. Trong bối cảnh nhạy cảm về bảo mật như xác minh thông tin xác thực, an toàn kiểu là một lợi thế lớn.
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.
redirect_uri
để nhận và xác minh phản hồi cuối cùng từ CMWallet một cách an
toàn.Việc triển khai của chúng ta dựa vào một bộ thư viện cụ thể cho frontend và backend:
Lưu ý về openid-client
: Các trình xác minh cấp sản xuất, tiên tiến hơn có thể sử
dụng thư viện openid-client
để xử lý trực tiếp giao thức OpenID4VP trên backend, cho
phép các tính năng như redirect_uri
động. Trong một luồng OpenID4VP do máy chủ điều
khiển với redirect_uri
, openid-client
sẽ được sử dụng để phân tích và xác thực trực
tiếp các phản hồi vp_token
. Đối với hướng dẫn này, chúng ta đang sử dụng một luồng đơn
giản hơn, qua trung gian trình duyệt mà không yêu cầu nó, làm cho quá trình dễ hiểu hơn.
Tech stack này đảm bảo một triển khai trình xác minh mạnh mẽ, an toàn về kiểu và có khả năng mở rộng, tập trung vào API Digital Credential của trình duyệt và định dạng thông tin xác thực ISO mDoc.
Để kiểm tra trình xác minh của bạn, bạn cần một wallet di động có thể tương tác với API Digital Credential của trình duyệt.
Chúng ta sẽ sử dụng CMWallet, một wallet thử nghiệm mạnh mẽ, tuân thủ OpenID4VP cho Android.
Cách cài đặt CMWallet (Android):
Lưu ý: Chỉ cài đặt các tệp APK từ các nguồn bạn tin cậy. Liên kết được cung cấp là từ kho lưu trữ dự án chính thức.
Trước khi chúng ta đi sâu vào việc triển khai, điều cần thiết là phải hiểu các khái niệm mật mã học làm nền tảng cho thông tin xác thực có thể xác minh. Đây là điều làm cho chúng "có thể xác minh" và đáng tin cậy.
Về cơ bản, một Verifiable Credential là một tập hợp các claim (như tên, ngày sinh, v.v.) đã được ký điện tử bởi một bên phát hành. Một chữ ký số cung cấp hai đảm bảo quan trọng:
Chữ ký số được tạo bằng mật mã hóa khóa công khai/khóa riêng tư (còn gọi là mật mã hóa bất đối xứng). Đây là cách nó hoạt động trong bối cảnh của chúng ta:
Lưu ý về DID: Trong hướng dẫn này, chúng ta không giải quyết các khóa của bên phát hành thông qua DID. Trong môi trường sản xuất, các bên phát hành thường sẽ công khai các khóa công khai của họ qua DID hoặc các điểm cuối có thẩm quyền khác, mà trình xác minh sẽ sử dụng để xác thực mật mã.
Verifiable Credentials thường được định dạng dưới dạng JSON Web Tokens (JWTs). Một JWT
là một cách nhỏ gọn, an toàn cho URL để biểu diễn các claim được chuyển giữa hai bên. Một
JWT đã ký (còn được gọi là JWS) có ba phần được phân tách bằng dấu chấm (.
):
alg
).vc
claim), bao gồm issuer
,
credentialSubject
, v.v.// Ví dụ về cấu trúc JWT [Header].[Payload].[Signature]
Lưu ý: Verifiable Credentials dựa trên JWT nằm ngoài phạm vi của bài đăng blog này. Việc triển khai này tập trung vào thông tin xác thực ISO mDoc và OpenID4VP, không phải W3C Verifiable Credentials hay thông tin xác thực dựa trên JWT.
Việc một trình xác minh biết rằng một thông tin xác thực là hợp lệ là chưa đủ; nó cũng cần biết rằng người trình bày thông tin xác thực là người nắm giữ hợp pháp. Điều này ngăn chặn ai đó sử dụng một thông tin xác thực bị đánh cắp.
Điều này được giải quyết bằng cách sử dụng Verifiable Presentation (VP). Một VP là một lớp bọc xung quanh một hoặc nhiều VC được ký bởi chính người nắm giữ.
Luồng diễn ra như sau:
Trình xác minh của chúng ta sau đó phải thực hiện hai lần kiểm tra chữ ký riêng biệt:
Việc kiểm tra hai cấp độ này đảm bảo cả tính xác thực của thông tin xác thực và danh tính của người trình bày nó, tạo ra một mô hình tin cậy mạnh mẽ và an toàn.
Lưu ý: Khái niệm Verifiable Presentations như được định nghĩa trong hệ sinh thái W3C
VC nằm ngoài phạm vi của bài đăng blog này. Thuật ngữ
Verifiable Presentation ở đây đề cập đến phản hồi
vp_token
của OpenID4VP, hoạt động tương tự như một W3C VP nhưng dựa trên ngữ nghĩa của
ISO mDoc thay vì mô hình chữ ký JSON-LD của W3C. Guide này tập
trung vào thông tin xác thực ISO mDoc và OpenID4VP, không phải W3C Verifiable
Presentations hay việc xác thực chữ ký của chúng.
Kiến trúc trình xác minh của chúng ta sử dụng API Digital Credential tích hợp của trình duyệt làm trung gian an toàn để kết nối ứng dụng web của chúng ta với CMWallet di động của người dùng. Cách tiếp cận này đơn giản hóa luồng bằng cách để trình duyệt xử lý việc hiển thị mã QR gốc và giao tiếp với wallet.
navigator.credentials.get()
của trình duyệt, nhận kết quả và chuyển tiếp nó đến
backend để xác minh.openid4vp
và tự động tạo mã QR. Sau đó, nó chờ wallet trả
về một phản hồi.Dưới đây là một sơ đồ tuần tự minh họa luồng hoàn chỉnh và chính xác:
Giải thích luồng:
/api/verify/start
),
backend này tạo một đối tượng yêu cầu chứa truy vấn và một nonce, sau đó trả về nó.navigator.credentials.get()
với đối tượng yêu
cầu.openid4vp
và tự động hiển thị
một mã QR. Lời hứa (promise) .get()
bây giờ đang ở trạng thái chờ.Lưu ý: Luồng mã QR này xảy ra trên các trình duyệt máy tính để bàn. Trên các trình
duyệt di động (Android Chrome với cờ thử nghiệm được bật), trình duyệt có thể giao tiếp
trực tiếp với các wallet tương thích trên cùng một
thiết bị, loại bỏ nhu cầu quét mã QR. Để bật tính năng này trên Android Chrome, hãy điều
hướng đến chrome://flags#web-identity-digital-credentials
và đặt cờ thành "Enabled".
.get()
ban
đầu trên frontend cuối cùng được giải quyết, cung cấp payload của bản trình bày./api/verify/finish
của backend. Backend xác thực nonce và thông tin xác thực.Bây giờ chúng ta đã hiểu rõ về các tiêu chuẩn, giao thức và luồng kiến trúc, chúng ta có thể bắt đầu xây dựng trình xác minh 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ã. 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 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ý việc giải mã CBOR, kết nối cơ sở dữ liệu và tạo UUID.
npm install cbor-web mysql2 uuid @types/uuid
Lệnh này cài đặt:
cbor-web
: Để giải mã payload thông tin xác thực mdoc.mysql2
: Client MySQL cho cơ sở dữ liệu của chúng ta.uuid
: Để tạo các chuỗi challenge 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ữ dữ liệu phiên OIDC, đảm
bảo rằng mỗi luồng xác minh đều an toàn và có trạng thái. Chúng ta đã bao gồm một tệp
docker-compose.yml
để làm cho việc 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:
-- 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) );
Khi cả hai tệp đã ở đúng vị trí, hãy mở terminal của bạn trong 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.
Ứ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.
src/app/page.tsx
): Một trang React duy nhất khởi
tạo luồng xác minh và hiển thị kết quả. Nó tương tác với API Digital Credential của
trình duyệt.src/app/api/verify/...
):
start/route.ts
: Tạo yêu cầu OpenID4VP và một nonce
bảo mật.finish/route.ts
: Nhận bản trình bày từ wallet (qua trình duyệt), xác thực nonce và
giải mã thông tin xác thực.src/lib/
):
database.ts
: Quản lý tất cả các tương tác cơ sở dữ liệu (tạo challenge, xác minh
phiên).crypto.ts
: Xử lý việc giải mã thông tin xác thực mDoc dựa trên CBOR.Dưới đây là một sơ đồ minh họa kiến trúc nội bộ:
Frontend của chúng ta được cố ý làm nhẹ. Trách nhiệm chính của nó là hoạt động như một trình kích hoạt hướng tới người dùng cho luồng xác minh và giao tiếp với cả backend của chúng ta và khả năng xử lý thông tin xác thực gốc của trình duyệt. Nó không chứa bất kỳ logic giao thức phức tạp nào; tất cả đều được ủy quyền.
Cụ thể, frontend sẽ xử lý những việc sau:
/api/verify/start
và nhận một payload JSON có cấu
trúc (protocol
, request
, state
) mô tả chính xác những gì wallet nên trình bày.navigator.credentials.get()
, đối
tượng này sẽ hiển thị một mã QR gốc và chờ phản hồi của wallet./api/verify/finish
của chúng ta trong một yêu cầu POST
để xác thực cuối cùng phía máy chủ.Logic cốt lõi nằm trong hàm startVerification
:
// src/app/page.tsx const startVerification = async () => { setLoading(true); setVerificationResult(null); try { // 1. Check if the browser supports the API if (!navigator.credentials?.get) { throw new Error("Browser does not support the Credential API."); } // 2. Ask our backend for a request object const res = await fetch("/api/verify/start"); const { protocol, request } = await res.json(); // 3. Hand that object to the browser – this triggers the native QR code const credential = await (navigator.credentials as any).get({ mediation: "required", digital: { requests: [ { protocol, // "openid4vp" data: request, // contains dcql_query, nonce, etc. }, ], }, }); // 4. Forward the wallet response (from the browser) to our finish endpoint for server-side checks const verifyRes = await fetch("/api/verify/finish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(credential), }); const result = await verifyRes.json(); if (verifyRes.ok && result.verified) { setVerificationResult(`Success: ${result.message}`); } else { throw new Error(result.message || "Verification failed."); } } catch (err) { setVerificationResult(`Error: ${(err as Error).message}`); } finally { setLoading(false); } };
Hàm này cho thấy bốn bước chính của logic frontend: kiểm tra hỗ trợ API, lấy yêu cầu từ backend, gọi API trình duyệt và gửi kết quả trở lại để xác minh. Phần còn lại của tệp là boilerplate React tiêu chuẩn để quản lý trạng thái và hiển thị giao diện người dùng, bạn có thể xem trong kho lưu trữ GitHub.
digital
và mediation: 'required'
?#Bạn có thể nhận thấy lệnh gọi navigator.credentials.get()
của chúng ta trông khác với
các ví dụ đơn giản hơn. Điều này là do chúng ta tuân thủ nghiêm ngặt
đặc tả W3C Digital Credentials API
chính thức.
Thành viên digital
: Đặc tả yêu cầu tất cả các yêu cầu thông tin xác thực kỹ thuật
số phải được lồng bên trong một đối tượng digital
. Điều này cung cấp một không gian
tên rõ ràng, được tiêu chuẩn hóa cho API này, phân biệt nó với các loại thông tin xác
thực khác (như password
hoặc federated
) và cho phép các phần mở rộng trong tương lai
mà không có xung đột.
mediation: 'required'
: Tùy chọn này là một tính năng
bảo mật và trải nghiệm người dùng quan trọng.
Nó bắt buộc người dùng phải tương tác tích cực với một lời nhắc (ví dụ: quét sinh trắc
học, nhập mã PIN hoặc màn hình đồng ý) để phê duyệt yêu cầu thông tin xác thực. Nếu
không có nó, một trang web có thể cố gắng truy cập thông tin xác thực một cách âm thầm
trong nền, điều này gây ra rủi ro riêng tư đáng kể. Bằng cách yêu cầu mediation, chúng
ta đảm bảo rằng người dùng luôn kiểm soát và đưa ra sự đồng ý rõ ràng cho mỗi giao dịch.
Với giao diện người dùng React đã sẵn sàng, bây giờ chúng ta cần hai API route thực hiện công việc nặng nhọc trên máy chủ:
/api/verify/start
– xây dựng một yêu cầu OpenID4VP, lưu trữ một challenge sử dụng
một lần trong MySQL và giao mọi thứ lại cho trình duyệt./api/verify/finish
– nhận phản hồi từ wallet, xác thực challenge, xác minh & giải
mã thông tin xác thực, và cuối cùng trả về một kết quả JSON ngắn gọn cho giao diện
người dùng./api/verify/start
: Tạo yêu cầu OpenID4VP#// src/app/api/verify/start/route.ts import { NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { createChallenge, cleanupExpiredChallenges } from "@/lib/database"; export async function GET() { // 1️⃣ Create a short-lived, random nonce (challenge) const challenge = uuidv4(); const challengeId = uuidv4(); const expiresAt = new Date(Date.now() + 5 * 60 * 1000); await createChallenge(challengeId, challenge, expiresAt); cleanupExpiredChallenges().catch(console.error); // 2️⃣ Build a DCQL query that describes *what* we want const dcqlQuery = { credentials: [ { id: "cred1", format: "mso_mdoc", meta: { doctype_value: "eu.europa.ec.eudi.pid.1" }, claims: [ { path: ["eu.europa.ec.eudi.pid.1", "family_name"] }, { path: ["eu.europa.ec.eudi.pid.1", "given_name"] }, { path: ["eu.europa.ec.eudi.pid.1", "birth_date"] }, ], }, ], }; // 3️⃣ Return an object the browser can pass to navigator.credentials.get() return NextResponse.json({ protocol: "openid4vp", // tells the browser which wallet protocol to use request: { dcql_query: dcqlQuery, // WHAT to present nonce: challenge, // anti-replay response_type: "vp_token", response_mode: "dc_api", // the wallet will POST directly to /finish }, state: { credential_type: "mso_mdoc", // kept for later checks nonce: challenge, challenge_id: challengeId, }, }); }
Các tham số chính
• nonce
– thử thách mật mã liên kết yêu cầu &
phản hồi (ngăn chặn tấn công phát lại). • dcql_query
– Một đối tượng mô tả chính
xác các claim chúng ta cần. Đối với guide này, chúng ta sử dụng cấu trúc dcql_query
lấy cảm hứng từ các bản nháp gần đây của Digital Credential Query Language, mặc dù đây
chưa phải là một tiêu chuẩn hoàn thiện. • state
– JSON tùy ý được wallet phản hồi
lại để chúng ta có thể tra cứu bản ghi trong DB.
Tệp src/lib/database.ts
bao bọc các hoạt động MySQL cơ bản cho các challenge & phiên xác
minh (chèn, đọc, đánh dấu đã sử dụng). Việc giữ logic này trong một module duy nhất giúp
dễ dàng thay đổi kho dữ liệu sau này.
/api/verify/finish
: Xác thực & Giải mã Presentation#// src/app/api/verify/finish/route.ts import { NextResponse, NextRequest } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { getChallenge, markChallengeAsUsed, createVerificationSession, updateVerificationSession, } from "@/lib/database"; import { decodeDigitalCredential, decodeAllNamespaces } from "@/lib/crypto"; export async function POST(request: NextRequest) { const body = await request.json(); // 1️⃣ Extract the verifiable presentation pieces const vpTokenMap = body.vp_token ?? body.data?.vp_token; const state = body.state; const mdocToken = vpTokenMap?.cred1; // we asked for this ID in dcqlQuery if (!vpTokenMap || !state || !mdocToken) { return NextResponse.json( { verified: false, message: "Malformed response" }, { status: 400 }, ); } // 2️⃣ One-time-use challenge validation const stored = await getChallenge(state.nonce); if (!stored) { return NextResponse.json( { verified: false, message: "Invalid or expired challenge" }, { status: 400 }, ); } const sessionId = uuidv4(); await createVerificationSession(sessionId, stored.id); // 3️⃣ (Pseudo) cryptographic checks – replace with real mDL validation in prod // In a real application, you would use a dedicated library to perform full // cryptographic validation of the mdoc signature against the issuer's public key. const isValid = mdocToken.length > 0; if (!isValid) { await updateVerificationSession(sessionId, "failed", { reason: "mdoc validation failed", }); return NextResponse.json( { verified: false, message: "Credential validation failed" }, { status: 400 }, ); } // 4️⃣ Decode the mobile-DL (mdoc) payload into human readable JSON const decoded = await decodeDigitalCredential(mdocToken); const readable = decodeAllNamespaces(decoded)["eu.europa.ec.eudi.pid.1"]; await markChallengeAsUsed(state.nonce); await updateVerificationSession(sessionId, "verified", { readable }); return NextResponse.json({ verified: true, message: "mdoc credential verified successfully!", credentialData: readable, sessionId, }); }
Các trường quan trọng trong phản hồi của wallet
• vp_token
– map chứa mỗi thông tin xác thực mà wallet trả về. Đối với demo của
chúng ta, chúng ta lấy vp_token.cred1
. • state
– bản sao của blob chúng ta đã
cung cấp trong /start
; chứa nonce
để chúng ta có thể tra cứu bản ghi trong DB. •
mdocToken
– một cấu trúc CBOR được mã hóa Base64URL đại diện cho ISO mDoc.
Khi trình xác minh nhận được một thông tin xác thực mdoc từ trình duyệt, nó là một chuỗi
Base64URL chứa dữ liệu nhị phân được mã hóa CBOR. Để trích xuất các claim thực tế,
endpoint finish
thực hiện một quy trình giải mã nhiều bước bằng cách sử dụng các hàm trợ
giúp từ src/lib/crypto.ts
.
Hàm decodeDigitalCredential
xử lý việc chuyển đổi từ chuỗi đã mã hóa sang một đối tượng
có thể sử dụng được:
// src/lib/crypto.ts export async function decodeDigitalCredential(encodedCredential: string) { // 1. Convert Base64URL to standard Base64 const base64UrlToBase64 = (input: string) => { let base64 = input.replace(/-/g, "+").replace(/_/g, "/"); const pad = base64.length % 4; if (pad) base64 += "=".repeat(4 - pad); return base64; }; const base64 = base64UrlToBase64(encodedCredential); // 2. Decode Base64 to binary const binaryString = atob(base64); const byteArray = Uint8Array.from(binaryString, (char) => char.charCodeAt(0)); // 3. Decode CBOR const decoded = await cbor.decodeFirst(byteArray); return decoded; }
cbor-web
để giải mã dữ liệu nhị phân thành một đối
tượng JavaScript có cấu trúc.Hàm decodeAllNamespaces
xử lý thêm đối tượng CBOR đã giải mã để trích xuất các claim
thực tế từ các namespace liên quan:
// src/lib/crypto.ts export function decodeAllNamespaces(jsonObj) { const decoded = {}; try { jsonObj.documents.forEach((doc, idx) => { // 1) issuerSigned.nameSpaces: const issuerNS = doc.issuerSigned?.nameSpaces || {}; Object.entries(issuerNS).forEach(([nsName, entries]) => { if (!decoded[nsName]) decoded[nsName] = {}; (entries as any[]).forEach((entry) => { const bytes = Uint8Array.from(entry.value); const decodedEntry = cbor.decodeFirstSync(bytes); Object.assign(decoded[nsName], decodedEntry); }); }); // 2) deviceSigned.nameSpaces (if present): const deviceNS = doc.deviceSigned?.nameSpaces; if (deviceNS?.value?.data) { const bytes = Uint8Array.from(deviceNS.value); decoded[`deviceSigned_ns_${idx}`] = cbor.decodeFirstSync(bytes); } }); } catch (e) { console.error(e); } return decoded; }
eu.europa.ec.eudi.pid.1
) để trích xuất các giá trị
claim thực tế (như tên, ngày sinh, v.v.).Sau khi chạy qua các bước này, endpoint finish thu được một đối tượng có thể đọc được chứa các claim từ mdoc, ví dụ:
{ "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" }
Quá trình này đảm bảo rằng trình xác minh có thể trích xuất thông tin cần thiết từ thông tin xác thực mdoc một cách an toàn và đáng tin cậy để hiển thị và xử lý thêm.
Endpoint finish trả về một đối tượng JSON tối thiểu cho frontend:
{ "verified": true, "message": "mdoc credential verified successfully!", "credentialData": { "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" } }
Frontend nhận phản hồi này trong startVerification()
và chỉ cần lưu nó vào trạng thái
React để chúng ta có thể hiển thị một thẻ xác nhận đẹp mắt hoặc hiển thị các claim riêng
lẻ – ví dụ: “Chào mừng, John Doe (sinh ngày 1990-01-01)!”.
Bây giờ bạn đã có một trình xác minh hoàn chỉnh, hoạt động, sử dụng khả năng xử lý thông tin xác thực gốc của trình duyệt. Dưới đây là cách chạy nó cục bộ và những gì bạn có thể làm để đưa nó từ một bằng chứng khái niệm (proof-of-concept) thành một ứng dụng sẵn sàng cho môi trường 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 trên máy của bạn, sau đó khởi động container MySQL:
docker-compose up -d
Chạy ứng dụng:
npm run dev
Mở trình duyệt của bạn đến http://localhost:3000
, và bạn sẽ thấy giao diện người
dùng của trình xác minh. Bây giờ bạn có thể sử dụng CMWallet của mình để quét mã QR và
hoàn thành luồng xác minh.
Hướng dẫn này cung cấp các khối xây dựng nền tảng cho một trình xác minh. Để làm cho nó sẵn sàng cho môi trường sản xuất, bạn cần triển khai một số tính năng bổ sung:
Xác thực mật mã đầy đủ: Triển khai hiện tại sử dụng một kiểm tra giữ chỗ
(mdocToken.length > 0
). Trong một kịch bản thực tế, bạn phải thực hiện xác thực mật mã
đầy đủ của chữ ký mdoc đối với khóa công khai của bên phát hành (ví
dụ: bằng cách giải quyết DID của họ hoặc lấy chứng chỉ khóa công khai của họ). Để biết
các tiêu chuẩn giải quyết DID, hãy tham khảo
đặc tả W3C DID Resolution.
Kiểm tra thu hồi của bên phát hành: Thông tin xác thực có thể bị bên phát hành thu hồi trước ngày hết hạn. Một trình xác minh sản xuất phải kiểm tra trạng thái của thông tin xác thực bằng cách truy vấn danh sách thu hồi hoặc endpoint trạng thái do bên phát hành cung cấp. Danh sách trạng thái Verifiable Credentials của W3C cung cấp tiêu chuẩn cho danh sách thu hồi thông tin xác thực.
Xử lý lỗi & Bảo mật mạnh mẽ: Thêm xử lý lỗi toàn diện, xác thực đầu vào, giới hạn tốc độ trên các endpoint API và đảm bảo tất cả giao tiếp đều qua HTTPS (TLS) để bảo vệ dữ liệu khi truyền. Hướng dẫn bảo mật API của OWASP cung cấp các phương pháp tốt nhất về bảo mật API toàn diện.
Hỗ trợ nhiều loại thông tin xác thực: Mở rộng logic để xử lý các giá trị doctype
và định dạng thông tin xác thực khác nhau nếu bạn dự kiến sẽ nhận được nhiều hơn chỉ
thông tin xác thực PID của Digital Identity Châu Âu
(EUDI).
Mô hình dữ liệu Verifiable Credentials của W3C
cung cấp các đặc tả định dạng VC toàn diện.
Ví dụ này được cố ý tập trung vào luồng cốt lõi qua trung gian trình duyệt để làm cho nó dễ hiểu. Các chủ đề sau được coi là nằm ngoài phạm vi:
redirect_uri
hoặc đăng ký client động.Bằng cách xây dựng trên nền tảng này và kết hợp các bước tiếp theo này, bạn có thể phát triển một trình xác minh mạnh mẽ và an toàn, có khả năng tin cậy và xác thực thông tin xác thực kỹ thuật số trong các ứng dụng của riêng bạn.
Vậy là xong! Với ít hơn 250 dòng TypeScript, bây giờ chúng ta có một trình xác minh end-to-end có thể:
Trong môi trường sản xuất, bạn sẽ thay thế việc xác thực giữ chỗ bằng các kiểm tra ISO 18013-5 đầy đủ, thêm tra cứu thu hồi của bên phát hành, giới hạn tốc độ, ghi nhật ký kiểm toán, và tất nhiên là TLS end-to-end—nhưng các khối xây dựng cốt lõi vẫn giữ nguyên.
Dưới đây là một số tài liệu, đặc 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 đặc tả chính:
Công cụ:
Thư viện:
Related Articles
Table of Contents