Aprende a construir un verificador de credenciales digitales desde cero con Next.js, OpenID4VP e ISO mDoc. Esta guía para desarrolladores te muestra paso a paso cómo crear un verificador que puede solicitar, recibir y validar licencias de conducir móviles
Amine
Created: August 20, 2025
Updated: August 21, 2025
See the original blog version in English here.
Probar identidades en línea es un desafío constante que nos ha llevado a depender de contraseñas y a compartir documentos sensibles por canales inseguros. Esto ha hecho que la verificación de identidad para las empresas sea un proceso lento, costoso y propenso al fraude. Las credenciales digitales ofrecen un nuevo enfoque que devuelve a los usuarios el control de sus datos. Son el equivalente digital de una wallet física, que contiene desde una licencia de conducir hasta un título universitario, pero con los beneficios añadidos de ser criptográficamente seguras, preservar la privacidad y poder verificarse al instante.
Esta guía ofrece a los desarrolladores un tutorial práctico y paso a paso para construir un verificador de credenciales digitales. Aunque los estándares existen, hay poca orientación sobre cómo implementarlos. Este tutorial llena ese vacío, mostrando cómo construir un verificador utilizando la API nativa de credenciales digitales del navegador, OpenID4VP para el protocolo de presentación e ISO mDoc (por ejemplo, una licencia de conducir móvil) como formato de credencial.
El resultado final será una aplicación Next.js simple pero funcional que puede solicitar, recibir y verificar una credencial digital desde una wallet móvil compatible.
Aquí tenemos un vistazo rápido a la aplicación final en acción. El proceso consta de cuatro pasos principales:
Paso 1: Página inicial El usuario llega a la página inicial y hace clic en "Verificar con Identidad Digital" para iniciar el proceso.
Paso 2: Solicitud de confianza El navegador solicita al usuario que confíe en el sitio. El usuario hace clic en "Continuar" para proceder.
Paso 3: Escaneo de código QR Se muestra un código QR que el usuario escanea con su aplicación de wallet compatible.
Paso 4: Credencial decodificada Tras una verificación exitosa, la aplicación muestra los datos de la credencial decodificada.
Recent Articles
📝
Cómo construir un verificador de credenciales digitales (Guía para desarrolladores)
📝
Cómo construir un emisor de credenciales digitales (Guía para desarrolladores)
📖
Clave residente de WebAuthn: credenciales descubribles como passkeys
🔑
Acceso con tarjetas físicas y Passkeys: Guía técnica
🔑
MFA obligatoria y transición a Passkeys: Buenas prácticas
La magia detrás de las credenciales digitales reside en un modelo simple pero potente de "triángulo de confianza" que involucra a tres actores clave:
Cuando un usuario quiere acceder a un servicio, presenta la credencial desde su wallet. El verificador puede entonces comprobar instantáneamente su autenticidad sin necesidad de contactar directamente al emisor original.
Para que este ecosistema de identidad descentralizada prospere, el papel del verificador es absolutamente crucial. Son los guardianes de esta nueva infraestructura de confianza, los que consumen las credenciales y las hacen útiles en el mundo real. Como ilustra el siguiente diagrama, un verificador completa el triángulo de confianza solicitando, recibiendo y validando una credencial del titular.
Si eres desarrollador, construir un servicio para realizar esta verificación es una habilidad fundamental para la próxima generación de aplicaciones seguras y centradas en el usuario. Esta guía está diseñada para guiarte exactamente a través de ese proceso. Cubriremos todo lo que necesitas saber para construir tu propio verificador de credenciales verificables, desde los conceptos y estándares básicos hasta los detalles de implementación paso a paso para validar firmas y comprobar el estado de las credenciales.
¿Quieres ir directamente al final? Puedes encontrar el proyecto completo y terminado de este tutorial en GitHub. Siéntete libre de clonarlo y probarlo tú mismo: https://github.com/corbado/digital-credentials-example
Empecemos.
Antes de empezar, asegúrate de tener:
Ahora repasaremos cada uno de estos prerrequisitos en detalle, empezando por los estándares y protocolos que sustentan este verificador basado en mdoc.
Nuestro verificador está construido para lo siguiente:
Estándar / Protocolo | Descripción |
---|---|
W3C VC | El modelo de datos de credenciales verificables del W3C. Define la estructura estándar para las credenciales digitales, incluyendo declaraciones (claims), metadatos y pruebas. |
SD-JWT | Divulgación selectiva para JWT. Un formato para VCs basado en JSON Web Tokens que permite a los titulares divulgar selectivamente solo declaraciones específicas de una credencial, mejorando la privacidad. |
ISO mDoc | ISO/IEC 18013-5. El estándar internacional para licencias de conducir móviles (mDL) y otras identificaciones móviles, que define estructuras de datos y protocolos de comunicación para uso en línea y sin conexión. |
OpenID4VP | OpenID para presentaciones verificables. Un protocolo de presentación interoperable construido sobre OAuth 2.0. Define cómo un verificador solicita credenciales y cómo la wallet del titular las presenta. |
Para este tutorial, usamos específicamente:
Nota sobre el alcance: Aunque introducimos brevemente W3C VC y SD-JWT para proporcionar un contexto más amplio, este tutorial implementa exclusivamente credenciales ISO mDoc a través de OpenID4VP. Las VCs basadas en W3C están fuera del alcance de este ejemplo.
El estándar ISO/IEC 18013-5 mDoc define la estructura y codificación para documentos móviles como las licencias de conducir móviles (mDL). Las credenciales mDoc están codificadas en CBOR, firmadas criptográficamente y pueden presentarse digitalmente para su verificación. Nuestro verificador se centrará en decodificar y validar estas credenciales mdoc.
OpenID4VP es un protocolo interoperable para solicitar y presentar credenciales digitales, construido sobre OAuth 2.0 y OpenID Connect. En esta implementación, OpenID4VP se utiliza para:
Ahora que tenemos una comprensión clara de los estándares y protocolos, necesitamos elegir la pila tecnológica adecuada para construir nuestro verificador. Nuestras elecciones están diseñadas para la robustez, la experiencia del desarrollador y la compatibilidad con el ecosistema web moderno.
Usaremos TypeScript tanto para nuestro código de frontend como de backend. Como superconjunto de JavaScript, añade tipado estático, lo que ayuda a detectar errores temprano, mejora la calidad del código y facilita la gestión de aplicaciones complejas. En un contexto sensible a la seguridad como la verificación de credenciales, la seguridad de tipos es una gran ventaja.
Next.js es nuestro framework de elección porque proporciona una experiencia fluida e integrada para construir aplicaciones full-stack.
redirect_uri
para recibir y verificar de forma
segura la respuesta final de la CMWallet.Nuestra implementación se basa en un conjunto específico de bibliotecas para el frontend y el backend:
Nota sobre openid-client
: Los verificadores más avanzados y de grado de producción
podrían usar la biblioteca openid-client
para manejar el protocolo OpenID4VP
directamente en el backend, habilitando características como una redirect_uri
dinámica. En un flujo OpenID4VP impulsado por el servidor con una redirect_uri
,
openid-client
se usaría para analizar y validar las respuestas vp_token
directamente. Para este tutorial, estamos utilizando un flujo más simple, mediado por el
navegador, que no lo requiere, lo que facilita la comprensión del proceso.
Esta pila tecnológica garantiza una implementación de verificador robusta, con seguridad de tipos y escalable, centrada en la API de credenciales digitales del navegador y el formato de credencial ISO mDoc.
Para probar tu verificador, necesitas una wallet móvil que pueda interactuar con la API de credenciales digitales del navegador.
Usaremos la CMWallet, una robusta wallet de prueba compatible con OpenID4VP para Android.
Cómo instalar CMWallet (Android):
Nota: Solo instala archivos APK de fuentes en las que confíes. El enlace proporcionado es del repositorio oficial del proyecto.
Antes de sumergirnos en la implementación, es esencial comprender los conceptos criptográficos que sustentan las credenciales verificables. Esto es lo que las hace "verificables" y confiables.
En esencia, una credencial verificable es un conjunto de declaraciones (como nombre, fecha de nacimiento, etc.) que ha sido firmada digitalmente por un emisor. Una firma digital proporciona dos garantías cruciales:
Las firmas digitales se crean utilizando criptografía de clave pública/privada (también llamada criptografía asimétrica). Así es como funciona en nuestro contexto:
Nota sobre los DID: En este tutorial, no resolvemos las claves del emisor a través de DID. En producción, los emisores normalmente expondrían sus claves públicas a través de DID u otros endpoints autorizados, que el verificador usaría para la validación criptográfica.
Las credenciales verificables a menudo se formatean como JSON Web Tokens (JWT). Un JWT
es una forma compacta y segura para URL de representar declaraciones que se transferirán
entre dos partes. Un JWT firmado (también conocido como JWS) tiene tres partes separadas
por puntos (.
):
alg
).vc
), incluyendo
el issuer
, credentialSubject
, etc.// Ejemplo de una estructura JWT [Encabezado].[Carga útil].[Firma]
Nota: Las credenciales verificables basadas en JWT están fuera del alcance de esta publicación de blog. Esta implementación se centra en las credenciales ISO mDoc y OpenID4VP, no en las credenciales verificables W3C o las credenciales basadas en JWT.
No es suficiente que un verificador sepa que una credencial es válida; también necesita saber que la persona que presenta la credencial es el titular legítimo. Esto evita que alguien use una credencial robada.
Esto se soluciona usando una Presentación Verificable (VP). Una VP es un envoltorio alrededor de una o más VCs que está firmado por el propio titular.
El flujo es el siguiente:
Nuestro verificador debe entonces realizar dos comprobaciones de firma separadas:
Esta comprobación de dos niveles garantiza tanto la autenticidad de la credencial como la identidad de la persona que la presenta, creando un modelo de confianza robusto y seguro.
Nota: El concepto de presentaciones verificables tal como se define en el ecosistema
W3C VC está fuera del alcance de esta publicación de blog. El término
Presentación Verificable aquí se refiere a la
respuesta vp_token
de OpenID4VP, que se comporta de manera similar a una VP de W3C
pero se basa en la semántica de ISO mDoc en lugar del modelo de firma
JSON-LD de W3C. Esta guía se centra en las credenciales ISO mDoc y
OpenID4VP, no en las presentaciones verificables W3C ni en su validación de firma.
Nuestra arquitectura de verificador utiliza la API de credenciales digitales integrada del navegador como un intermediario seguro para conectar nuestra aplicación web con la CMWallet móvil del usuario. Este enfoque simplifica el flujo al permitir que el navegador maneje la visualización nativa del código QR y la comunicación con la wallet.
navigator.credentials.get()
del navegador, recibir el resultado y reenviarlo a nuestro
backend para su verificación.openid4vp
y genera un código QR de forma
nativa. Luego espera a que la wallet devuelva una respuesta.Aquí hay un diagrama de secuencia que ilustra el flujo completo y preciso:
Explicación del flujo:
/api/verify/start
),
que genera un objeto de solicitud que contiene la consulta y un nonce, y luego lo
devuelve.navigator.credentials.get()
con el objeto de solicitud.openid4vp
y
muestra un código QR de forma nativa. La promesa .get()
ahora está pendiente.Nota: Este flujo de código QR ocurre en navegadores de escritorio. En navegadores
móviles (Android Chrome con la bandera experimental habilitada), el navegador puede
comunicarse directamente con wallets compatibles en el mismo dispositivo, eliminando la
necesidad de escanear un código QR. Para habilitar esta función en Android Chrome,
navega a chrome://flags#web-identity-digital-credentials
y establece la bandera en
"Enabled".
.get()
en el frontend finalmente se resuelve, entregando la carga útil de la
presentación./api/verify/finish
de nuestro backend. El backend
valida el nonce y la credencial.Ahora que tenemos una sólida comprensión de los estándares, protocolos y el flujo arquitectónico, podemos empezar a construir nuestro verificador.
Sigue el paso a paso o usa el código final
Ahora repasaremos la configuración y la implementación del código paso a paso. Si prefieres saltar directamente al producto terminado, puedes clonar el proyecto completo desde nuestro repositorio de GitHub y ejecutarlo localmente.
git clone https://github.com/corbado/digital-credentials-example.git
Primero, inicializaremos un nuevo proyecto de Next.js, instalaremos las dependencias necesarias y pondremos en marcha nuestra base de datos.
Abre tu terminal, navega al directorio donde quieres crear tu proyecto y ejecuta el siguiente comando. Estamos usando el App Router, TypeScript y Tailwind CSS para este proyecto.
npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm
Este comando crea una nueva aplicación Next.js en tu directorio actual.
A continuación, necesitamos instalar las bibliotecas que se encargarán de la decodificación CBOR, las conexiones a la base de datos y la generación de UUID.
npm install cbor-web mysql2 uuid @types/uuid
Este comando instala:
cbor-web
: Para decodificar la carga útil de la credencial mdoc.mysql2
: El cliente de MySQL para nuestra base de datos.uuid
: Para generar cadenas de desafío únicas.@types/uuid
: Tipos de TypeScript para la biblioteca uuid
.Nuestro backend requiere una base de datos MySQL para almacenar los datos de la sesión
OIDC, asegurando que cada flujo de verificación sea seguro y con estado. Hemos incluido un
archivo docker-compose.yml
para facilitar esto.
Si has clonado el repositorio, puedes simplemente ejecutar docker-compose up -d
. Si
estás construyendo desde cero, crea un archivo llamado docker-compose.yml
con el
siguiente contenido:
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:
Esta configuración de Docker Compose también requiere un script de inicialización SQL.
Crea un directorio llamado sql
y dentro de él, un archivo llamado init.sql
con el
siguiente contenido para configurar las tablas necesarias:
-- 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) );
Una vez que ambos archivos estén en su lugar, abre tu terminal en la raíz del proyecto y ejecuta:
docker-compose up -d
Este comando iniciará un contenedor de MySQL en segundo plano.
Nuestra aplicación Next.js está estructurada para separar las responsabilidades entre el frontend y el backend, aunque forman parte del mismo proyecto.
src/app/page.tsx
): Una única página de React que
inicia el flujo de verificación y muestra el resultado. Interactúa con la
API de credenciales digitales del navegador.src/app/api/verify/...
):
start/route.ts
: Genera la solicitud OpenID4VP y un nonce de seguridad.finish/route.ts
: Recibe la presentación de la wallet (a través del navegador),
valida el nonce y decodifica la credencial.src/lib/
):
database.ts
: Gestiona todas las interacciones con la base de datos (creación de
desafíos, verificación de sesiones).crypto.ts
: Se encarga de la decodificación de la credencial mDoc basada en CBOR.Aquí hay un diagrama que ilustra la arquitectura interna:
Nuestro frontend es intencionadamente ligero. Su responsabilidad principal es actuar como el disparador de cara al usuario para el flujo de verificación y comunicarse tanto con nuestro backend como con las capacidades nativas de manejo de credenciales del navegador. No contiene ninguna lógica de protocolo compleja; todo eso se delega.
Específicamente, el frontend se encargará de lo siguiente:
/api/verify/start
y recibe una
carga útil JSON estructurada (protocol
, request
, state
) que describe exactamente
lo que la wallet debe presentar.navigator.credentials.get()
, que renderiza un código QR nativo y espera la respuesta
de la wallet./api/verify/finish
en una solicitud POST para la validación final del lado del servidor.La lógica principal está en la función startVerification
:
// src/app/page.tsx const startVerification = async () => { setLoading(true); setVerificationResult(null); try { // 1. Comprobar si el navegador soporta la API if (!navigator.credentials?.get) { throw new Error("El navegador no soporta la API de credenciales."); } // 2. Pedir a nuestro backend un objeto de solicitud const res = await fetch("/api/verify/start"); const { protocol, request } = await res.json(); // 3. Pasar ese objeto al navegador – esto activa el código QR nativo const credential = await (navigator.credentials as any).get({ mediation: "required", digital: { requests: [ { protocol, // "openid4vp" data: request, // contiene dcql_query, nonce, etc. }, ], }, }); // 4. Reenviar la respuesta de la wallet (desde el navegador) a nuestro endpoint de finalización para las comprobaciones del lado del servidor const verifyRes = await fetch("/api/verify/finish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(credential), }); const result = await verifyRes.json(); if (verifyRes.ok && result.verified) { setVerificationResult(`Éxito: ${result.message}`); } else { throw new Error(result.message || "La verificación falló."); } } catch (err) { setVerificationResult(`Error: ${(err as Error).message}`); } finally { setLoading(false); } };
Esta función muestra los cuatro pasos clave de la lógica del frontend: comprobar el soporte de la API, obtener la solicitud del backend, llamar a la API del navegador y enviar el resultado de vuelta para su verificación. El resto del archivo es código estándar de React para el estado y la renderización de la UI, que puedes ver en el repositorio de GitHub.
digital
y mediation: 'required'
?#Puede que notes que nuestra llamada a navigator.credentials.get()
se ve diferente de
ejemplos más simples. Esto se debe a que nos adherimos estrictamente a la
especificación oficial de la API de credenciales digitales del W3C.
Miembro digital
: La especificación requiere que todas las solicitudes de
credenciales digitales estén anidadas dentro de un objeto digital
. Esto proporciona un
espacio de nombres claro y estandarizado para esta API, distinguiéndola de otros tipos
de credenciales (como password
o federated
) y permitiendo futuras extensiones sin
conflictos.
mediation: 'required'
: Esta opción es una característica crucial de seguridad y
experiencia de usuario. Obliga a que el usuario interactúe activamente con una
indicación (por ejemplo, un escaneo biométrico, la entrada de un PIN o una pantalla de
consentimiento) para aprobar la solicitud de credencial. Sin ella, un sitio web podría
intentar acceder a las credenciales en segundo plano de forma silenciosa, lo que supone
un riesgo significativo para la privacidad. Al requerir la mediación, nos aseguramos de
que el usuario siempre tenga el control y dé su consentimiento explícito para cada
transacción.
Con la interfaz de usuario de React en su lugar, ahora necesitamos dos rutas de API que hagan el trabajo pesado en el servidor:
/api/verify/start
– construye una solicitud OpenID4VP, persiste un desafío de un
solo uso en MySQL y devuelve todo al navegador./api/verify/finish
– recibe la respuesta de la wallet, valida el desafío,
verifica y decodifica la credencial, y finalmente devuelve un resultado JSON conciso a
la UI./api/verify/start
: Generar la solicitud OpenID4VP#// src/app/api/verify/start/route.ts import { NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { createChallenge, cleanupExpiredChallenges } from "@/lib/database"; export async function GET() { // 1️⃣ Crear un nonce (desafío) aleatorio y de corta duración const challenge = uuidv4(); const challengeId = uuidv4(); const expiresAt = new Date(Date.now() + 5 * 60 * 1000); await createChallenge(challengeId, challenge, expiresAt); cleanupExpiredChallenges().catch(console.error); // 2️⃣ Construir una consulta DCQL que describa *qué* queremos const dcqlQuery = { credentials: [ { id: "cred1", format: "mso_mdoc", meta: { doctype_value: "eu.europa.ec.eudi.pid.1" }, claims: [ { path: ["eu.europa.ec.eudi.pid.1", "family_name"] }, { path: ["eu.europa.ec.eudi.pid.1", "given_name"] }, { path: ["eu.europa.ec.eudi.pid.1", "birth_date"] }, ], }, ], }; // 3️⃣ Devolver un objeto que el navegador pueda pasar a navigator.credentials.get() return NextResponse.json({ protocol: "openid4vp", // le dice al navegador qué protocolo de wallet usar request: { dcql_query: dcqlQuery, // QUÉ presentar nonce: challenge, // anti-repetición response_type: "vp_token", response_mode: "dc_api", // la wallet hará un POST directamente a /finish }, state: { credential_type: "mso_mdoc", // se guarda para comprobaciones posteriores nonce: challenge, challenge_id: challengeId, }, }); }
Parámetros clave
• nonce
– desafío criptográfico que vincula
la solicitud y la respuesta (previene la repetición). • dcql_query
– Un objeto que
describe las declaraciones exactas que necesitamos. Para esta guía, usamos una
estructura dcql_query
inspirada en borradores recientes del Digital Credential Query
Language, aunque este aún no es un estándar finalizado. • state
– JSON arbitrario
que la wallet devuelve para que podamos buscar el registro en la base de datos.
El archivo src/lib/database.ts
envuelve las operaciones básicas de MySQL para los
desafíos y las sesiones de verificación (insertar, leer, marcar como usado). Mantener esta
lógica en un único módulo facilita el cambio del almacén de datos más adelante.
/api/verify/finish
: Validar y decodificar la presentación#// src/app/api/verify/finish/route.ts import { NextResponse, NextRequest } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { getChallenge, markChallengeAsUsed, createVerificationSession, updateVerificationSession, } from "@/lib/database"; import { decodeDigitalCredential, decodeAllNamespaces } from "@/lib/crypto"; export async function POST(request: NextRequest) { const body = await request.json(); // 1️⃣ Extraer las piezas de la presentación verificable const vpTokenMap = body.vp_token ?? body.data?.vp_token; const state = body.state; const mdocToken = vpTokenMap?.cred1; // pedimos este ID en dcqlQuery if (!vpTokenMap || !state || !mdocToken) { return NextResponse.json( { verified: false, message: "Respuesta mal formada" }, { status: 400 }, ); } // 2️⃣ Validación del desafío de un solo uso const stored = await getChallenge(state.nonce); if (!stored) { return NextResponse.json( { verified: false, message: "Desafío inválido o caducado" }, { status: 400 }, ); } const sessionId = uuidv4(); await createVerificationSession(sessionId, stored.id); // 3️⃣ Comprobaciones (pseudo)criptográficas – reemplazar con validación mDL real en producción // En una aplicación real, usarías una biblioteca dedicada para realizar la validación // criptográfica completa de la firma mdoc contra la clave pública del emisor. const isValid = mdocToken.length > 0; if (!isValid) { await updateVerificationSession(sessionId, "failed", { reason: "la validación de mdoc falló", }); return NextResponse.json( { verified: false, message: "La validación de la credencial falló" }, { status: 400 }, ); } // 4️⃣ Decodificar la carga útil de la licencia de conducir móvil (mdoc) a JSON legible const decoded = await decodeDigitalCredential(mdocToken); const readable = decodeAllNamespaces(decoded)["eu.europa.ec.eudi.pid.1"]; await markChallengeAsUsed(state.nonce); await updateVerificationSession(sessionId, "verified", { readable }); return NextResponse.json({ verified: true, message: "¡Credencial mdoc verificada con éxito!", credentialData: readable, sessionId, }); }
Campos importantes en la respuesta de la wallet
• vp_token
– mapa que contiene cada credencial que la wallet devuelve. Para
nuestra demo extraemos vp_token.cred1
. • state
– eco del blob que proporcionamos
en /start
; contiene el nonce
para que podamos buscar el registro en la base de
datos. • mdocToken
– una estructura CBOR codificada en Base64URL que representa el
ISO mDoc.
Cuando el verificador recibe una credencial mdoc del navegador, es una cadena Base64URL
que contiene datos binarios codificados en CBOR. Para extraer las declaraciones reales, el
endpoint finish
realiza un proceso de decodificación de varios pasos utilizando
funciones de ayuda de src/lib/crypto.ts
.
La función decodeDigitalCredential
se encarga de la conversión de la cadena codificada a
un objeto utilizable:
// src/lib/crypto.ts export async function decodeDigitalCredential(encodedCredential: string) { // 1. Convertir Base64URL a Base64 estándar const base64UrlToBase64 = (input: string) => { let base64 = input.replace(/-/g, "+").replace(/_/g, "/"); const pad = base64.length % 4; if (pad) base64 += "=".repeat(4 - pad); return base64; }; const base64 = base64UrlToBase64(encodedCredential); // 2. Decodificar Base64 a binario const binaryString = atob(base64); const byteArray = Uint8Array.from(binaryString, (char) => char.charCodeAt(0)); // 3. Decodificar CBOR const decoded = await cbor.decodeFirst(byteArray); return decoded; }
cbor-web
para decodificar los datos
binarios en un objeto JavaScript estructurado.La función decodeAllNamespaces
procesa aún más el objeto CBOR decodificado para extraer
las declaraciones reales de los espacios de nombres relevantes:
// src/lib/crypto.ts export function decodeAllNamespaces(jsonObj) { const decoded = {}; try { jsonObj.documents.forEach((doc, idx) => { // 1) issuerSigned.nameSpaces: const issuerNS = doc.issuerSigned?.nameSpaces || {}; Object.entries(issuerNS).forEach(([nsName, entries]) => { if (!decoded[nsName]) decoded[nsName] = {}; (entries as any[]).forEach((entry) => { const bytes = Uint8Array.from(entry.value); const decodedEntry = cbor.decodeFirstSync(bytes); Object.assign(decoded[nsName], decodedEntry); }); }); // 2) deviceSigned.nameSpaces (si está presente): const deviceNS = doc.deviceSigned?.nameSpaces; if (deviceNS?.value?.data) { const bytes = Uint8Array.from(deviceNS.value); decoded[`deviceSigned_ns_${idx}`] = cbor.decodeFirstSync(bytes); } }); } catch (e) { console.error(e); } return decoded; }
eu.europa.ec.eudi.pid.1
) para
extraer los valores reales de las declaraciones (como nombre, fecha de nacimiento,
etc.).Después de ejecutar estos pasos, el endpoint de finalización obtiene un objeto legible por humanos que contiene las declaraciones del mdoc, por ejemplo:
{ "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" }
Este proceso garantiza que el verificador pueda extraer de forma segura y fiable la información necesaria de la credencial mdoc para su visualización y posterior procesamiento.
El endpoint de finalización devuelve un objeto JSON mínimo al frontend:
{ "verified": true, "message": "¡Credencial mdoc verificada con éxito!", "credentialData": { "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" } }
El frontend recibe esta respuesta en startVerification()
y simplemente la persiste en el
estado de React para que podamos renderizar una bonita tarjeta de confirmación o mostrar
declaraciones individuales, por ejemplo, “¡Bienvenido, John Doe (nacido el
1990-01-01)!”.
Ahora tienes un verificador completo y funcional que utiliza las capacidades nativas de manejo de credenciales del navegador. Aquí te explicamos cómo ejecutarlo localmente y qué puedes hacer para llevarlo de una prueba de concepto a una aplicación lista para producción.
Clona el repositorio:
git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
Instala las dependencias:
npm install
Inicia la base de datos: Asegúrate de que Docker se está ejecutando en tu máquina y luego inicia el contenedor de MySQL:
docker-compose up -d
Ejecuta la aplicación:
npm run dev
Abre tu navegador en http://localhost:3000
y deberías ver la interfaz de usuario del
verificador. Ahora puedes usar tu CMWallet para escanear el código QR y completar el
flujo de verificación.
Este tutorial proporciona los componentes básicos para un verificador. Para que esté listo para producción, necesitarías implementar varias características adicionales:
Validación criptográfica completa: La implementación actual utiliza una comprobación
de marcador de posición (mdocToken.length > 0
). En un escenario real, debes realizar
una validación criptográfica completa de la firma mdoc contra la clave pública del
emisor (por ejemplo, resolviendo su DID o recuperando su certificado
de clave pública). Para los estándares de resolución de DID, consulta la
especificación de resolución de DID del W3C.
Comprobación de revocación del emisor: Las credenciales pueden ser revocadas por el emisor antes de su fecha de caducidad. Un verificador de producción debe comprobar el estado de la credencial consultando una lista de revocación o un endpoint de estado proporcionado por el emisor. La Lista de estado de credenciales verificables del W3C proporciona el estándar para las listas de revocación de credenciales.
Manejo de errores robusto y seguridad: Añade un manejo de errores completo, validación de entradas, limitación de velocidad en los endpoints de la API y asegúrate de que toda la comunicación sea a través de HTTPS (TLS) para proteger los datos en tránsito. Las directrices de seguridad de API de OWASP proporcionan las mejores prácticas de seguridad para API.
Soporte para múltiples tipos de credenciales: Amplía la lógica para manejar
diferentes valores de doctype
y formatos de credenciales si esperas recibir más que
solo la credencial PID de la Identidad Digital Europea
(EUDI). El
Modelo de Datos de Credenciales Verificables del W3C
proporciona especificaciones completas de formato de VC.
Este ejemplo está intencionadamente enfocado en el flujo central mediado por el navegador para que sea fácil de entender. Los siguientes temas se consideran fuera del alcance:
redirect_uri
o el registro dinámico de clientes.Al construir sobre esta base e incorporar estos próximos pasos, puedes desarrollar un verificador robusto y seguro capaz de confiar y validar credenciales digitales en tus propias aplicaciones.
¡Eso es todo! Con menos de 250 líneas de TypeScript, ahora tenemos un verificador de extremo a extremo que:
En producción, reemplazarías la validación de marcador de posición con comprobaciones completas de ISO 18013-5, añadirías búsquedas de revocación del emisor, limitación de velocidad, registro de auditoría y, por supuesto, TLS de extremo a extremo, pero los componentes básicos siguen siendo exactamente los mismos.
Aquí tienes algunos de los recursos, especificaciones y herramientas clave utilizados o referenciados en este tutorial:
Repositorio del proyecto:
Especificaciones clave:
Herramientas:
Bibliotecas:
Related Articles
Table of Contents