Lernen Sie, wie man einen Issuer für W3C Verifiable Credentials mit dem OpenID4VCI-Protokoll erstellt. Diese Schritt-für-Schritt-Anleitung zeigt, wie eine Next.js-Anwendung entwickelt wird, die kryptografisch signierte Nachweise ausstellt, die mit digital
Amine
Created: August 20, 2025
Updated: August 21, 2025
See the original blog version in English here.
Digitale Nachweise sind eine leistungsstarke Methode, um Identität und Ansprüche auf eine sichere und datenschutzfreundliche Weise zu belegen. Aber wie erhalten Nutzer diese Nachweise überhaupt? Hier wird die Rolle des Issuers entscheidend. Ein Issuer ist eine vertrauenswürdige Instanz – wie zum Beispiel eine Regierungsbehörde, eine Universität oder eine Bank –, die für die Erstellung und Verteilung von digital signierten Nachweisen an Nutzer verantwortlich ist.
Dieser Guide bietet eine umfassende Schritt-für-Schritt-Anleitung zur Erstellung eines Issuers für digitale Nachweise. Wir konzentrieren uns auf das Protokoll OpenID for Verifiable Credential Issuance (OpenID4VCI), einen modernen Standard, der festlegt, wie Nutzer Nachweise von einem Issuer erhalten und sicher in ihren digitalen Wallets speichern können.
Das Endergebnis wird eine funktionierende Next.js-Anwendung sein, die Folgendes kann:
Recent Articles
📝
So erstellst du einen Verifier für digitale Nachweise (Entwickler-Guide)
📝
Wie man einen Issuer für digitale Nachweise erstellt (Entwickler-Guide)
📖
WebAuthn Resident Keys: Discoverable Credentials als Passkeys
🔑
Physischer Zutritt per Badge & Passkeys: Ein technischer Guide
🔑
MFA-Pflicht & der Umstieg auf Passkeys: Best Practices
Bevor wir fortfahren, ist es wichtig, den Unterschied zwischen zwei verwandten, aber unterschiedlichen Konzepten zu klären:
Digitale Nachweise (allgemeiner Begriff): Dies ist eine breite Kategorie, die jede digitale Form von Nachweisen, Zertifikaten oder Bestätigungen (Attestations) umfasst. Dazu können einfache digitale Zertifikate, grundlegende digitale Badges oder jeder elektronisch gespeicherte Nachweis gehören, der kryptografische Sicherheitsmerkmale haben kann oder auch nicht.
Verifiable Credentials (VCs – W3C-Standard): Dies ist ein spezifischer Typ digitaler Nachweise, der dem W3C-Standard für das „Verifiable Credentials Data Model“ folgt. Verifiable Credentials sind kryptografisch signierte, manipulationssichere und datenschutzfreundliche Nachweise, die unabhängig überprüft werden können. Sie beinhalten spezifische technische Anforderungen wie:
In diesem Guide erstellen wir speziell einen Issuer für Verifiable Credentials, der dem W3C-Standard folgt, nicht nur ein beliebiges System für digitale Nachweise. Das von uns verwendete OpenID4VCI-Protokoll ist speziell für die Ausstellung von Verifiable Credentials konzipiert, und das JWT-VC-Format, das wir implementieren werden, ist ein W3C-konformes Format für Verifiable Credentials.
Die Magie hinter digitalen Nachweisen liegt in einem einfachen, aber leistungsstarken „Vertrauensdreieck“-Modell mit drei Hauptakteuren:
Der Ausstellungsprozess ist der erste Schritt in diesem Ökosystem. Der Issuer validiert die Informationen des Nutzers und stellt ihm einen Nachweis zur Verfügung. Sobald der Holder diesen Nachweis in seiner Wallet hat, kann er ihn einem Verifier vorlegen, um seine Identität oder Ansprüche nachzuweisen, was das Dreieck schließt.
Hier ist ein kurzer Blick auf die fertige Anwendung in Aktion:
Schritt 1: Eingabe der Nutzerdaten Der Nutzer füllt ein Formular mit seinen persönlichen Informationen aus, um einen neuen Nachweis anzufordern.
Schritt 2: Erstellung des Credential-Angebots Die Anwendung generiert ein sicheres Credential-Angebot, das als QR-Code und Pre-Authorized-Code angezeigt wird.
Schritt 3: Interaktion mit der Wallet Der Nutzer scannt den QR-Code mit einer kompatiblen Wallet (z. B. Sphereon Wallet) und gibt eine PIN ein, um die Ausstellung zu autorisieren.
Schritt 4: Nachweis ausgestellt Die Wallet empfängt und speichert den neu ausgestellten digitalen Nachweis, bereit für die zukünftige Verwendung.
Bevor wir uns den Code ansehen, gehen wir die Grundlagen und Tools durch, die wir benötigen. Dieser Guide setzt grundlegende Kenntnisse in der Webentwicklung voraus, aber die folgenden Voraussetzungen sind für die Erstellung eines Credential-Issuers unerlässlich.
Unser Issuer basiert auf einer Reihe offener Standards, die die Interoperabilität zwischen Wallets und Ausstellungsdiensten gewährleisten. Für dieses Tutorial konzentrieren wir uns auf Folgendes:
Standard / Protokoll | Beschreibung |
---|---|
OpenID4VCI | OpenID for Verifiable Credential Issuance. Dies ist das Kernprotokoll, das wir verwenden werden. Es definiert einen Standardablauf, wie ein Nutzer (über seine Wallet) einen Nachweis von einem Issuer anfordern und erhalten kann. |
JWT-VC | JWT-basierte Verifiable Credentials. Das Format für den Nachweis, den wir ausstellen werden. Es ist ein W3C-Standard, der Verifiable Credentials als JSON Web Tokens (JWTs) kodiert, was sie kompakt und webfreundlich macht. |
ISO mDoc | ISO/IEC 18013-5. Der internationale Standard für mobile Führerscheine (mDLs). Obwohl wir ein JWT-VC ausstellen, sind die Claims darin so strukturiert, dass sie mit dem mDoc-Datenmodell kompatibel sind (z. B. eu.europa.ec.eudi.pid.1 ). |
OAuth 2.0 | Das zugrunde liegende Autorisierungs-Framework, das von OpenID4VCI verwendet wird. Wir werden einen pre-authorized_code -Flow implementieren, der ein spezifischer Grant-Typ ist, der für eine sichere und benutzerfreundliche Ausstellung von Nachweisen entwickelt wurde. |
OpenID4VCI unterstützt zwei primäre Autorisierungs-Flows für die Ausstellung von Nachweisen:
Pre-Authorized Code Flow: In diesem Flow generiert der Issuer einen kurzlebigen
Einmal-Code (pre-authorized_code
), der dem Nutzer sofort zur Verfügung steht. Die
Wallet des Nutzers kann diesen Code dann direkt gegen einen Nachweis eintauschen.
Dieser Flow ist ideal für Szenarien, in denen der Nutzer bereits authentifiziert und
auf der Website des Issuers präsent ist, da er eine nahtlose, sofortige Ausstellung
ohne Umleitungen ermöglicht.
Authorization Code Flow: Dies ist der Standard-OAuth 2.0-Flow,
bei dem der Nutzer zu einem Autorisierungsserver weitergeleitet wird, um seine
Zustimmung zu erteilen. Nach der Genehmigung sendet der Server einen
authorization_code
an eine registrierte redirect_uri
zurück. Dieser Flow eignet
sich besser für Drittanwendungen, die den Ausstellungsprozess im Namen des Nutzers
initiieren.
Für dieses Tutorial verwenden wir den pre-authorized_code
-Flow. Wir haben diesen
Ansatz gewählt, weil er einfacher ist und eine direktere Benutzererfahrung für unseren
spezifischen Anwendungsfall bietet: Ein Nutzer fordert direkt einen Nachweis von der
eigenen Website des Issuers an. Er eliminiert die Notwendigkeit komplexer Umleitungen und
Client-Registrierungen, was die Kernlogik der Ausstellung einfacher zu verstehen und zu
implementieren macht.
Diese Kombination von Standards ermöglicht es uns, einen Issuer zu erstellen, der mit einer Vielzahl von digitalen Wallets kompatibel ist und einen sicheren, standardisierten Prozess für den Nutzer gewährleistet.
Um unseren Issuer zu erstellen, verwenden wir denselben robusten und modernen Tech-Stack, den wir auch für den Verifier verwendet haben, um eine konsistente und hochwertige Entwicklererfahrung zu gewährleisten.
Wir werden TypeScript sowohl für unseren Frontend- als auch für unseren Backend-Code verwenden. Seine statische Typisierung ist in einer sicherheitskritischen Anwendung wie einem Issuer von unschätzbarem Wert, da sie hilft, häufige Fehler zu vermeiden und die allgemeine Qualität und Wartbarkeit des Codes zu verbessern.
Next.js ist unser bevorzugtes Framework, da es eine nahtlose, integrierte Erfahrung für die Erstellung von Full-Stack-Anwendungen bietet.
Unsere Implementierung wird auf einige wichtige Bibliotheken zurückgreifen, um spezifische Aufgaben zu erledigen:
pre-authorized_code
-Werten verwenden werden.Um Ihren Issuer zu testen, benötigen Sie eine mobile Wallet, die das OpenID4VCI-Protokoll unterstützt. Für dieses Tutorial empfehlen wir die Sphereon Wallet, die sowohl für Android als auch für iOS verfügbar ist.
So installieren Sie die Sphereon Wallet:
Die Ausstellung eines Nachweises ist ein sicherheitskritischer Vorgang, der auf grundlegenden kryptografischen Konzepten beruht, um Vertrauen und Authentizität zu gewährleisten.
Im Kern ist ein Verifiable Credential eine Reihe von Behauptungen (Claims), die vom Issuer digital signiert wurden. Diese Signatur bietet zwei Garantien:
Digitale Signaturen werden mittels Public-Key-Kryptografie erstellt. So funktioniert es:
In unserer Implementierung generieren wir ein Elliptic Curve (EC)-Schlüsselpaar und
verwenden den ES256
-Algorithmus, um das JWT-VC zu signieren. Der öffentliche Schlüssel
ist in der DID (did:web
) des Issuers eingebettet, sodass jeder Verifier ihn entdecken
und die Signatur des Nachweises validieren kann. Hinweis: Der aud
(Audience)-Claim
wird in unseren JWTs absichtlich weggelassen, da der Nachweis für allgemeine Zwecke
konzipiert ist und nicht an eine bestimmte Wallet gebunden sein soll. Wenn Sie die Nutzung
auf eine bestimmte Zielgruppe beschränken möchten, fügen Sie einen aud
-Claim hinzu und
setzen Sie ihn entsprechend.
Unsere Issuer-Anwendung ist als Full-Stack-Next.js-Projekt aufgebaut, mit einer klaren
Trennung zwischen Frontend- und Backend-Logik. Diese Architektur ermöglicht es uns, eine
nahtlose Benutzererfahrung zu schaffen, während alle sicherheitskritischen Operationen auf
dem Server abgewickelt werden. Wichtig: Die enthaltenen Tabellen
verification_sessions
und verified_credentials
im SQL sind für diesen Issuer nicht
erforderlich, werden aber der Vollständigkeit halber aufgeführt.
src/app/issue/page.tsx
): Eine einzelne
React-Seite, auf der Nutzer ihre Daten eingeben können, um einen
Nachweis anzufordern. Sie macht API-Aufrufe an unser Backend, um den Ausstellungsprozess
zu initiieren.src/app/api/issue/...
): Eine Reihe von serverseitigen
Endpunkten, die das OpenID4VCI-Protokoll implementieren.
/.well-known/openid-credential-issuer
: Ein öffentlicher Metadaten-Endpunkt. Dies
ist die erste URL, die eine Wallet überprüft, um die Fähigkeiten des Issuers zu
entdecken, einschließlich seines Autorisierungsservers, Token-Endpunkts,
Credential-Endpunkts und der Arten von Nachweisen, die er anbietet./.well-known/openid-configuration
: Ein Standard-OpenID-Connect-Discovery-Endpunkt.
Obwohl eng mit dem obigen verwandt, dient dieser Endpunkt einer breiteren
OIDC-bezogenen Konfiguration und ist oft für die Interoperabilität mit
Standard-OpenID-Clients erforderlich./.well-known/did.json
: Das DID-Dokument für unseren Issuer. Bei Verwendung der
did:web
-Methode wird diese Datei verwendet, um die öffentlichen Schlüssel des
Issuers zu veröffentlichen, die Verifier zur Validierung der Signaturen der von ihm
ausgestellten Nachweise verwenden können.authorize/route.ts
: Erstellt einen pre-authorized_code
und ein
Credential-Angebot.token/route.ts
: Tauscht den pre-authorized_code
gegen einen
Access Token ein.credential/route.ts
: Stellt das endgültige, kryptografisch signierte JWT-VC aus.schemas/pid/route.ts
: Stellt das JSON-Schema für den PID-Nachweis bereit. Dies
ermöglicht es jedem Konsumenten des Nachweises, dessen Struktur und Datentypen zu
verstehen.src/lib/
):
database.ts
: Verwaltet alle Datenbankinteraktionen, wie das Speichern von
Autorisierungscodes und Issuer-Schlüsseln.crypto.ts
: Handhabt alle kryptografischen Operationen, einschließlich der
Schlüsselgenerierung und JWT-Signierung.Hier ist ein Diagramm, das den Ausstellungsprozess veranschaulicht:
Nachdem wir nun ein solides Verständnis der Standards, Protokolle und Architektur haben, können wir mit der Erstellung unseres Issuers beginnen.
Mitmachen oder den fertigen Code verwenden
Wir werden nun Schritt für Schritt durch die Einrichtung und Code-Implementierung gehen. Wenn Sie lieber direkt zum fertigen Produkt springen möchten, können Sie das vollständige Projekt aus unserem GitHub-Repository klonen und lokal ausführen.
git clone https://github.com/corbado/digital-credentials-example.git
Zuerst initialisieren wir ein neues Next.js-Projekt, installieren die notwendigen Abhängigkeiten und starten unsere Datenbank.
Öffnen Sie Ihr Terminal, navigieren Sie zu dem Verzeichnis, in dem Sie Ihr Projekt erstellen möchten, und führen Sie den folgenden Befehl aus. Wir verwenden für dieses Projekt den App Router, TypeScript und Tailwind CSS.
npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm
Dieser Befehl erstellt eine neue Next.js-Anwendung in Ihrem aktuellen Verzeichnis.
Als Nächstes müssen wir die Bibliotheken installieren, die sich um JWTs, Datenbankverbindungen und die Generierung von UUIDs kümmern.
npm install jose mysql2 uuid @types/uuid
Dieser Befehl installiert:
jose
: Zum Signieren und Verifizieren von JSON Web Tokens (JWTs).mysql2
: Den MySQL-Client für unsere
Datenbank.uuid
: Zur Generierung eindeutiger Challenge-Strings.@types/uuid
: TypeScript-Typen für die uuid
-Bibliothek.Unser Backend benötigt eine MySQL-Datenbank, um
Autorisierungscodes, Ausstellungssitzungen und Issuer-Schlüssel zu speichern. Wir haben
eine docker-compose.yml
-Datei beigefügt, um dies zu vereinfachen.
Wenn Sie das Repository geklont haben, können Sie einfach docker-compose up -d
ausführen. Wenn Sie von Grund auf neu bauen, erstellen Sie eine Datei namens
docker-compose.yml
mit dem folgenden Inhalt:
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:
Dieses Docker-Compose-Setup erfordert auch ein SQL-Initialisierungsskript. Erstellen Sie
ein Verzeichnis namens sql
und darin eine Datei namens init.sql
mit dem folgenden
Inhalt, um die notwendigen Tabellen sowohl für den Verifier als auch für den Issuer
einzurichten:
-- 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) );
Sobald beide Dateien vorhanden sind, öffnen Sie Ihr Terminal im Projektstammverzeichnis und führen Sie aus:
docker-compose up -d
Dieser Befehl startet einen MySQL-Container im Hintergrund, der für unsere Anwendung bereit ist.
Bevor wir die API-Endpunkte erstellen, lassen Sie uns die Shared Libraries erstellen, die die Kern-Geschäftslogik behandeln. Dieser Ansatz hält unsere API-Routen sauber und auf die Verarbeitung von HTTP-Anfragen konzentriert, während die komplexe Arbeit an diese Module delegiert wird.
src/lib/database.ts
)#Diese Datei ist die alleinige Quelle der Wahrheit für alle Datenbankinteraktionen. Sie
verwendet die mysql2
-Bibliothek, um sich mit unserem MySQL-Container zu verbinden, und
stellt eine Reihe von exportierten Funktionen zum Erstellen, Lesen und Aktualisieren von
Datensätzen in unseren Tabellen bereit. Diese Abstraktionsschicht macht unseren Code
modularer und einfacher zu warten.
Erstellen Sie die Datei src/lib/database.ts
mit dem folgenden Inhalt:
// src/lib/database.ts import mysql from "mysql2/promise"; // Konfiguration der Datenbankverbindung 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)-Funktionen für jede Tabelle // ... (z. B. createChallenge, getChallenge, createAuthorizationCode, etc.)
Hinweis: Aus Gründen der Kürze wurde die vollständige Liste der DAO-Funktionen weggelassen. Den vollständigen Code finden Sie im Projekt-Repository. Diese Datei enthält Funktionen zur Verwaltung von Challenges, Verifizierungssitzungen, Autorisierungscodes, Ausstellungssitzungen und Issuer-Schlüsseln.
src/lib/crypto.ts
)#Diese Datei behandelt alle sicherheitskritischen kryptografischen Operationen. Sie
verwendet die jose
-Bibliothek, um Schlüsselpaare zu generieren und JSON Web Tokens
(JWTs) zu signieren.
Schlüsselgenerierung Die Funktion generateIssuerKeyPair
erstellt ein neues
Elliptic-Curve-Schlüsselpaar, das zum Signieren von Nachweisen verwendet wird. Der
öffentliche Schlüssel wird im JSON Web Key (JWK)-Format exportiert, sodass er in unserem
did.json
-Dokument veröffentlicht werden kann.
// 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; // Eindeutige Schlüssel-ID zuweisen // ... (Export des privaten Schlüssels und weitere Einrichtung) return { publicKey, privateKey, publicKeyJWK /* ... */ }; }
Erstellung von JWT-Credentials Die Funktion createJWTVerifiableCredential
ist der
Kern des Ausstellungsprozesses. Sie nimmt die Claims des Nutzers, das Schlüsselpaar des
Issuers und andere Metadaten und erstellt daraus ein signiertes JWT-VC.
// 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 = { // Die DID des Issuers iss: issuerKeyPair.issuerDid, // Die DID des Subjekts (Holders) sub: subjectId, // Zeitpunkt der Ausstellung (iat) und des Ablaufs (exp) des Nachweises iat: now, exp: now + oneYear, // Das Datenmodell für Verifiable Credentials 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, }, }, }; // Signieren des Payloads mit dem privaten Schlüssel des Issuers return await new SignJWT(vcPayload) .setProtectedHeader({ alg: issuerKeyPair.algorithm, kid: issuerKeyPair.keyId, typ: "JWT", }) .sign(issuerKeyPair.privateKey); }
Diese Funktion erstellt den JWT-Payload gemäß dem W3C Verifiable Credentials Data Model und signiert ihn mit dem privaten Schlüssel des Issuers, wodurch ein sicherer und verifizierbarer Nachweis entsteht.
Unsere Next.js-Anwendung ist so strukturiert, dass die Belange zwischen Frontend und Backend getrennt sind, obwohl sie Teil desselben Projekts sind. Dies wird erreicht, indem der App Router sowohl für UI-Seiten als auch für API-Endpunkte genutzt wird.
Frontend (src/app/issue/page.tsx
): Eine einzelne
React-Seitenkomponente, die die Benutzeroberfläche für die
/issue
-Route definiert. Sie verarbeitet Benutzereingaben und kommuniziert mit unserer
Backend-API.
Backend API Routes (src/app/api/...
):
.well-known/.../route.ts
): Diese Routen stellen öffentliche
Metadaten-Endpunkte bereit, die es Wallets und anderen Clients ermöglichen, die
Fähigkeiten und öffentlichen Schlüssel des Issuers zu entdecken.issue/.../route.ts
): Diese Endpunkte implementieren die Kernlogik
von OpenID4VCI, einschließlich der Erstellung von Credential-Angeboten, der
Ausstellung von Tokens und der Signierung des endgültigen Nachweises.schemas/pid/route.ts
): Diese Route liefert das JSON-Schema für den
Nachweis und definiert dessen Struktur.Library (src/lib/
): Dieses Verzeichnis enthält wiederverwendbare Logik, die im
gesamten Backend geteilt wird.
database.ts
: Verwaltet alle Datenbankinteraktionen und abstrahiert SQL-Abfragen.crypto.ts
: Handhabt alle kryptografischen Operationen, wie z. B. die
Schlüsselgenerierung und JWT-Signierung.Diese klare Trennung macht die Anwendung modular und einfacher zu warten.
Hinweis: Die Funktion generateIssuerDid()
muss eine gültige did:web
zurückgeben,
die Ihrer Issuer-Domäne entspricht. Bei der Bereitstellung muss die Datei
.well-known/did.json
über HTTPS auf dieser Domäne bereitgestellt werden, damit Verifier
die Nachweise validieren können.
Unser Frontend ist eine einzelne React-Seite, die ein einfaches Formular für Benutzer bereitstellt, um einen neuen digitalen Nachweis anzufordern. Seine Aufgaben sind:
Die Kernlogik wird in der handleSubmit
-Funktion behandelt, die ausgelöst wird, wenn der
Benutzer das Formular abschickt.
// src/app/issue/page.tsx const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(null); setCredentialOffer(null); try { // 1. Erforderliche Felder validieren if (!userData.given_name || !userData.family_name || !userData.birth_date) { throw new Error("Bitte füllen Sie alle erforderlichen Felder aus"); } // 2. Ein Credential-Angebot vom Backend anfordern 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 || "Fehler beim Erstellen des Credential-Angebots", ); } // 3. Das Credential-Angebot im State setzen, um den QR-Code anzuzeigen const result = await response.json(); setCredentialOffer(result); } catch (err) { const errorMessage = (err as Error).message || "Unbekannter Fehler aufgetreten"; setError(errorMessage); } finally { setLoading(false); } };
Diese Funktion führt drei wesentliche Aktionen aus:
POST
-Anfrage an unseren /api/issue/authorize
-Endpunkt mit den Daten
des Benutzers.Der Rest der Datei enthält Standard-React-Code zum Rendern des Formulars und der QR-Code-Anzeige. Sie können die vollständige Datei im Projekt-Repository einsehen.
Bevor wir die Backend-API erstellen, müssen wir unsere Umgebung konfigurieren und die
Discovery-Endpunkte einrichten. Diese .well-known
-Dateien sind entscheidend, damit
Wallets unseren Issuer finden und verstehen können, wie sie mit ihm interagieren können.
Erstellen Sie eine Datei namens .env.local
im Stammverzeichnis Ihres Projekts und fügen
Sie die folgende Zeile hinzu. Diese URL muss öffentlich zugänglich sein, damit eine mobile
Wallet sie erreichen kann. Für die lokale Entwicklung können Sie einen Tunneling-Dienst
wie ngrok verwenden, um Ihren
localhost
freizugeben.
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Wallets entdecken die Fähigkeiten eines Issuers, indem sie Standard-.well-known
-URLs
abfragen. Wir müssen drei dieser Endpunkte erstellen.
1. Issuer-Metadaten (/.well-known/openid-credential-issuer
)
Dies ist die primäre Discovery-Datei für OpenID4VCI. Sie teilt der Wallet alles mit, was sie über den Issuer wissen muss, einschließlich seiner Endpunkte, der Arten von Nachweisen, die er anbietet, und der unterstützten kryptografischen Algorithmen.
Erstellen Sie die Datei src/app/.well-known/openid-credential-issuer/route.ts
:
// src/app/.well-known/openid-credential-issuer/route.ts import { NextResponse } from "next/server"; export async function GET() { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const issuerMetadata = { // Der eindeutige Bezeichner des Issuers. issuer: baseUrl, // Die URL des Autorisierungsservers. Der Einfachheit halber ist unser Issuer sein eigener Autorisierungsserver. authorization_servers: [baseUrl], // Die URL des Credential-Issuers. credential_issuer: baseUrl, // Der Endpunkt, an den die Wallet POST-Anfragen sendet, um den eigentlichen Nachweis zu erhalten. credential_endpoint: `${baseUrl}/api/issue/credential`, // Der Endpunkt, an dem die Wallet einen Autorisierungscode gegen einen Access Token austauscht. token_endpoint: `${baseUrl}/api/issue/token`, // Der Endpunkt für den Autorisierungs-Flow (wird in unserem Pre-Authorized-Flow nicht verwendet, aber es ist gute Praxis, ihn anzugeben). authorization_endpoint: `${baseUrl}/api/issue/authorize`, // Zeigt die Unterstützung für den Pre-Authorized-Code-Flow ohne Client-Authentifizierung an. pre_authorized_grant_anonymous_access_supported: true, // Lesbare Informationen über den Issuer. display: [ { name: "Corbado Credentials Issuer", locale: "en-US", }, ], // Eine Liste der Nachweistypen, die dieser Issuer ausstellen kann. credential_configurations_supported: { "eu.europa.ec.eudi.pid.1": { // Das Format des Nachweises (z. B. jwt_vc, mso_mdoc). format: "jwt_vc", // Der spezifische Dokumententyp, der den ISO-mDoc-Standards entspricht. doctype: "eu.europa.ec.eudi.pid.1", // Der mit diesem Nachweistyp verbundene OAuth 2.0-Scope. scope: "eu.europa.ec.eudi.pid.1", // Methoden, die die Wallet verwenden kann, um den Besitz ihres Schlüssels nachzuweisen. cryptographic_binding_methods_supported: ["jwk"], // Signaturalgorithmen, die der Issuer für diesen Nachweis unterstützt. credential_signing_alg_values_supported: ["ES256"], // Proof-of-Possession-Typen, die die Wallet verwenden kann. proof_types_supported: { jwt: { proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"], }, }, // Anzeige-Eigenschaften für den Nachweis. display: [ { name: "Corbado Credential Issuer", locale: "en-US", logo: { uri: `${baseUrl}/logo.png`, alt_text: "EU Digital Identity", }, background_color: "#003399", text_color: "#FFFFFF", }, ], // Eine Liste der Claims (Attribute) im Nachweis. 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" }], }, }, }, }, }, // Authentifizierungsmethoden, die vom Token-Endpunkt unterstützt werden. 'none' bedeutet öffentlicher Client. token_endpoint_auth_methods_supported: ["none"], // Unterstützte PKCE-Code-Challenge-Methoden. code_challenge_methods_supported: ["S256"], // OAuth 2.0 Grant-Typen, die der Issuer unterstützt. 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. OpenID-Konfiguration (/.well-known/openid-configuration
)
Dies ist ein Standard-OIDC-Discovery-Dokument, das einen breiteren Satz von Konfigurationsdetails bereitstellt.
Erstellen Sie die Datei src/app/.well-known/openid-configuration/route.ts
:
// src/app/.well-known/openid-configuration/route.ts import { NextResponse } from "next/server"; export async function GET() { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const openidConfiguration = { // Der eindeutige Bezeichner des Issuers. credential_issuer: baseUrl, // Der Endpunkt, an den die Wallet POST-Anfragen sendet, um den eigentlichen Nachweis zu erhalten. credential_endpoint: `${baseUrl}/api/issue/credential`, // Der Endpunkt für den Autorisierungs-Flow. authorization_endpoint: `${baseUrl}/api/issue/authorize`, // Der Endpunkt, an dem die Wallet einen Autorisierungscode gegen einen Access Token austauscht. token_endpoint: `${baseUrl}/api/issue/token`, // Eine Liste der Nachweistypen, die dieser Issuer ausstellen kann. 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-Typen, die der Issuer unterstützt. grant_types_supported: [ "authorization_code", "urn:ietf:params:oauth:grant-type:pre-authorized_code", ], // Zeigt die Unterstützung für den Pre-Authorized-Code-Flow an. pre_authorized_grant_anonymous_access_supported: true, // Unterstützte PKCE-Code-Challenge-Methoden. code_challenge_methods_supported: ["S256"], // Authentifizierungsmethoden, die vom Token-Endpunkt unterstützt werden. token_endpoint_auth_methods_supported: ["none"], // OAuth 2.0-Scopes, die der Issuer unterstützt. 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. DID-Dokument (/.well-known/did.json
)
Diese Datei veröffentlicht den öffentlichen Schlüssel des Issuers unter Verwendung der
did:web
-Methode, sodass jeder die Signatur der von ihm ausgestellten Nachweise
überprüfen kann.
Erstellen Sie die Datei src/app/.well-known/did.json/route.ts
:
// src/app/.well-known/did.json/route.ts import { NextResponse } from "next/server"; import { getActiveIssuerKey } from "../../../lib/database"; import { generateIssuerDid } from "../../../lib/crypto"; export async function GET() { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const issuerKey = await getActiveIssuerKey(); if (!issuerKey) { return NextResponse.json( { error: "No active issuer key found" }, { status: 404 }, ); } const publicKeyJWK = JSON.parse(issuerKey.public_key); const didId = generateIssuerDid(); const didDocument = { // Der Kontext definiert das im Dokument verwendete Vokabular. "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1", ], // Der DID-URI, der der eindeutige Bezeichner für den Issuer ist. id: didId, // Der DID-Controller, also die Entität, die die DID kontrolliert. Hier ist es der Issuer selbst. controller: didId, // Eine Liste von öffentlichen Schlüsseln, die zur Überprüfung von Signaturen des Issuers verwendet werden können. verificationMethod: [ { // Ein eindeutiger Bezeichner für den Schlüssel, bezogen auf die DID. id: `${didId}#${issuerKey.key_id}`, // Der Typ des Schlüssels. type: "JsonWebKey2020", // Die DID des Schlüssel-Controllers. controller: didId, // Der öffentliche Schlüssel im JWK-Format. publicKeyJwk: publicKeyJWK, }, ], // Gibt an, welche Schlüssel zur Authentifizierung (Nachweis der Kontrolle über die DID) verwendet werden können. authentication: [`${didId}#${issuerKey.key_id}`], // Gibt an, welche Schlüssel zur Erstellung von Verifiable Credentials verwendet werden können. assertionMethod: [`${didId}#${issuerKey.key_id}`], // Eine Liste von Diensten, die vom DID-Subjekt bereitgestellt werden, wie z. B. der Issuer-Endpunkt. 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", }, }); }
Warum kein Caching? Sie werden feststellen, dass alle drei dieser Endpunkte Header
zurückgeben, die das Caching aggressiv verhindern (Cache-Control: no-cache
,
Pragma: no-cache
, Expires: 0
). Dies ist eine wichtige Sicherheitspraxis für
Discovery-Dokumente. Issuer-Konfigurationen können sich ändern – zum Beispiel könnte ein
kryptografischer Schlüssel rotiert werden. Wenn eine Wallet oder ein Client eine alte
Version der did.json
- oder openid-credential-issuer
-Datei zwischenspeichern würde,
könnte sie neue Nachweise nicht validieren oder mit aktualisierten Endpunkten
interagieren. Indem wir Clients zwingen, bei jeder Anfrage eine frische Kopie abzurufen,
stellen wir sicher, dass sie immer die aktuellsten Informationen haben.
Das letzte Stück unserer öffentlich zugänglichen Infrastruktur ist der Credential-Schema-Endpunkt. Diese Route liefert ein JSON-Schema, das die Struktur, Datentypen und Einschränkungen des von uns ausgestellten PID-Nachweises formal definiert. Wallets und Verifier können dieses Schema verwenden, um den Inhalt des Nachweises zu validieren.
Erstellen Sie die Datei src/app/api/schemas/pid/route.ts
mit dem folgenden Inhalt:
// 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", // Ersetzen Sie dies durch Ihre tatsächliche 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" }, // ... andere Eigenschaften des Credential-Subjekts }, required: ["given_name", "family_name", "birth_date"], }, // ... andere Top-Level-Eigenschaften eines Verifiable Credentials }, }; return NextResponse.json(schema, { headers: { "Content-Type": "application/schema+json", "Access-Control-Allow-Origin": "*", // Cross-Origin-Anfragen erlauben }, }); }
Hinweis: Das JSON-Schema für einen PID-Nachweis kann ziemlich groß und detailliert sein. Aus Gründen der Kürze wurde das vollständige Schema gekürzt. Sie finden die vollständige Datei im Projekt-Repository.
Mit dem Frontend an Ort und Stelle benötigen wir nun die serverseitige Logik, um den
OpenID4VCI-Flow zu handhaben. Wir beginnen mit dem ersten Endpunkt, den das Frontend
aufruft: /api/issue/authorize
.
/api/issue/authorize
: Erstellen des Credential-Angebots#Dieser Endpunkt ist dafür verantwortlich, die Daten des Benutzers entgegenzunehmen, einen
sicheren Einmal-Code zu generieren und ein credential_offer
zu erstellen, das die Wallet
des Benutzers verstehen kann.
Hier ist die Kernlogik:
// 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. Benutzerdaten validieren 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. Einen Pre-Authorized-Code und eine PIN generieren const code = uuidv4(); const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 Minuten const txCode = Math.floor(1000 + Math.random() * 9000).toString(); // 4-stellige PIN // 3. Den Code und die Benutzerdaten speichern await createAuthorizationCode(uuidv4(), code, expiresAt); // Hinweis: Dies verwendet nur zu Demozwecken einen In-Memory-Speicher. // In der Produktion sollten Daten sicher in einer Datenbank mit korrektem Ablaufdatum gespeichert werden. 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. Das Credential-Angebot-Objekt erstellen const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const credentialOffer = { // Der Bezeichner des Issuers, also seine Basis-URL. credential_issuer: baseUrl, // Ein Array von Nachweistypen, die der Issuer anbietet. credential_configuration_ids: ["eu.europa.ec.eudi.pid.1"], // Gibt die Grant-Typen an, die die Wallet verwenden kann. grants: { // Wir verwenden den Pre-Authorized-Code-Flow. "urn:ietf:params:oauth:grant-type:pre-authorized_code": { // Der Einmal-Code, den die Wallet gegen einen Token eintauschen wird. "pre-authorized_code": code, // Zeigt an, dass der Benutzer eine PIN (tx_code) eingeben muss, um den Code einzulösen. user_pin_required: true, }, }, }; // 5. Die vollständige Credential-Offer-URI erstellen (ein Deep-Link für Wallets) const credentialOfferUri = `openid-credential-offer://?credential_offer=${encodeURIComponent( JSON.stringify(credentialOffer), )}`; // Die endgültige Antwort an das Frontend. return NextResponse.json({ // Der Deep-Link für den QR-Code. credential_offer_uri: credentialOfferUri, // Der rohe Pre-Authorized-Code zur Anzeige oder manuellen Eingabe. pre_authorized_code: code, // Die 4-stellige PIN, die der Benutzer in seiner Wallet eingeben muss. tx_code: txCode, }); } catch (error) { console.error("Authorization error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }
Wichtige Schritte in diesem Endpunkt:
pre-authorized_code
(eine UUID) und ein
4-stelliger tx_code
(PIN) für eine zusätzliche Sicherheitsebene erstellt.pre-authorized_code
wird mit einer kurzen Ablaufzeit in der
Datenbank gespeichert. Die Daten des Benutzers und die PIN werden im Speicher abgelegt
und mit dem Code verknüpft.credential_offer
-Objekt gemäß der
OpenID4VCI-Spezifikation erstellt. Dieses Objekt teilt der Wallet mit, wo sich der
Issuer befindet, welche Nachweise er anbietet und welcher Code benötigt wird, um sie zu
erhalten.openid-credential-offer://...
) erstellt und zusammen mit dem tx_code
an das
Frontend zurückgegeben, damit der Benutzer ihn sehen kann./api/issue/token
: Den Code gegen einen Token austauschen#Sobald der Benutzer den QR-Code scannt und seine PIN eingibt, sendet die Wallet eine
POST
-Anfrage an diesen Endpunkt. Seine Aufgabe ist es, den pre-authorized_code
und den
user_pin
(PIN) zu validieren und, falls sie gültig sind, einen kurzlebigen
Access Token auszustellen.
Erstellen Sie die Datei src/app/api/issue/token/route.ts
mit dem folgenden Inhalt:
// 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. Den Grant-Typ validieren if (grant_type !== "urn:ietf:params:oauth:grant-type:pre-authorized_code") { return NextResponse.json( { error: "unsupported_grant_type" }, { status: 400 }, ); } // 2. Den Pre-Authorized-Code validieren const authCode = await getAuthorizationCode(code); if (!authCode) { return NextResponse.json( { error: "invalid_grant", error_description: "Invalid or expired code", }, { status: 400 }, ); } // 3. Die PIN (tx_code) validieren 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. Access Token und c_nonce generieren const accessToken = uuidv4(); const cNonce = uuidv4(); const cNonceExpiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 Minuten // 5. Eine neue Ausstellungssitzung erstellen const userData = (global as any).userDataStore?.get(code); await createIssuanceSession( uuidv4(), authCode.id, accessToken, cNonce, cNonceExpiresAt, userData, ); // 6. Den Code als verwendet markieren und temporäre Daten bereinigen await markAuthorizationCodeAsUsed(code); (global as any).txCodeStore?.delete(code); (global as any).userDataStore?.delete(code); // 7. Die Access-Token-Antwort zurückgeben return NextResponse.json({ access_token: accessToken, token_type: "Bearer", expires_in: 3600, // 1 Stunde c_nonce: cNonce, c_nonce_expires_in: 300, // 5 Minuten }); } catch (error) { console.error("Token endpoint error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }
Wichtige Schritte in diesem Endpunkt:
pre-authorized_code
-Grant-Typ verwendet.pre-authorized_code
in der Datenbank
existiert, nicht abgelaufen ist und noch nicht verwendet wurde.user_pin
aus der Wallet mit dem zuvor gespeicherten
tx_code
verglichen, um sicherzustellen, dass der Benutzer die Transaktion autorisiert
hat.access_token
und eine c_nonce
(Credential Nonce) erstellt, die ein einmaliger Wert ist, um Replay-Angriffe auf den
Credential-Endpunkt zu verhindern.issuance_sessions
-Eintrag in der Datenbank
erstellt, der den Access Token mit den Daten des Benutzers
verknüpft.pre-authorized_code
als verwendet markiert.access_token
und die c_nonce
an die Wallet
zurückgegeben./api/issue/credential
: Den signierten Nachweis ausstellen#Dies ist der letzte und wichtigste Endpunkt. Die Wallet verwendet den Access Token, den
sie vom /token
-Endpunkt erhalten hat, um eine authentifizierte POST
-Anfrage an diese
Route zu senden. Die Aufgabe dieses Endpunkts ist es, die endgültige Validierung
durchzuführen, den kryptografisch signierten Nachweis zu erstellen und ihn an die Wallet
zurückzugeben.
Erstellen Sie die Datei src/app/api/issue/credential/route.ts
mit dem folgenden Inhalt:
// 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. Den Bearer-Token validieren 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. Die Benutzerdaten aus der Sitzung abrufen const userData = session.user_data; if (!userData) { return NextResponse.json({ error: "missing_user_data" }, { status: 400 }); } // 3. Den aktiven Issuer-Schlüssel abrufen const issuerKey = await getActiveIssuerKey(); if (!issuerKey) { // In einer echten Anwendung hätten Sie ein robusteres Schlüsselverwaltungssystem. // Für diese Demo können wir einen Schlüssel on-the-fly generieren, wenn keiner existiert. // Dieser Teil wurde aus Gründen der Kürze weggelassen, befindet sich aber im Repository. return NextResponse.json( { error: "server_error", error_description: "Failed to get issuer key", }, { status: 500 }, ); } // 4. Den JWT-VC erstellen 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. Den ausgestellten Nachweis in der Datenbank speichern await createIssuedCredential(/* ... Nachweisdetails ... */); await updateIssuanceSession(session.id, "credential_issued"); // 6. Den signierten Nachweis zurückgeben return NextResponse.json({ format: "jwt_vc", credential: credentialData, c_nonce: uuidv4(), // Eine neue Nonce für nachfolgende Anfragen c_nonce_expires_in: 300, }); } catch (error) { console.error("Credential endpoint error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }
Wichtige Schritte in diesem Endpunkt:
Bearer
-Token im
Authorization
-Header gesucht und damit die aktive Ausstellungssitzung nachgeschlagen.createJWTVerifiableCredential
aus
src/lib/crypto.ts
aufgerufen, um den JWT-VC zu erstellen und zu signieren.Sie haben jetzt eine vollständige End-to-End-Implementierung eines Issuers für digitale Nachweise. Hier erfahren Sie, wie Sie ihn lokal ausführen und was Sie beachten müssen, um ihn von einem Proof-of-Concept zu einer produktionsreifen Anwendung zu machen.
Das Repository klonen:
git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
Abhängigkeiten installieren:
npm install
Die Datenbank starten: Stellen Sie sicher, dass Docker läuft, und starten Sie dann den MySQL-Container:
docker-compose up -d
Umgebung konfigurieren & Tunnel starten: Dies ist der kritischste Schritt für
lokale Tests. Da Ihre mobile Wallet sich über das Internet mit Ihrer
Entwicklungsmaschine verbinden muss, müssen Sie Ihren lokalen Server mit einer
öffentlichen HTTPS-URL freigeben. Wir verwenden dafür ngrok
.
a. ngrok starten:
ngrok http 3000
b. Kopieren Sie die HTTPS-URL aus der
ngrok-Ausgabe (z. B.
https://random-string.ngrok.io
). c. Erstellen Sie eine .env.local
-Datei und
setzen Sie die URL:
NEXT_PUBLIC_BASE_URL=https://<your-ngrok-url>
Die Anwendung ausführen:
npm run dev
Öffnen Sie Ihren Browser unter http://localhost:3000/issue
. Sie können nun das
Formular ausfüllen, und der generierte QR-Code wird korrekt auf Ihre öffentliche
ngrok-URL verweisen, sodass Ihre mobile Wallet sich verbinden und den Nachweis
empfangen kann.
ngrok
#Protokolle für digitale Nachweise werden mit
Sicherheit als oberster Priorität entwickelt. Aus
diesem Grund werden Wallets fast immer die Verbindung zu einem Issuer über eine unsichere
(http://
) Verbindung ablehnen. Der gesamte Prozess basiert auf einer sicheren
HTTPS-Verbindung, die durch ein SSL-Zertifikat ermöglicht wird.
Ein Tunneldienst wie ngrok
löst beide Probleme, indem er eine sichere, öffentlich
zugängliche HTTPS-URL (mit einem gültigen SSL-Zertifikat) erstellt, die den gesamten
Datenverkehr an Ihren lokalen Entwicklungsserver weiterleitet. Wallets erfordern HTTPS und
werden sich weigern, sich mit unsicheren (http://
) Endpunkten zu verbinden. Dies ist ein
unverzichtbares Werkzeug zum Testen von Webdiensten, die mit mobilen Geräten oder externen
Webhooks interagieren müssen.
Dieses Beispiel konzentriert sich bewusst auf den Kern des Ausstellungsprozesses, um es leicht verständlich zu machen. Die folgenden Themen werden als außerhalb des Rahmens betrachtet:
revoked
-Flag für die
zukünftige Verwendung enthält, wird hier keine Widerrufslogik bereitgestellt.pre-authorized_code
-Flow konzentriert. Eine vollständige Implementierung des
authorization_code
-Flows würde einen Zustimmungsbildschirm für den Benutzer und eine
komplexere OAuth 2.0-Logik erfordern.Das war's! Mit ein paar Seiten Code haben wir nun einen vollständigen End-to-End-Issuer für digitale Nachweise, der:
pre-authorized_code
-Flow implementiert.Obwohl dieser Leitfaden eine solide Grundlage bietet, würde ein produktionsreifer Issuer zusätzliche Funktionen wie robustes Schlüsselmanagement, persistenten Speicher anstelle von In-Memory-Speichern, Widerruf von Nachweisen und eine umfassende Sicherheitshärtung erfordern. Die Kompatibilität mit Wallets variiert ebenfalls; die Sphereon Wallet wird zum Testen empfohlen, aber andere Wallets unterstützen den hier implementierten Pre-Authorized-Flow möglicherweise nicht. Die Kernbausteine und der Interaktionsfluss würden jedoch gleich bleiben. Indem Sie diesen Mustern folgen, können Sie einen sicheren und interoperablen Issuer für jede Art von digitalem Nachweis erstellen.
Hier sind einige der wichtigsten Ressourcen, Spezifikationen und Tools, die in diesem Tutorial verwendet oder referenziert wurden:
Projekt-Repository:
Wichtige Spezifikationen:
did:web
-Methode: Die DID-Methode,
die für den öffentlichen Schlüssel unseres Issuers verwendet wird.Tools:
Bibliotheken:
Related Articles
Table of Contents