Get your free and exclusive 80-page Banking Passkey Report
Back to Overview

Cómo construir un verificador de credenciales digitales (Guía para desarrolladores)

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

Blog-Post-Header-Image

See the original blog version in English here.

DigitalCredentialsDemo Icon

Want to experience digital credentials in action?

Try Digital Credentials

1. Introducción#

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.

1.1 Cómo funciona#

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:

  • Emisor: Una autoridad de confianza (por ejemplo, una agencia gubernamental, una universidad o un banco) que firma criptográficamente y emite una credencial a un usuario.
  • Titular: El usuario, que recibe la credencial y la almacena de forma segura en una billetera digital personal en su dispositivo.
  • Verificador: Una aplicación o servicio que necesita comprobar la credencial del usuario.

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.

1.2 Por qué los verificadores son esenciales (y por qué estás aquí)#

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.

2. Prerrequisitos para construir un verificador#

Antes de empezar, asegúrate de tener:

  1. Conocimientos básicos de credenciales digitales y mdoc
    • Este tutorial se centra en el formato ISO mDoc (por ejemplo, para licencias de conducir móviles) y no cubre otros formatos como las credenciales verificables (VC) del W3C. Estar familiarizado con los conceptos básicos de mdoc será útil.
  2. Docker y Docker Compose
    • Nuestro proyecto utiliza una base de datos MySQL en un contenedor de Docker para gestionar el estado de la sesión OIDC. Asegúrate de tener ambos instalados y en funcionamiento.
  3. Protocolo elegido: OpenID4VP
    • Usaremos el protocolo OpenID4VP (OpenID for Verifiable Presentations) para el flujo de intercambio de credenciales.
  4. Pila tecnológica lista
    • Usaremos TypeScript (Node.js) para la lógica del backend.
    • Usaremos Next.js tanto para el backend (rutas de API) como para el frontend (UI).
    • Bibliotecas clave: bibliotecas de decodificación CBOR para el análisis de mdoc y un cliente de MySQL.
  5. Credenciales de prueba y wallet
  6. Conocimientos básicos de criptografía
    • Entender las firmas digitales y los conceptos de clave pública/privada en relación con los flujos de mdoc y OIDC.

Ahora repasaremos cada uno de estos prerrequisitos en detalle, empezando por los estándares y protocolos que sustentan este verificador basado en mdoc.

2.1 Elección de protocolos#

Nuestro verificador está construido para lo siguiente:

Estándar / ProtocoloDescripción
W3C VCEl modelo de datos de credenciales verificables del W3C. Define la estructura estándar para las credenciales digitales, incluyendo declaraciones (claims), metadatos y pruebas.
SD-JWTDivulgació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 mDocISO/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.
OpenID4VPOpenID 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:

  • OpenID4VP como el protocolo para solicitar y recibir credenciales.
  • ISO mDoc como el formato de credencial (por ejemplo, para licencias de conducir móviles).

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.

2.1.1 ISO mDoc (Mobile Document)#

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.

2.1.2 OpenID4VP (OpenID for Verifiable Presentations)#

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:

  • Iniciar el flujo de presentación de credenciales (a través de un código QR o API del navegador)
  • Recibir la credencial mdoc de la wallet del usuario
  • Garantizar un intercambio de credenciales seguro, con estado y que preserva la privacidad

2.2 Elección de la pila tecnológica#

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.

2.2.1 Lenguaje: TypeScript#

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.

2.2.2 Framework: Next.js#

Next.js es nuestro framework de elección porque proporciona una experiencia fluida e integrada para construir aplicaciones full-stack.

  • Para el frontend: Usaremos Next.js con React para construir la interfaz de usuario donde se inicia el proceso de verificación (por ejemplo, mostrando un código QR).
  • Para el backend: Aprovecharemos las rutas de API de Next.js para crear los endpoints del lado del servidor. Estos endpoints son responsables de crear solicitudes OpenID4VP válidas y de actuar como redirect_uri para recibir y verificar de forma segura la respuesta final de la CMWallet.

2.2.3 Bibliotecas clave#

Nuestra implementación se basa en un conjunto específico de bibliotecas para el frontend y el backend:

  • next: El framework Next.js, utilizado tanto para las rutas de API del backend como para la UI del frontend.
  • react y react-dom: Impulsan la interfaz de usuario del frontend.
  • cbor-web: Se utiliza para decodificar credenciales mdoc codificadas en CBOR en objetos JavaScript utilizables.
  • mysql2: Proporciona conectividad a la base de datos MySQL para almacenar desafíos y sesiones de verificación.
  • uuid: Una biblioteca para generar cadenas de desafío (nonces) únicas.
  • @types/uuid: Tipos de TypeScript para la generación de UUID.

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.

2.3 Obtener una wallet y credenciales de prueba#

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

  1. Descarga el archivo APK usando el enlace de arriba directamente en tu dispositivo Android.
  2. Abre Ajustes > Seguridad en tu dispositivo.
  3. Habilita "Instalar aplicaciones desconocidas" para el navegador que usaste para descargar el archivo.
  4. Localiza el APK descargado en tu carpeta "Descargas" y tócalo para comenzar la instalación.
  5. Sigue las instrucciones en pantalla para completar la instalación.
  6. Abre CMWallet y la encontrarás precargada con credenciales de prueba, lista para el flujo de verificación.

Nota: Solo instala archivos APK de fuentes en las que confíes. El enlace proporcionado es del repositorio oficial del proyecto.

2.4 Conocimientos de criptografía#

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.

2.4.1 Firmas digitales: la base de la confianza#

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:

  • Autenticidad: Prueba que la credencial fue creada por el emisor y no por un impostor.
  • Integridad: Prueba que la credencial no ha sido alterada o manipulada desde que fue firmada.

2.4.2 Criptografía de clave pública/privada#

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:

  1. El emisor tiene un par de claves: una clave privada, que se mantiene en secreto, y una clave pública, que está disponible para todos (generalmente a través de su Documento DID).
  2. Firma: Cuando un emisor crea una credencial, utiliza su clave privada para generar una firma digital única para esos datos de credencial específicos.
  3. Verificación: Cuando nuestro verificador recibe la credencial, utiliza la clave pública del emisor para comprobar la firma. Si la comprobación es exitosa, el verificador sabe que la credencial es auténtica y no ha sido manipulada. Cualquier cambio en los datos de la credencial invalidaría la firma.

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.

2.4.3 Credenciales verificables como JWT#

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 (.):

  • Encabezado: Contiene metadatos sobre el token, como el algoritmo de firma utilizado (alg).
  • Carga útil (Payload): Contiene las declaraciones reales de la credencial verificable (declaración vc), incluyendo el issuer, credentialSubject, etc.
  • Firma: La firma digital generada por el emisor, que cubre el encabezado y la carga útil.
// 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.

2.4.4 La presentación verificable: probar la posesión#

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:

  1. El verificador le pide al usuario que presente una credencial.
  2. La wallet del usuario crea una Presentación Verificable, empaqueta las credenciales requeridas dentro de ella y firma la presentación completa usando la clave privada del titular.
  3. La wallet envía esta VP firmada al verificador.

Nuestro verificador debe entonces realizar dos comprobaciones de firma separadas:

  1. Verificar la(s) credencial(es): Comprobar la firma en cada VC dentro de la presentación usando la clave pública del emisor. (Prueba que la credencial es real).
  2. Verificar la presentación: Comprobar la firma en la propia VP usando la clave pública del titular. (Prueba que la persona que la presenta es el propietario).

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.

3. Descripción general de la arquitectura#

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.

  • Frontend (Next.js y React): Un sitio web ligero de cara al usuario. Su trabajo es obtener un objeto de solicitud de nuestro backend, pasarlo a la API navigator.credentials.get() del navegador, recibir el resultado y reenviarlo a nuestro backend para su verificación.
  • Backend (rutas de API de Next.js): El motor del verificador. Genera un objeto de solicitud válido para la API del navegador y expone un endpoint para recibir la presentación de la credencial desde el frontend para la validación final.
  • Navegador (API de credenciales): El facilitador. Recibe el objeto de solicitud de nuestro frontend, entiende el protocolo openid4vp y genera un código QR de forma nativa. Luego espera a que la wallet devuelva una respuesta.
  • CMWallet (aplicación móvil): La wallet del usuario. Escanea el código QR, procesa la solicitud, obtiene el consentimiento del usuario y envía la respuesta firmada de vuelta al navegador.

Aquí hay un diagrama de secuencia que ilustra el flujo completo y preciso:

Explicación del flujo:

  1. Iniciación: El usuario hace clic en el botón "Verificar" en nuestro Frontend.
  2. Objeto de solicitud: El frontend llama a nuestro Backend (/api/verify/start), que genera un objeto de solicitud que contiene la consulta y un nonce, y luego lo devuelve.
  3. Llamada a la API del navegador: El frontend llama a navigator.credentials.get() con el objeto de solicitud.
  4. Código QR nativo: El Navegador ve la solicitud del protocolo 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".

  1. Escanear y presentar: El usuario escanea el código QR con CMWallet. La wallet obtiene la aprobación del usuario y envía la Presentación Verificable de vuelta al navegador.
  2. Resolución de la promesa: El navegador recibe la respuesta, y la promesa original .get() en el frontend finalmente se resuelve, entregando la carga útil de la presentación.
  3. Verificación del backend: El frontend envía la carga útil de la presentación mediante POST al endpoint /api/verify/finish de nuestro backend. El backend valida el nonce y la credencial.
  4. Resultado: El backend devuelve un mensaje final de éxito o fracaso al frontend, que actualiza la interfaz de usuario.

4. Construyendo el verificador#

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

4.1 Configurando el proyecto#

Primero, inicializaremos un nuevo proyecto de Next.js, instalaremos las dependencias necesarias y pondremos en marcha nuestra base de datos.

4.1.1 Inicializando la aplicación Next.js#

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.

4.1.2 Instalando dependencias#

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.

4.1.3 Poniendo en marcha la base de datos#

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.

4.2 Descripción general de la arquitectura de la aplicación Next.js#

Nuestra aplicación Next.js está estructurada para separar las responsabilidades entre el frontend y el backend, aunque forman parte del mismo proyecto.

  • Frontend (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.
  • Rutas de API del backend (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.
  • Biblioteca (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:

4.3 Construyendo el frontend#

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:

  • Interacción del usuario: Proporciona una interfaz simple, como un botón "Verificar", para que el usuario inicie el proceso.
  • Gestión de estado: Gestiona el estado de la UI, mostrando indicadores de carga mientras la verificación está en progreso y mostrando el mensaje final de éxito o error.
  • Comunicación con el backend (Solicitud): Llama a /api/verify/start y recibe una carga útil JSON estructurada (protocol, request, state) que describe exactamente lo que la wallet debe presentar.
  • Invocación de la API del navegador: Pasa ese objeto JSON a navigator.credentials.get(), que renderiza un código QR nativo y espera la respuesta de la wallet.
  • Comunicación con el backend (Respuesta): Una vez que la API del navegador devuelve la presentación verificable, envía estos datos a nuestro endpoint /api/verify/finish en una solicitud POST para la validación final del lado del servidor.
  • Mostrar resultados: Actualiza la UI para informar al usuario si la verificación fue exitosa o falló, basándose en la respuesta del backend.

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.

¿Por qué 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.

4.4 Construyendo los endpoints del backend#

Con la interfaz de usuario de React en su lugar, ahora necesitamos dos rutas de API que hagan el trabajo pesado en el servidor:

  1. /api/verify/start – construye una solicitud OpenID4VP, persiste un desafío de un solo uso en MySQL y devuelve todo al navegador.
  2. /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.

4.4.1 /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

noncedesafí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.

4.4.2 Ayudantes de 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.


4.5 /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.

4.6 Decodificando la credencial 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.

4.6.1 Paso 1: Decodificación de Base64URL y CBOR#

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; }
  • Base64URL a Base64: Convierte la credencial de codificación Base64URL a Base64 estándar.
  • Base64 a binario: Decodifica la cadena Base64 en un array de bytes binario.
  • Decodificación CBOR: Utiliza la biblioteca cbor-web para decodificar los datos binarios en un objeto JavaScript estructurado.

4.6.2 Paso 2: Extrayendo declaraciones con espacios de nombres#

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; }
  • Itera sobre todos los documentos en la credencial decodificada.
  • Decodifica cada espacio de nombres (por ejemplo, eu.europa.ec.eudi.pid.1) para extraer los valores reales de las declaraciones (como nombre, fecha de nacimiento, etc.).
  • Maneja tanto los espacios de nombres firmados por el emisor como los firmados por el dispositivo, si están presentes.

Salida de ejemplo#

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.

4.7 Mostrando el resultado en la UI#

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)!”.

5. Ejecutando el verificador y próximos pasos#

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.

5.1 Cómo ejecutar el ejemplo#

  1. Clona el repositorio:

    git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
  2. Instala las dependencias:

    npm install
  3. 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
  4. 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.

5.2 Próximos pasos: de la demo a la producció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.

5.3 Qué está fuera del alcance de este tutorial#

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:

  • Seguridad lista para producción: El verificador es para fines educativos y carece del endurecimiento requerido para un entorno en vivo.
  • Credenciales verificables del W3C: Este tutorial se centra exclusivamente en el formato ISO mDoc para licencias de conducir móviles. No cubre otros formatos populares como JWT-VCs o VCs con pruebas de datos vinculados (LD-Proofs).
  • Flujos avanzados de OpenID4VP: No implementamos características más complejas de OpenID4VP, como la comunicación directa de la wallet al backend usando una 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.

Conclusión#

¡Eso es todo! Con menos de 250 líneas de TypeScript, ahora tenemos un verificador de extremo a extremo que:

  1. Publica una solicitud para la API de credenciales del navegador.
  2. Permite que cualquier wallet compatible proporcione una presentación verificable.
  3. Valida la presentación en el servidor.
  4. Actualiza la interfaz de usuario en tiempo real.

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.

Recursos#

Aquí tienes algunos de los recursos, especificaciones y herramientas clave utilizados o referenciados en este tutorial:

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start Free Trial

Share this article


LinkedInTwitterFacebook

Table of Contents