Apprenez à construire un émetteur de Verifiable Credentials W3C en utilisant le protocole OpenID4VCI. Ce guide étape par étape vous montre comment créer une application Next.js qui émet des identifiants signés cryptographiquement, compatibles avec les wal
Amine
Created: August 20, 2025
Updated: August 21, 2025
See the original blog version in English here.
Les identifiants numériques sont un moyen puissant de prouver une identité et des attestations de manière sécurisée et respectueuse de la vie privée. Mais comment les utilisateurs obtiennent-ils ces identifiants ? C'est là que le rôle de l'Issuer (émetteur) devient crucial. Un Issuer est une entité de confiance, comme une agence gouvernementale, une université ou une banque, chargée de créer et de distribuer des identifiants signés numériquement aux utilisateurs.
Ce guide propose un tutoriel complet, étape par étape, pour construire un Issuer d'identifiants numériques. Nous nous concentrerons sur le protocole OpenID for Verifiable Credential Issuance (OpenID4VCI), une norme moderne qui définit comment les utilisateurs peuvent obtenir des identifiants auprès d'un Issuer et les stocker en toute sécurité dans leurs wallets numériques.
Le résultat final sera une application Next.js fonctionnelle capable de :
Recent Articles
📝
Comment construire un vérificateur d'identifiants numériques (Guide du développeur)
📝
Comment créer un émetteur d'identifiants numériques (Guide du développeur)
📖
Clé résidente WebAuthn : les informations d'identification détectables en tant que passkeys
🔑
Accès par badge physique et Passkeys : Guide technique
🔑
MFA obligatoire et transition vers les Passkeys : les bonnes pratiques
Avant de continuer, il est important de clarifier la distinction entre deux concepts liés mais différents :
Identifiants numériques (terme général) : Il s'agit d'une vaste catégorie qui englobe toute forme numérique d'identifiants, de certificats ou d'attestations. Cela peut inclure de simples certificats numériques, des badges numériques de base, ou tout identifiant stocké électroniquement qui peut ou non avoir des fonctionnalités de sécurité cryptographique.
Verifiable Credentials (VC - Standard W3C) : Il s'agit d'un type spécifique d'identifiant numérique qui suit la norme W3C Verifiable Credentials Data Model. Les Verifiable Credentials sont des identifiants signés cryptographiquement, infalsifiables et respectueux de la vie privée, qui peuvent être vérifiés de manière indépendante. Ils incluent des exigences techniques spécifiques comme :
Dans ce guide, nous construisons spécifiquement un émetteur de Verifiable Credentials qui suit la norme W3C, et non un simple système d'identifiants numériques. Le protocole OpenID4VCI que nous utilisons est conçu spécifiquement pour l'émission de Verifiable Credentials, et le format JWT-VC que nous mettrons en œuvre est un format conforme au W3C pour les Verifiable Credentials.
La magie des identifiants numériques repose sur un modèle simple mais puissant de « triangle de confiance » impliquant trois acteurs clés :
Le flux d'émission est la première étape de cet écosystème. L'Issuer valide les informations de l'utilisateur et lui fournit un identifiant. Une fois que le Holder a cet identifiant dans son wallet, il peut le présenter à un Verifier pour prouver son identité ou ses attestations, complétant ainsi le triangle.
Voici un aperçu rapide de l'application finale en action :
Étape 1 : Saisie des données de l'utilisateur L'utilisateur remplit un formulaire avec ses informations personnelles pour demander un nouvel identifiant.
Étape 2 : Génération de l'offre d'identifiant L'application génère une offre d'identifiant sécurisée, affichée sous forme de QR code et d'un code pré-autorisé.
Étape 3 : Interaction avec le wallet L'utilisateur scanne le QR code avec un wallet compatible (par exemple, Sphereon Wallet) et saisit un code PIN pour autoriser l'émission.
Étape 4 : Identifiant émis Le wallet reçoit et stocke le nouvel identifiant numérique émis, prêt pour une utilisation future.
Avant de plonger dans le code, passons en revue les connaissances fondamentales et les outils dont nous aurons besoin. Ce guide suppose une familiarité de base avec les concepts de développement web, mais les prérequis suivants sont essentiels pour construire un émetteur d'identifiants.
Notre Issuer est basé sur un ensemble de normes ouvertes qui garantissent l'interopérabilité entre les wallets et les services d'émission. Pour ce tutoriel, nous nous concentrerons sur les éléments suivants :
Norme / Protocole | Description |
---|---|
OpenID4VCI | OpenID for Verifiable Credential Issuance. C'est le protocole principal que nous utiliserons. Il définit un flux standard pour la manière dont un utilisateur (via son wallet) peut demander et recevoir un identifiant d'un Issuer. |
JWT-VC | Verifiable Credentials basés sur JWT. Le format de l'identifiant que nous émettrons. C'est une norme W3C qui encode les Verifiable Credentials en tant que JSON Web Tokens (JWT), les rendant compacts et adaptés au web. |
ISO mDoc | ISO/IEC 18013-5. La norme internationale pour les permis de conduire mobiles (mDL). Bien que nous émettions un JWT-VC, les claims qu'il contient sont structurés pour être compatibles avec le modèle de données mDoc (par exemple, eu.europa.ec.eudi.pid.1 ). |
OAuth 2.0 | Le framework d'autorisation sous-jacent utilisé par OpenID4VCI. Nous mettrons en œuvre un flux pre-authorized_code , qui est un type de subvention spécifique conçu pour une émission d'identifiants sécurisée et conviviale. |
OpenID4VCI prend en charge deux flux d'autorisation principaux pour l'émission d'identifiants :
Flux de code pré-autorisé : Dans ce flux, l'Issuer génère un code à usage unique et
de courte durée (pre-authorized_code
) qui est immédiatement disponible pour
l'utilisateur. Le wallet de l'utilisateur peut ensuite échanger ce code directement
contre un identifiant. Ce flux est idéal pour les scénarios où l'utilisateur est déjà
authentifié et présent sur le site web de l'Issuer, car il offre
une expérience d'émission instantanée et transparente, sans redirection.
Flux de code d'autorisation : Il s'agit du flux OAuth 2.0
standard, où l'utilisateur est redirigé vers un serveur d'autorisation pour donner son
consentement. Après approbation, le serveur renvoie un authorization_code
à un
redirect_uri
enregistré. Ce flux est plus adapté aux applications tierces qui lancent
le processus d'émission au nom de l'utilisateur.
Pour ce tutoriel, nous utiliserons le flux pre-authorized_code
. Nous avons choisi
cette approche car elle est plus simple et offre une expérience utilisateur plus directe
pour notre cas d'usage spécifique : un utilisateur demandant directement un identifiant
sur le site web de l'Issuer. Cela élimine le besoin de redirections
complexes et d'enregistrement de client, rendant la logique d'émission de base plus facile
à comprendre et à mettre en œuvre.
Cette combinaison de normes nous permet de construire un émetteur compatible avec un large éventail de wallets numériques et garantit un processus sécurisé et standardisé pour l'utilisateur.
Pour construire notre émetteur, nous utiliserons la même stack technique robuste et moderne que celle utilisée pour le vérificateur, garantissant une expérience de développement cohérente et de haute qualité.
Nous utiliserons TypeScript pour notre code frontend et backend. Son typage statique est inestimable dans une application critique pour la sécurité comme un émetteur, car il aide à prévenir les erreurs courantes et améliore la qualité et la maintenabilité globales du code.
Next.js est notre framework de prédilection car il offre une expérience intégrée et transparente pour la création d'applications full-stack.
Notre implémentation s'appuiera sur quelques bibliothèques clés pour gérer des tâches spécifiques :
pre-authorized_code
.Pour tester votre émetteur, vous aurez besoin d'un wallet mobile qui prend en charge le protocole OpenID4VCI. Pour ce tutoriel, nous recommandons le Sphereon Wallet, qui est disponible pour Android et iOS.
Comment installer Sphereon Wallet :
L'émission d'un identifiant est une opération critique pour la sécurité qui repose sur des concepts cryptographiques fondamentaux pour garantir la confiance et l'authenticité.
Au cœur, un Verifiable Credential est un ensemble d'attestations qui a été signé numériquement par l'Issuer. Cette signature offre deux garanties :
Les signatures numériques sont créées à l'aide de la cryptographie à clé publique/privée. Voici comment cela fonctionne :
Dans notre implémentation, nous générerons une paire de clés à courbe elliptique (EC) et
utiliserons l'algorithme ES256
pour signer le
JWT-VC. La clé publique est intégrée dans le
DID de l'Issuer (did:web
), permettant à n'importe quel Verifier de la découvrir et de
valider la signature de l'identifiant.
Remarque : Le claim aud
(audience) est intentionnellement omis dans nos
JWT, car l'identifiant est conçu pour être
polyvalent et non lié à un wallet spécifique.
Si vous souhaitez restreindre l'utilisation à une audience particulière, incluez un claim
aud
et définissez-le en conséquence.
Notre application Issuer est construite comme un projet Next.js
full-stack, avec une séparation claire
entre la logique frontend et backend. Cette architecture nous permet de créer une
expérience utilisateur transparente tout en gérant toutes les opérations critiques pour la
sécurité sur le serveur.
Important : Les tables verification_sessions
et verified_credentials
incluses dans
le SQL ne sont pas requises pour cet émetteur mais sont incluses pour être complet.
src/app/issue/page.tsx
) : Une seule page React
qui permet aux utilisateurs de saisir leurs données pour demander un identifiant. Elle
effectue des appels API vers notre backend pour lancer le processus d'émission.src/app/api/issue/...
) : Un ensemble de points de terminaison
côté serveur qui implémentent le protocole OpenID4VCI.
/.well-known/openid-credential-issuer
: Un point de terminaison de métadonnées
public. C'est la première URL qu'un wallet vérifiera pour découvrir les capacités de
l'émetteur, y compris son serveur d'autorisation, son point de terminaison de jeton,
son point de terminaison d'identifiant et les types d'identifiants qu'il propose./.well-known/openid-configuration
: Un point de terminaison de découverte OpenID
Connect standard. Bien que étroitement lié au précédent, ce point de terminaison
sert une configuration plus large liée à OIDC et est souvent requis pour
l'interopérabilité avec les clients OpenID standard./.well-known/did.json
: Le document DID pour notre émetteur. Lors de l'utilisation
de la méthode did:web
, ce fichier est utilisé pour publier les clés publiques de
l'émetteur, que les vérificateurs peuvent utiliser pour valider les signatures des
identifiants qu'il émet.authorize/route.ts
: Crée un pre-authorized_code
et une offre d'identifiant.token/route.ts
: Échange le pre-authorized_code
contre un
access token.credential/route.ts
: Émet le JWT-VC final, signé cryptographiquement.schemas/pid/route.ts
: Expose le schéma JSON pour l'identifiant PID. Cela permet à
tout consommateur de l'identifiant de comprendre sa structure et ses types de
données.src/lib/
) :
database.ts
: Gère toutes les interactions avec la base de données, comme le
stockage des codes d'autorisation et des clés de l'émetteur.crypto.ts
: Gère toutes les opérations cryptographiques, y compris la génération
de clés et la signature JWT.Voici un diagramme illustrant le flux d'émission :
Maintenant que nous avons une solide compréhension des normes, des protocoles et de l'architecture, nous pouvons commencer à construire notre émetteur.
Suivez pas à pas ou utilisez le code final
Nous allons maintenant passer en revue la configuration et l'implémentation du code étape par étape. Si vous préférez passer directement au produit fini, vous pouvez cloner le projet complet depuis notre dépôt GitHub et l'exécuter localement.
git clone https://github.com/corbado/digital-credentials-example.git
Tout d'abord, nous allons initialiser un nouveau projet Next.js, installer les dépendances nécessaires et démarrer notre base de données.
Ouvrez votre terminal, naviguez vers le répertoire où vous souhaitez créer votre projet, et exécutez la commande suivante. Nous utilisons l'App Router, TypeScript et Tailwind CSS pour ce projet.
npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm
Cette commande crée un nouveau squelette d'application Next.js dans votre répertoire actuel.
Ensuite, nous devons installer les bibliothèques qui géreront les JWT, les connexions à la base de données et la génération d'UUID.
npm install jose mysql2 uuid @types/uuid
Cette commande installe :
jose
: Pour signer et vérifier les JSON Web Tokens (JWT).mysql2
: Le client MySQL pour notre base de
données.uuid
: Pour générer des chaînes de défi uniques.@types/uuid
: Les types TypeScript pour la bibliothèque uuid
.Notre backend nécessite une base de données MySQL
pour stocker les codes d'autorisation, les sessions d'émission et les clés de l'émetteur.
Nous avons inclus un fichier docker-compose.yml
pour faciliter cela.
Si vous avez cloné le dépôt, vous pouvez simplement exécuter docker-compose up -d
. Si
vous partez de zéro, créez un fichier nommé docker-compose.yml
avec le contenu suivant :
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:
Cette configuration Docker Compose nécessite également un script d'initialisation SQL.
Créez un répertoire nommé sql
et à l'intérieur, un fichier nommé init.sql
avec le
contenu suivant pour configurer les tables nécessaires pour le vérificateur et l'émetteur
:
-- 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) );
Une fois les deux fichiers en place, ouvrez votre terminal à la racine du projet et exécutez :
docker-compose up -d
Cette commande démarrera un conteneur MySQL en arrière-plan, prêt à être utilisé par notre application.
Avant de construire les points de terminaison de l'API, créons les bibliothèques partagées qui géreront la logique métier de base. Cette approche maintient nos routes API propres et axées sur la gestion des requêtes HTTP, tandis que le travail complexe est délégué à ces modules.
src/lib/database.ts
)#Ce fichier est la seule source de vérité pour toutes les interactions avec la base de
données. Il utilise la bibliothèque mysql2
pour se connecter à notre conteneur MySQL et
fournit un ensemble de fonctions exportées pour créer, lire et mettre à jour des
enregistrements dans nos tables. Cette couche d'abstraction rend notre code plus modulaire
et plus facile à maintenir.
Créez le fichier src/lib/database.ts
avec le contenu suivant :
// src/lib/database.ts import mysql from "mysql2/promise"; // Configuration de la connexion à la base de données 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; } // Fonctions Data-Access-Object (DAO) pour chaque table // ... (par ex., createChallenge, getChallenge, createAuthorizationCode, etc.)
Remarque : Pour des raisons de brièveté, la liste complète des fonctions DAO a été omise. Vous pouvez trouver le code complet dans le dépôt du projet. Ce fichier inclut des fonctions pour gérer les défis, les sessions de vérification, les codes d'autorisation, les sessions d'émission et les clés de l'émetteur.
src/lib/crypto.ts
)#Ce fichier gère toutes les opérations cryptographiques critiques pour la sécurité. Il
utilise la bibliothèque jose
pour générer des paires de clés et signer des JSON Web
Tokens (JWT).
Génération de clés La fonction generateIssuerKeyPair
crée une nouvelle paire de clés
à courbe elliptique qui sera utilisée pour signer les identifiants. La clé publique est
exportée au format JSON Web Key (JWK) afin qu'elle puisse être publiée dans notre document
did.json
.
// 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; // Attribuer un ID de clé unique // ... (exportation de la clé privée et autre configuration) return { publicKey, privateKey, publicKeyJWK /* ... */ }; }
Création d'identifiants JWT La fonction createJWTVerifiableCredential
est au cœur du
processus d'émission. Elle prend les attestations de l'utilisateur, la paire de clés de
l'émetteur et d'autres métadonnées, et les utilise pour créer un JWT-VC signé.
// 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 = { // Le DID de l'émetteur iss: issuerKeyPair.issuerDid, // Le DID du sujet (détenteur) sub: subjectId, // L'heure à laquelle l'identifiant a été émis (iat) et quand il expire (exp) iat: now, exp: now + oneYear, // Le modèle de données du Verifiable Credential vc: { "@context": [ "https://www.w3.org/2018/credentials/v1", "https://europa.eu/eudi/pid/v1", ], type: ["VerifiableCredential", "eu.europa.ec.eudi.pid.1"], issuer: issuerKeyPair.issuerDid, issuanceDate: new Date(now * 1000).toISOString(), credentialSubject: { id: subjectId, ...claims, }, }, }; // Signer la charge utile avec la clé privée de l'émetteur return await new SignJWT(vcPayload) .setProtectedHeader({ alg: issuerKeyPair.algorithm, kid: issuerKeyPair.keyId, typ: "JWT", }) .sign(issuerKeyPair.privateKey); }
Cette fonction construit la charge utile du JWT conformément au modèle de données W3C Verifiable Credentials et la signe avec la clé privée de l'émetteur, produisant un Verifiable Credential sécurisé et vérifiable.
Notre application Next.js est structurée pour séparer les responsabilités entre le frontend et le backend, même s'ils font partie du même projet. Ceci est réalisé en tirant parti de l'App Router pour les pages d'interface utilisateur et les points de terminaison de l'API.
Frontend (src/app/issue/page.tsx
) : Un seul composant de page
React qui définit l'interface utilisateur pour la route
/issue
. Il gère la saisie de l'utilisateur et communique avec notre API backend.
Routes API Backend (src/app/api/...
) :
.well-known/.../route.ts
) : Ces routes exposent des points de
terminaison de métadonnées publics qui permettent aux wallets et autres clients de
découvrir les capacités de l'émetteur et ses clés publiques.issue/.../route.ts
) : Ces points de terminaison implémentent la
logique de base d'OpenID4VCI, y compris la création d'offres d'identifiants,
l'émission de jetons et la signature de l'identifiant final.schemas/pid/route.ts
) : Cette route sert le schéma JSON pour
l'identifiant, définissant sa structure.Bibliothèque (src/lib/
) : Ce répertoire contient une logique réutilisable partagée
à travers le backend.
database.ts
: Gère toutes les interactions avec la base de données, en abstrayant
les requêtes SQL.crypto.ts
: Gère toutes les opérations cryptographiques, telles que la génération
de clés et la signature JWT.Cette séparation claire rend l'application modulaire et plus facile à maintenir.
Remarque : La fonction generateIssuerDid()
doit retourner un did:web
valide
correspondant à votre domaine d'émetteur.
Lors du déploiement, le .well-known/did.json
doit être servi via HTTPS sur ce domaine
pour que les vérificateurs puissent valider les identifiants.
Notre frontend est une seule page React qui fournit un formulaire simple pour que les utilisateurs demandent un nouvel identifiant numérique. Ses responsabilités sont de :
La logique de base est gérée dans la fonction handleSubmit
, qui est déclenchée lorsque
l'utilisateur soumet le formulaire.
// src/app/issue/page.tsx const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(null); setCredentialOffer(null); try { // 1. Valider les champs requis if (!userData.given_name || !userData.family_name || !userData.birth_date) { throw new Error("Veuillez remplir tous les champs requis"); } // 2. Demander une offre d'identifiant au 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 || "Échec de la création de l'offre d'identifiant", ); } // 3. Définir l'offre d'identifiant dans l'état pour afficher le QR code const result = await response.json(); setCredentialOffer(result); } catch (err) { const errorMessage = (err as Error).message || "Une erreur inconnue s'est produite"; setError(errorMessage); } finally { setLoading(false); } };
Cette fonction effectue trois actions clés :
POST
à notre point de terminaison /api/issue/authorize
avec
les données de l'utilisateur.Le reste du fichier contient du code React standard pour le rendu du formulaire et l'affichage du QR code. Vous pouvez voir le fichier complet dans le dépôt du projet.
Avant de construire l'API backend, nous devons configurer notre environnement et mettre en
place les points de terminaison de découverte. Ces fichiers .well-known
sont cruciaux
pour que les wallets trouvent notre émetteur et comprennent comment interagir avec lui.
Créez un fichier nommé .env.local
à la racine de votre projet et ajoutez la ligne
suivante. Cette URL doit être publiquement accessible pour qu'un wallet mobile puisse
l'atteindre. Pour le développement local, vous pouvez utiliser un service de tunnel comme
ngrok pour exposer votre localhost
.
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Les wallets découvrent les capacités d'un émetteur en interrogeant des URL .well-known
standard. Nous devons créer trois de ces points de terminaison.
1. Métadonnées de l'émetteur (/.well-known/openid-credential-issuer
)
C'est le fichier de découverte principal pour OpenID4VCI. Il indique au wallet tout ce qu'il doit savoir sur l'émetteur, y compris ses points de terminaison, les types d'identifiants qu'il propose et les algorithmes cryptographiques pris en charge.
Créez le fichier 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 = { // L'identifiant unique de l'émetteur. issuer: baseUrl, // L'URL du serveur d'autorisation. Pour simplifier, notre émetteur est son propre serveur d'autorisation. authorization_servers: [baseUrl], // L'URL de l'émetteur d'identifiants. credential_issuer: baseUrl, // Le point de terminaison où le wallet enverra une requête POST pour recevoir l'identifiant réel. credential_endpoint: `${baseUrl}/api/issue/credential`, // Le point de terminaison où le wallet échange un code d'autorisation contre un jeton d'accès. token_endpoint: `${baseUrl}/api/issue/token`, // Le point de terminaison pour le flux d'autorisation (non utilisé dans notre flux pré-autorisé, mais bonne pratique de l'inclure). authorization_endpoint: `${baseUrl}/api/issue/authorize`, // Indique la prise en charge du flux de code pré-autorisé sans nécessiter d'authentification du client. pre_authorized_grant_anonymous_access_supported: true, // Informations lisibles par l'homme sur l'émetteur. display: [ { name: "Corbado Credentials Issuer", locale: "en-US", }, ], // Une liste des types d'identifiants que cet émetteur peut émettre. credential_configurations_supported: { "eu.europa.ec.eudi.pid.1": { // Le format de l'identifiant (par ex., jwt_vc, mso_mdoc). format: "jwt_vc", // Le type de document spécifique, conforme aux normes ISO mDoc. doctype: "eu.europa.ec.eudi.pid.1", // La portée OAuth 2.0 associée à ce type d'identifiant. scope: "eu.europa.ec.eudi.pid.1", // Méthodes que le wallet peut utiliser pour prouver la possession de sa clé. cryptographic_binding_methods_supported: ["jwk"], // Algorithmes de signature que l'émetteur prend en charge pour cet identifiant. credential_signing_alg_values_supported: ["ES256"], // Types de preuve de possession que le wallet peut utiliser. proof_types_supported: { jwt: { proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"], }, }, // Propriétés d'affichage pour l'identifiant. display: [ { name: "Corbado Credential Issuer", locale: "en-US", logo: { uri: `${baseUrl}/logo.png`, alt_text: "EU Digital Identity", }, background_color: "#003399", text_color: "#FFFFFF", }, ], // Une liste des attestations (attributs) dans l'identifiant. 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" }], }, }, }, }, }, // Méthodes d'authentification prises en charge par le point de terminaison de jeton. 'none' signifie client public. token_endpoint_auth_methods_supported: ["none"], // Méthodes de défi de code PKCE prises en charge. code_challenge_methods_supported: ["S256"], // Types de subvention OAuth 2.0 que l'émetteur prend en charge. 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. Configuration OpenID (/.well-known/openid-configuration
)
Il s'agit d'un document de découverte OIDC standard qui fournit un ensemble plus large de détails de configuration.
Créez le fichier 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 = { // L'identifiant unique de l'émetteur. credential_issuer: baseUrl, // Le point de terminaison où le wallet enverra une requête POST pour recevoir l'identifiant réel. credential_endpoint: `${baseUrl}/api/issue/credential`, // Le point de terminaison pour le flux d'autorisation. authorization_endpoint: `${baseUrl}/api/issue/authorize`, // Le point de terminaison où le wallet échange un code d'autorisation contre un jeton d'accès. token_endpoint: `${baseUrl}/api/issue/token`, // Une liste des types d'identifiants que cet émetteur peut émettre. 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"], }, }, }, }, // Types de subvention OAuth 2.0 que l'émetteur prend en charge. grant_types_supported: [ "authorization_code", "urn:ietf:params:oauth:grant-type:pre-authorized_code", ], // Indique la prise en charge du flux de code pré-autorisé. pre_authorized_grant_anonymous_access_supported: true, // Méthodes de défi de code PKCE prises en charge. code_challenge_methods_supported: ["S256"], // Méthodes d'authentification prises en charge par le point de terminaison de jeton. token_endpoint_auth_methods_supported: ["none"], // Portées OAuth 2.0 que l'émetteur prend en charge. 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. Document DID (/.well-known/did.json
)
Ce fichier publie la clé publique de l'émetteur en utilisant la méthode did:web
,
permettant à quiconque de vérifier la signature des identifiants qu'il a émis.
Créez le fichier 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 = { // Le contexte définit le vocabulaire utilisé dans le document. "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1", ], // L'URI du DID, qui est l'identifiant unique de l'émetteur. id: didId, // Le contrôleur du DID, qui est l'entité qui contrôle le DID. Ici, c'est l'émetteur lui-même. controller: didId, // Une liste de clés publiques qui peuvent être utilisées pour vérifier les signatures de l'émetteur. verificationMethod: [ { // Un identifiant unique pour la clé, dans le périmètre du DID. id: `${didId}#${issuerKey.key_id}`, // Le type de la clé. type: "JsonWebKey2020", // Le DID du contrôleur de la clé. controller: didId, // La clé publique au format JWK. publicKeyJwk: publicKeyJWK, }, ], // Spécifie quelles clés peuvent être utilisées pour l'authentification (prouver le contrôle du DID). authentication: [`${didId}#${issuerKey.key_id}`], // Spécifie quelles clés peuvent être utilisées pour créer des Verifiable Credentials. assertionMethod: [`${didId}#${issuerKey.key_id}`], // Une liste de services fournis par le sujet du DID, comme le point de terminaison de l'émetteur. 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", }, }); }
Pourquoi ne pas utiliser de cache ? Vous remarquerez que ces trois points de
terminaison retournent des en-têtes qui empêchent agressivement la mise en cache
(Cache-Control: no-cache
, Pragma: no-cache
, Expires: 0
). C'est une pratique de
sécurité essentielle pour les documents de découverte. Les configurations de l'émetteur
peuvent changer, par exemple, une clé cryptographique peut être renouvelée. Si un wallet
ou un client mettait en cache une ancienne version du fichier did.json
ou
openid-credential-issuer
, il ne parviendrait pas à valider les nouveaux identifiants
ou à interagir avec les points de terminaison mis à jour. En forçant les clients à
récupérer une nouvelle copie à chaque requête, nous nous assurons qu'ils disposent
toujours des informations les plus récentes.
La dernière pièce de notre infrastructure publique est le point de terminaison du schéma de l'identifiant. Cette route sert un schéma JSON qui définit formellement la structure, les types de données et les contraintes de l'identifiant PID que nous émettons. Les wallets et les vérificateurs peuvent utiliser ce schéma pour valider le contenu de l'identifiant.
Créez le fichier src/app/api/schemas/pid/route.ts
avec le contenu suivant :
// 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", // Remplacez par votre domaine réel 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" }, // ... autres propriétés du sujet de l'identifiant }, required: ["given_name", "family_name", "birth_date"], }, // ... autres propriétés de haut niveau d'un Verifiable Credential }, }; return NextResponse.json(schema, { headers: { "Content-Type": "application/schema+json", "Access-Control-Allow-Origin": "*", // Autoriser les requêtes cross-origin }, }); }
Remarque : Le schéma JSON pour un identifiant PID peut être assez volumineux et détaillé. Pour des raisons de brièveté, le schéma complet a été tronqué. Vous pouvez trouver le fichier complet dans le dépôt du projet.
Avec le frontend en place, nous avons maintenant besoin de la logique côté serveur pour
gérer le flux OpenID4VCI. Nous commencerons par le premier point de terminaison que le
frontend appelle : /api/issue/authorize
.
/api/issue/authorize
: Créer l'offre d'identifiant#Ce point de terminaison est chargé de prendre les données de l'utilisateur, de générer un
code sécurisé à usage unique et de construire une credential_offer
que le wallet de
l'utilisateur peut comprendre.
Voici la logique de base :
// 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. Valider les données de l'utilisateur 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. Générer un code pré-autorisé et un code PIN const code = uuidv4(); const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes const txCode = Math.floor(1000 + Math.random() * 9000).toString(); // PIN à 4 chiffres // 3. Stocker le code et les données de l'utilisateur await createAuthorizationCode(uuidv4(), code, expiresAt); // Remarque : Ceci utilise un stockage en mémoire à des fins de démonstration uniquement. // En production, persistez les données de manière sécurisée dans une base de données avec une expiration appropriée. 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. Créer l'objet de l'offre d'identifiant const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const credentialOffer = { // L'identifiant de l'émetteur, qui est son URL de base. credential_issuer: baseUrl, // Un tableau des types d'identifiants que l'émetteur propose. credential_configuration_ids: ["eu.europa.ec.eudi.pid.1"], // Spécifie les types de subvention que le wallet peut utiliser. grants: { // Nous utilisons le flux de code pré-autorisé. "urn:ietf:params:oauth:grant-type:pre-authorized_code": { // Le code à usage unique que le wallet échangera contre un jeton. "pre-authorized_code": code, // Indique que l'utilisateur doit entrer un code PIN (tx_code) pour utiliser le code. user_pin_required: true, }, }, }; // 5. Créer l'URI complète de l'offre d'identifiant (un lien profond pour les wallets) const credentialOfferUri = `openid-credential-offer://?credential_offer=${encodeURIComponent( JSON.stringify(credentialOffer), )}`; // La réponse finale au frontend. return NextResponse.json({ // Le lien profond pour le QR code. credential_offer_uri: credentialOfferUri, // Le code pré-autorisé brut, pour affichage ou saisie manuelle. pre_authorized_code: code, // Le code PIN à 4 chiffres que l'utilisateur doit entrer dans son wallet. tx_code: txCode, }); } catch (error) { console.error("Authorization error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }
Étapes clés de ce point de terminaison :
pre-authorized_code
unique (un UUID) et un
tx_code
à 4 chiffres (PIN) pour une couche de sécurité supplémentaire.pre-authorized_code
est stocké dans la base de
données avec un court délai d'expiration. Les données de l'utilisateur et le PIN sont
stockés en mémoire, liés au code.credential_offer
conformément à la
spécification OpenID4VCI. Cet objet indique au wallet où se trouve l'émetteur, quels
identifiants il propose et le code nécessaire pour les obtenir.openid-credential-offer://...
) et la retourne au frontend, avec le tx_code
que
l'utilisateur doit voir./api/issue/token
: Échanger le code contre un jeton#Une fois que l'utilisateur a scanné le QR code et entré son PIN, le wallet effectue une
requête POST
à ce point de terminaison. Son travail consiste à valider le
pre-authorized_code
et le user_pin
(PIN), et s'ils sont valides, à émettre un
access token de courte durée.
Créez le fichier src/app/api/issue/token/route.ts
avec le contenu suivant :
// 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. Valider le type de subvention if (grant_type !== "urn:ietf:params:oauth:grant-type:pre-authorized_code") { return NextResponse.json( { error: "unsupported_grant_type" }, { status: 400 }, ); } // 2. Valider le code pré-autorisé const authCode = await getAuthorizationCode(code); if (!authCode) { return NextResponse.json( { error: "invalid_grant", error_description: "Invalid or expired code", }, { status: 400 }, ); } // 3. Valider le 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. Générer le jeton d'accès et le c_nonce const accessToken = uuidv4(); const cNonce = uuidv4(); const cNonceExpiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes // 5. Créer une nouvelle session d'émission const userData = (global as any).userDataStore?.get(code); await createIssuanceSession( uuidv4(), authCode.id, accessToken, cNonce, cNonceExpiresAt, userData, ); // 6. Marquer le code comme utilisé et nettoyer les données temporaires await markAuthorizationCodeAsUsed(code); (global as any).txCodeStore?.delete(code); (global as any).userDataStore?.delete(code); // 7. Retourner la réponse du jeton d'accès return NextResponse.json({ access_token: accessToken, token_type: "Bearer", expires_in: 3600, // 1 heure 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 }); } }
Étapes clés de ce point de terminaison :
pre-authorized_code
.pre-authorized_code
existe dans la base de
données, n'est pas expiré et n'a pas été utilisé auparavant.user_pin
du wallet avec le tx_code
que nous
avons stocké précédemment pour s'assurer que l'utilisateur a autorisé la transaction.access_token
sécurisé et un c_nonce
(credential nonce), qui est une valeur à usage unique pour prévenir les attaques par
rejeu sur le point de terminaison de l'identifiant.issuance_sessions
dans la
base de données, liant l'access token aux données de
l'utilisateur.pre-authorized_code
comme utilisé.access_token
et le c_nonce
au wallet./api/issue/credential
: Émettre l'identifiant signé#C'est le point de terminaison final et le plus important. Le wallet utilise le jeton
d'accès qu'il a reçu du point de terminaison /token
pour effectuer une requête POST
authentifiée à cette route. Le travail de ce point de terminaison est d'effectuer la
validation finale, de créer l'identifiant signé cryptographiquement et de le retourner au
wallet.
Créez le fichier src/app/api/issue/credential/route.ts
avec le contenu suivant :
// 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. Valider le jeton Bearer const authHeader = request.headers.get("authorization"); const accessToken = authHeader?.substring(7); const session = await getIssuanceSessionByToken(accessToken); if (!session) { return NextResponse.json({ error: "invalid_token" }, { status: 401 }); } // 2. Obtenir les données de l'utilisateur de la session const userData = session.user_data; if (!userData) { return NextResponse.json({ error: "missing_user_data" }, { status: 400 }); } // 3. Obtenir la clé d'émetteur active const issuerKey = await getActiveIssuerKey(); if (!issuerKey) { // Dans une application réelle, vous auriez un système de gestion de clés plus robuste. // Pour cette démo, nous pouvons générer une clé à la volée si elle n'existe pas. // Cette partie est omise pour des raisons de brièveté mais se trouve dans le dépôt. return NextResponse.json( { error: "server_error", error_description: "Failed to get issuer key", }, { status: 500 }, ); } // 4. Créer le 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. Stocker l'identifiant émis dans la base de données await createIssuedCredential(/* ... détails de l'identifiant ... */); await updateIssuanceSession(session.id, "credential_issued"); // 6. Retourner l'identifiant signé return NextResponse.json({ format: "jwt_vc", credential: credentialData, c_nonce: uuidv4(), // Un nouveau nonce pour les requêtes ultérieures c_nonce_expires_in: 300, }); } catch (error) { console.error("Credential endpoint error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }
Étapes clés de ce point de terminaison :
Bearer
valide dans
l'en-tête Authorization
et l'utilise pour rechercher la session d'émission active.createJWTVerifiableCredential
de src/lib/crypto.ts
pour construire et signer le JWT-VC.Vous disposez maintenant d'une implémentation complète et de bout en bout d'un émetteur d'identifiants numériques. Voici comment l'exécuter localement et ce que vous devez considérer pour le faire passer d'une preuve de concept à une application prête pour la production.
Clonez le dépôt :
git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
Installez les dépendances :
npm install
Démarrez la base de données : Assurez-vous que Docker est en cours d'exécution, puis démarrez le conteneur MySQL :
docker-compose up -d
Configurez l'environnement et lancez le tunnel : C'est l'étape la plus critique
pour les tests locaux. Étant donné que votre wallet mobile doit se connecter à votre
machine de développement via Internet, vous devez exposer votre serveur local avec une
URL HTTPS publique. Nous utiliserons ngrok
pour cela.
a. Démarrez ngrok :
ngrok http 3000
b. Copiez l'URL HTTPS depuis la sortie de
ngrok (par ex.,
https://random-string.ngrok.io
). c. Créez un fichier .env.local
et définissez
l'URL :
NEXT_PUBLIC_BASE_URL=https://<your-ngrok-url>
Exécutez l'application :
npm run dev
Ouvrez votre navigateur à l'adresse http://localhost:3000/issue
. Vous pouvez
maintenant remplir le formulaire, et le QR code généré pointera correctement vers
votre URL ngrok publique, permettant à votre wallet mobile de se connecter et de
recevoir l'identifiant.
ngrok
#Les protocoles d'identifiants numériques sont conçus avec la sécurité comme priorité
absolue. Pour cette raison, les wallets refuseront presque toujours de se connecter à un
émetteur via une connexion non sécurisée (http://
). L'ensemble du processus repose sur
une connexion HTTPS sécurisée, qui est activée par un certificat SSL.
Un service de tunnel comme ngrok
résout ces deux problèmes en créant une URL HTTPS
publique et sécurisée (avec un certificat SSL valide) qui transfère tout le trafic vers
votre serveur de développement local.
Les wallets nécessitent HTTPS et refuseront de se connecter à des points de terminaison
non sécurisés (http://
). C'est un outil essentiel pour tester tout service web qui doit
interagir avec des appareils mobiles ou des webhooks externes.
Cet exemple est intentionnellement axé sur le flux d'émission de base pour le rendre facile à comprendre. Les sujets suivants sont considérés comme hors du champ d'application :
revoked
pour une utilisation future, aucune
logique de révocation n'est fournie ici.pre-authorized_code
. Une implémentation complète du flux authorization_code
nécessiterait un écran de consentement de l'utilisateur et une logique
OAuth 2.0 plus complexe.C'est tout ! Avec quelques pages de code, nous avons maintenant un émetteur d'identifiants numériques complet et de bout en bout qui :
pre-authorized_code
d'OpenID4VCI.Bien que ce guide fournisse une base solide, un émetteur prêt pour la production
nécessiterait des fonctionnalités supplémentaires comme une gestion robuste des clés, un
stockage persistant au lieu de stockages en mémoire, la révocation des identifiants et un
renforcement complet de la sécurité.
La compatibilité des wallets varie également ; Sphereon Wallet est recommandé pour les
tests, mais d'autres wallets pourraient ne pas prendre en charge le flux pré-autorisé tel
qu'implémenté ici. Cependant, les éléments de base et le flux d'interaction resteraient
les mêmes. En suivant ces modèles, vous pouvez construire un émetteur sécurisé et
interopérable pour tout type d'identifiant numérique.
Voici quelques-unes des principales ressources, spécifications et outils utilisés ou référencés dans ce tutoriel :
Dépôt du projet :
Spécifications clés :
did:web
: La méthode DID
utilisée pour la clé publique de notre émetteur.Outils :
Bibliothèques :
Related Articles
Table of Contents