---
url: 'https://www.corbado.com/es/blog/como-construir-emisor-credenciales-verificables'
title: 'Cómo construir un emisor de credenciales digitales (Guía para desarrolladores)'
description: 'Aprende a construir un emisor de credenciales verificables W3C utilizando el protocolo OpenID4VCI. Esta guía paso a paso te muestra cómo crear una aplicación Next.js que emite credenciales firmadas criptográficamente y compatibles con billeteras digitales'
lang: 'es'
author: 'Amine'
date: '2025-08-20T15:39:12.347Z'
lastModified: '2026-03-25T10:03:29.637Z'
keywords: 'emisor de credenciales digitales, tutorial emisor, construir emisor'
category: 'Digital Credentials'
---

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

## 1. Introducción

Las [credenciales digitales](https://www.corbado.com/es/blog/digital-credentials-api) son una forma poderosa de
probar la identidad y las afirmaciones de manera segura y preservando la privacidad. Pero,
¿cómo obtienen los usuarios estas credenciales en primer lugar? Aquí es donde el papel del
**Emisor** se vuelve crucial. Un Emisor es una entidad de confianza, como una agencia
[gubernamental](https://www.corbado.com/passkeys-for-public-sector), una universidad o un banco, responsable de
crear y distribuir credenciales firmadas digitalmente a los usuarios.

Esta guía ofrece un [tutorial](https://www.corbado.com/es/blog/aplicacion-crud-react-express-mysql) completo y
paso a paso para construir un Emisor de
[credenciales digitales](https://www.corbado.com/es/blog/digital-credentials-api). Nos centraremos en el
protocolo **OpenID for Verifiable Credential Issuance (OpenID4VCI)**, un estándar moderno
que define cómo los usuarios pueden obtener credenciales de un Emisor y almacenarlas de
forma segura en sus billeteras digitales.

El resultado final será una aplicación funcional de [Next.js](https://www.corbado.com/blog/nextjs-passkeys) que
puede:

1. Aceptar datos del usuario a través de un simple formulario web.
2. Generar una oferta de credencial segura y de un solo uso.
3. Mostrar la oferta como un [código QR](https://www.corbado.com/es/blog/inicio-sesion-codigo-qr-autenticacion)
   para que el usuario la escanee con su
   [billetera](https://www.corbado.com/es/blog/garantia-billetera-digital-marcos-ue-eeuu-australia) móvil.
4. Emitir una credencial firmada criptográficamente que el usuario puede almacenar y
   presentar para su [verificación](https://www.corbado.com/es/blog/verificacion-de-identidad-digital).

### 1.1 Entendiendo la terminología: Credenciales digitales vs. Credenciales verificables

Antes de continuar, es importante aclarar la distinción entre dos conceptos relacionados
pero diferentes:

- **Credenciales digitales (término general):** Es una categoría amplia que engloba
  cualquier forma digital de credenciales, certificados o atestaciones. Pueden incluir
  certificados digitales simples, insignias digitales básicas o cualquier credencial
  almacenada electrónicamente que pueda o no tener características de
  [seguridad](https://www.corbado.com/es/blog/como-eliminar-contrasenas-por-completo) criptográfica.

- **Credenciales verificables (VCs - Estándar W3C):** Es un tipo específico de credencial
  digital que sigue el estándar del Modelo de Datos de Credenciales Verificables del W3C.
  Las credenciales verificables son credenciales firmadas criptográficamente, a prueba de
  manipulaciones y que respetan la privacidad, y que pueden ser verificadas de forma
  independiente. Incluyen requisitos técnicos específicos como:
    - Firmas criptográficas para autenticidad e integridad
    - Modelo de datos y formatos estandarizados
    - Mecanismos de presentación que preservan la privacidad
    - Protocolos de [verificación](https://www.corbado.com/es/blog/verificacion-de-identidad-digital)
      interoperables

**En esta guía, estamos construyendo específicamente un emisor de credenciales
verificables** que sigue el estándar W3C, no solo un sistema de
[credenciales digitales](https://www.corbado.com/es/blog/digital-credentials-api) cualquiera. El protocolo
[OpenID4VCI](https://www.corbado.com/glossary/openid4vci) que estamos usando está diseñado específicamente para
emitir credenciales verificables, y el formato [JWT](https://www.corbado.com/es/glossary/jwks)-VC que
implementaremos es un formato compatible con W3C para credenciales verificables.

### 1.2 Cómo funciona

La magia detrás de las credenciales digitales reside en un modelo simple pero poderoso de
"**triángulo de confianza**" que involucra a tres actores clave:

- **Emisor:** Una autoridad de confianza (por ejemplo, una agencia
  [gubernamental](https://www.corbado.com/passkeys-for-public-sector), universidad o banco) que firma
  criptográficamente y emite una credencial a un usuario. **Este es el rol que estamos
  construyendo en esta guía.**
- **Titular (Holder):** El usuario, que recibe la credencial y la almacena de forma segura
  en una [billetera digital](https://www.corbado.com/es/blog/garantia-billetera-digital-marcos-ue-eeuu-australia)
  personal en su dispositivo.
- **Verificador:** Una aplicación o servicio que necesita comprobar la credencial del
  usuario.

![Ecosistema de Credenciales Verificables W3C](https://www.w3.org/TR/vc-data-model/diagrams/ecosystem.svg)

El flujo de emisión es el primer paso en este ecosistema. El Emisor valida la información
del usuario y le proporciona una credencial. Una vez que el Titular tiene esta credencial
en su [billetera](https://www.corbado.com/es/blog/garantia-billetera-digital-marcos-ue-eeuu-australia), puede
presentarla a un Verificador para probar su identidad o sus afirmaciones, completando así
el triángulo.

Aquí un vistazo rápido a la aplicación final en acción:

**Paso 1: Ingreso de datos del usuario** El usuario completa un formulario con su
información personal para solicitar una nueva credencial.
![Formulario de ingreso de datos del usuario](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_1_0733a9e1da.png)

**Paso 2: Generación de la oferta de credencial** La aplicación genera una oferta de
credencial segura, que se muestra como un
[código QR](https://www.corbado.com/es/blog/inicio-sesion-codigo-qr-autenticacion) y un código preautorizado.
![Código QR de la oferta de credencial](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_2_3f1881c473.png)

**Paso 3: Interacción con la billetera** El usuario escanea el
[código QR](https://www.corbado.com/es/blog/inicio-sesion-codigo-qr-autenticacion) con una
[billetera](https://www.corbado.com/es/blog/garantia-billetera-digital-marcos-ue-eeuu-australia) compatible (por
ejemplo, Sphereon [Wallet](https://www.corbado.com/blog/digital-wallet-assurance)) e introduce un PIN para
autorizar la emisión.
![Oferta de credencial en la billetera](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_3_b80d689dfe.png)
![Inserción del código PIN](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_4_ca8bad8d11.png)

**Paso 4: Credencial emitida** La billetera recibe y almacena la nueva credencial digital
emitida, lista para su uso futuro.
![Confirmando los detalles de la credencial](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_5_55b8150597.png)
![Credencial añadida](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_6_7f5ac5745d.png)

## 2. Prerrequisitos para construir un emisor

Antes de sumergirnos en el código, cubramos los conocimientos y herramientas fundamentales
que necesitaremos. Esta guía asume que tienes una familiaridad básica con los conceptos de
desarrollo web, pero los siguientes prerrequisitos son esenciales para construir un emisor
de credenciales.

### 2.1 Elección de protocolos

Nuestro emisor se basa en un conjunto de estándares abiertos que garantizan la
interoperabilidad entre billeteras y servicios de emisión. Para este
[tutorial](https://www.corbado.com/es/blog/aplicacion-crud-react-express-mysql), nos centraremos en lo siguiente:

| Estándar / Protocolo                                              | Descripción                                                                                                                                                                                                                                                      |
| :---------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **OpenID4VCI**                                                    | **OpenID for Verifiable Credential Issuance.** Este es el protocolo central que usaremos. Define un flujo estándar sobre cómo un usuario (a través de su billetera) puede solicitar y recibir una credencial de un Emisor.                                       |
| **[JWT-VC](https://www.w3.org/TR/vc-data-model/#json-web-token)** | **Credenciales verificables basadas en JWT.** El formato de la credencial que emitiremos. Es un estándar del W3C que codifica las credenciales verificables como JSON Web Tokens (JWT), haciéndolas compactas y amigables para la web.                           |
| **[ISO mDoc](https://www.iso.org/standard/69084.html)**           | **ISO/IEC 18013-5.** El estándar internacional para las licencias de conducir móviles (mDL). Aunque emitimos un JWT-VC, las _claims_ dentro de él están estructuradas para ser compatibles con el modelo de datos mDoc (por ejemplo, `eu.europa.ec.eudi.pid.1`). |
| **OAuth 2.0**                                                     | El marco de autorización subyacente utilizado por OpenID4VCI. Implementaremos un flujo de `pre-authorized_code`, que es un tipo de concesión específico diseñado para una emisión de credenciales segura y fácil de usar.                                        |

#### 2.1.1 Flujos de autorización: Código preautorizado vs. Código de autorización

[OpenID4VCI](https://www.corbado.com/glossary/openid4vci) admite dos flujos de autorización principales para
emitir credenciales:

1. **Flujo de código preautorizado:** En este flujo, el Emisor genera un código de corta
   duración y de un solo uso (`pre-authorized_code`) que está disponible de inmediato para
   el usuario. La billetera del usuario puede luego intercambiar este código directamente
   por una credencial. Este flujo es ideal para escenarios donde el usuario ya está
   autenticado y presente en el sitio web del Emisor, ya que proporciona una experiencia
   de emisión instantánea y fluida sin redirecciones.

2. **Flujo de código de autorización:** Este es el flujo estándar de
   [OAuth 2.0](https://www.corbado.com/glossary/oauth2), donde el usuario es redirigido a un servidor de
   autorización para otorgar su consentimiento. Después de la aprobación, el servidor
   envía un `authorization_code` de vuelta a una `redirect_uri` registrada. Este flujo es
   más adecuado para aplicaciones de terceros que inician el proceso de emisión en nombre
   del usuario.

**Para este tutorial, usaremos el flujo de `pre-authorized_code`.** Elegimos este enfoque
porque es más simple y proporciona una experiencia de usuario más directa para nuestro
caso de uso específico: un usuario que solicita directamente una credencial desde el
propio sitio web del Emisor. Elimina la necesidad de redirecciones complejas y registro de
clientes, lo que facilita la comprensión e implementación de la lógica central de emisión.

Esta combinación de estándares nos permite construir un emisor compatible con una amplia
gama de billeteras digitales y garantiza un proceso seguro y estandarizado para el
usuario.

### 2.2 Elección de la pila tecnológica

Para construir nuestro emisor, utilizaremos la misma pila tecnológica robusta y moderna
que usamos para el verificador, asegurando una experiencia de desarrollador consistente y
de alta calidad.

#### 2.2.1 Lenguaje: TypeScript

Usaremos **TypeScript** tanto para nuestro código de frontend como de backend. Su tipado
estático es invaluable en una aplicación crítica para la
[seguridad](https://www.corbado.com/es/blog/como-eliminar-contrasenas-por-completo) como un emisor, ya que ayuda
a prevenir errores comunes y mejora la calidad y mantenibilidad general del código.

#### 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](https://www.corbado.com/es/blog/aplicacion-crud-react-express-mysql).

- **Para el frontend:** Usaremos [Next.js](https://www.corbado.com/blog/nextjs-passkeys) con
  [React](https://www.corbado.com/blog/react-passkeys) para construir la interfaz de usuario donde los usuarios
  pueden ingresar sus datos para solicitar una credencial.
- **Para el backend:** Aprovecharemos las **rutas de API de Next.js** para crear los
  endpoints del lado del servidor que manejan el flujo de
  [OpenID4VCI](https://www.corbado.com/glossary/openid4vci), desde la generación de ofertas de credenciales hasta
  la emisión de la credencial firmada final.

#### 2.2.3 Librerías clave

Nuestra implementación se basará en algunas librerías clave para manejar tareas
específicas:

- **next**, **react** y **react-dom**: Las librerías principales para nuestra aplicación
  [Next.js](https://www.corbado.com/blog/nextjs-passkeys).
- **mysql2**: Un cliente de [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) para
  [Node.js](https://www.corbado.com/blog/nodejs-passkeys), utilizado para almacenar códigos de autorización y
  datos de sesión.
- **uuid**: Una librería para generar identificadores únicos, que usaremos para crear
  valores de `pre-authorized_code`.
- **jose**: Una librería robusta para manejar JSON Web Signatures (JWS), que utilizaremos
  para firmar criptográficamente las credenciales que emitimos.

### 2.3 Obtén una billetera de prueba

Para probar tu emisor, necesitarás una billetera móvil que admita el protocolo OpenID4VCI.
Para este [tutorial](https://www.corbado.com/es/blog/aplicacion-crud-react-express-mysql), recomendamos la
**Sphereon Wallet**, que está disponible tanto para
[Android](https://www.corbado.com/blog/how-to-enable-passkeys-android) como para
[iOS](https://www.corbado.com/blog/how-to-enable-passkeys-ios).

**Cómo instalar Sphereon Wallet:**

1. **Descarga la billetera** desde la
   [Google Play Store](https://play.google.com/store/apps/details?id=com.sphereon.ssi.wallet)
   o la [Apple App Store](https://apps.apple.com/us/app/sphereon-wallet/id1661096796).
2. Instala la aplicación en tu dispositivo móvil.
3. Una vez instalada, la billetera está lista para recibir ofertas de credenciales
   escaneando un código QR.

### 2.4 Conocimientos de criptografía

Emitir una credencial es una operación crítica para la
[seguridad](https://www.corbado.com/es/blog/como-eliminar-contrasenas-por-completo) que se basa en conceptos
criptográficos fundamentales para garantizar la confianza y la autenticidad.

#### 2.4.1 Firmas digitales

En su núcleo, una credencial verificable es un conjunto de afirmaciones que ha sido
**firmado digitalmente** por el Emisor. Esta firma proporciona dos garantías:

- **Autenticidad:** Prueba que la credencial fue creada por un Emisor legítimo.
- **Integridad:** Prueba que la credencial no ha sido manipulada desde que fue emitida.

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

Las firmas digitales se crean utilizando
[criptografía de clave pública](https://www.corbado.com/es/blog/webauthn-pubkeycredparams-credentialpublickey)/privada.
Así es como funciona:

1. **El Emisor tiene un par de claves:** una **clave privada**, que se mantiene secreta y
   segura, y una **clave pública** correspondiente, que se hace pública.
2. **Firma:** Cuando el Emisor crea una credencial, utiliza su **clave privada** para
   generar una firma digital única para los datos de la credencial.
3. **Verificación:** Un Verificador puede usar más tarde 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 alterada.

En nuestra implementación, generaremos un par de claves de Curva Elíptica (EC) y usaremos
el algoritmo `ES256` para firmar el [JWT](https://www.corbado.com/es/glossary/jwks)-VC. La clave pública se
incrusta en el DID del Emisor (`did:web`), permitiendo que cualquier Verificador la
descubra y valide la firma de la credencial. **Nota:** La claim `aud` (audiencia) se omite
intencionadamente en nuestros JWTs, ya que la credencial está diseñada para ser de
propósito general y no estar vinculada a una billetera específica. Si deseas restringir el
uso a una audiencia particular, incluye una claim `aud` y configúrala en consecuencia.

## 3. Vista general de la arquitectura

Nuestra aplicación de emisor está construida como un proyecto
[full-stack](https://www.corbado.com/es/blog/aplicacion-crud-react-express-mysql) de Next.js, con una clara
separación entre la lógica del frontend y del backend. Esta arquitectura nos permite crear
una experiencia de usuario fluida mientras manejamos todas las operaciones críticas para
la seguridad en el servidor. **Importante:** Las tablas `verification_sessions` y
`verified_credentials` incluidas en el SQL no son necesarias para este emisor, pero se
incluyen para que esté completo.

- **Frontend (`src/app/issue/page.tsx`):** Una única página de
  [React](https://www.corbado.com/blog/react-passkeys) que permite a los usuarios introducir sus datos para
  solicitar una credencial. Realiza llamadas a la API de nuestro backend para iniciar el
  proceso de emisión.
- **Rutas de API del backend (`src/app/api/issue/...`):** Un conjunto de endpoints del
  lado del servidor que implementan el protocolo OpenID4VCI.
    - `/.well-known/openid-credential-issuer`: Un endpoint de metadatos público. Esta es
      la primera URL que una billetera comprobará para descubrir las capacidades del
      emisor, incluyendo su servidor de autorización, endpoint de token, endpoint de
      credencial y los tipos de credenciales que ofrece.
    - `/.well-known/openid-configuration`: Un endpoint de descubrimiento estándar de
      OpenID Connect. Aunque está estrechamente relacionado con el anterior, este endpoint
      sirve para una configuración más amplia relacionada con OIDC y a menudo es necesario
      para la interoperabilidad con clientes OpenID estándar.
    - `/.well-known/did.json`: El documento DID de nuestro emisor. Al usar el método
      `did:web`, este archivo se utiliza para publicar las claves públicas del emisor, que
      los verificadores pueden usar para validar las firmas de las credenciales que emite.
    - `authorize/route.ts`: Crea un `pre-authorized_code` y una oferta de credencial.
    - `token/route.ts`: Intercambia el `pre-authorized_code` por un token de acceso.
    - `credential/route.ts`: Emite el [JWT](https://www.corbado.com/es/glossary/jwks)-VC final, firmado
      criptográficamente.
    - `schemas/pid/route.ts`: Expone el esquema JSON para la credencial PID. Esto permite
      que cualquier consumidor de la credencial entienda su estructura y tipos de datos.
- **Librería (`src/lib/`):**
    - `database.ts`: Gestiona todas las interacciones con la base de datos, como el
      almacenamiento de códigos de autorización y claves del emisor.
    - `crypto.ts`: Maneja todas las operaciones criptográficas, incluyendo la generación
      de claves y la firma de JWT.

Aquí hay un diagrama que ilustra el flujo de emisión:

![Flujo de emisión de credenciales digitales](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Mermaid_Chart_Create_complex_visual_diagrams_with_text_A_smarter_way_of_creating_diagrams_2025_07_29_145228_d28fd13731.svg)

## 4. Construyendo el emisor

Ahora que tenemos una comprensión sólida de los estándares, protocolos y arquitectura,
podemos empezar a construir nuestro emisor.

> **Sigue los pasos o usa el código final**
>
> Ahora repasaremos la configuración y la implementación del código paso a paso. Si
> prefieres ir directamente al producto terminado, puedes clonar el proyecto completo
> desde nuestro repositorio de GitHub y ejecutarlo localmente.
>
> ```bash
> 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.

```bash
npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm
```

Este comando crea un esqueleto de una nueva aplicación Next.js en tu directorio actual.

#### 4.1.2 Instalando dependencias

A continuación, necesitamos instalar las librerías que manejarán los JWTs, las conexiones
a la base de datos y la generación de UUIDs.

```bash
npm install jose mysql2 uuid @types/uuid
```

Este comando instala:

- `jose`: Para firmar y verificar JSON Web Tokens (JWTs).
- `mysql2`: El cliente de [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) para nuestra base
  de datos.
- `uuid`: Para generar cadenas de desafío únicas.
- `@types/uuid`: Tipos de TypeScript para la librería `uuid`.

#### 4.1.3 Poniendo en marcha la base de datos

Nuestro backend requiere una base de datos [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide)
para almacenar códigos de autorización, sesiones de emisión y claves del emisor. Hemos
incluido un archivo `docker-compose.yml` para facilitar esto.

Si has clonado el repositorio, simplemente puedes ejecutar `docker-compose up -d`. Si
estás construyendo desde cero, crea un archivo llamado `docker-compose.yml` con el
siguiente contenido:

```yaml
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 tanto para el verificador como
para el emisor:

```sql
-- Crear la base de datos si no existe
CREATE DATABASE IF NOT EXISTS digital_credentials;
USE digital_credentials;

-- Tabla para almacenar desafíos
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)
);

-- Tabla para almacenar sesiones de verificación
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)
);

-- Tabla para almacenar datos de credenciales verificadas (opcional)
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)
);

-- TABLAS DEL EMISOR

-- Tabla para almacenar códigos de autorización en el flujo OpenID4VCI
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)
);

-- Tabla para almacenar sesiones de emisión
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)
);

-- Tabla para almacenar credenciales emitidas
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, -- mDoc codificado en Base64
    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)
);

-- Tabla para almacenar claves del emisor (simplificada para la 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, -- formato JWK
    private_key TEXT NOT NULL, -- formato JWK (cifrado en producción)
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_key_id (key_id),
    INDEX idx_is_active (is_active)
);
```

Una vez que ambos archivos estén en su lugar, abre tu terminal en la raíz del proyecto y
ejecuta:

```bash
docker-compose up -d
```

Este comando iniciará un contenedor de MySQL en segundo plano, listo para que nuestra
aplicación lo use.

### 4.2 Implementando las librerías compartidas

Antes de construir los endpoints de la API, vamos a crear las librerías compartidas que
manejarán la lógica de negocio principal. Este enfoque mantiene nuestras rutas de API
limpias y centradas en manejar las solicitudes HTTP, mientras que el trabajo complejo se
delega a estos módulos.

#### 4.2.1 La librería de base de datos (`src/lib/database.ts`)

Este archivo es la única fuente de verdad para todas las interacciones con la base de
datos. Utiliza la librería `mysql2` para conectarse a nuestro contenedor de MySQL y
proporciona un conjunto de funciones exportadas para crear, leer y actualizar registros en
nuestras tablas. Esta capa de abstracción hace que nuestro código sea más modular y fácil
de mantener.

Crea el archivo `src/lib/database.ts` con el siguiente contenido:

```typescript
// src/lib/database.ts
import mysql from "mysql2/promise";

// Configuración de la conexión a la base de datos
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;
}

// Funciones de Data-Access-Object (DAO) para cada tabla
// ... (ej., createChallenge, getChallenge, createAuthorizationCode, etc.)
```

> **Nota:** Por brevedad, se ha omitido la lista completa de funciones DAO. Puedes
> encontrar el código completo en el
> [repositorio del proyecto](https://github.com/corbado/digital-credentials-example/blob/main/src/lib/database.ts).
> Este archivo incluye funciones para gestionar desafíos, sesiones de
> [verificación](https://www.corbado.com/es/blog/verificacion-de-identidad-digital), códigos de autorización,
> sesiones de emisión y claves de emisor.

#### 4.2.2 La librería de criptografía (`src/lib/crypto.ts`)

Este archivo maneja todas las operaciones criptográficas críticas para la seguridad.
Utiliza la librería `jose` para generar pares de claves y firmar JSON Web Tokens (JWTs).

**Generación de claves** La función `generateIssuerKeyPair` crea un nuevo par de claves de
Curva Elíptica que se utilizará para firmar credenciales. La clave pública se exporta en
formato JSON Web Key (JWK) para que pueda publicarse en nuestro documento `did.json`.

```typescript
// 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; // Asignar un ID de clave único

    // ... (exportación de la clave privada y otra configuración)

    return { publicKey, privateKey, publicKeyJWK /* ... */ };
}
```

**Creación de credenciales JWT** La función `createJWTVerifiableCredential` es el núcleo
del proceso de emisión. Toma las afirmaciones del usuario, el par de claves del emisor y
otros metadatos, y los utiliza para crear un JWT-VC firmado.

```typescript
// 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 = {
        // El DID del emisor
        iss: issuerKeyPair.issuerDid,
        // El DID del sujeto (titular)
        sub: subjectId,
        // La hora en que se emitió la credencial (iat) y cuándo expira (exp)
        iat: now,
        exp: now + oneYear,
        // El modelo de datos de Credencial Verificable
        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,
            },
        },
    };

    // Firmar el payload con la clave privada del emisor
    return await new SignJWT(vcPayload)
        .setProtectedHeader({
            alg: issuerKeyPair.algorithm,
            kid: issuerKeyPair.keyId,
            typ: "JWT",
        })
        .sign(issuerKeyPair.privateKey);
}
```

Esta función construye el payload del JWT de acuerdo con el Modelo de Datos de
Credenciales Verificables del W3C y lo firma con la clave privada del emisor, produciendo
una credencial verificable segura.

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

Nuestra aplicación Next.js está estructurada para separar responsabilidades entre el
frontend y el backend, aunque forman parte del mismo proyecto. Esto se logra aprovechando
el App Router tanto para las páginas de la interfaz de usuario como para los endpoints de
la API.

- **Frontend (`src/app/issue/page.tsx`):** Un único componente de página de
  [React](https://www.corbado.com/blog/react-passkeys) que define la interfaz de usuario para la ruta `/issue`.
  Maneja la entrada del usuario y se comunica con nuestra API de backend.

- **Rutas de API del backend (`src/app/api/...`):**
    - **Descubrimiento (`.well-known/.../route.ts`):** Estas rutas exponen endpoints de
      metadatos públicos que permiten a las billeteras y otros clientes descubrir las
      capacidades y las claves públicas del emisor.
    - **Emisión (`issue/.../route.ts`):** Estos endpoints implementan la lógica central de
      OpenID4VCI, incluyendo la creación de ofertas de credenciales, la emisión de tokens
      y la firma de la credencial final.
    - **Esquema (`schemas/pid/route.ts`):** Esta ruta sirve el esquema JSON para la
      credencial, definiendo su estructura.

- **Librería (`src/lib/`):** Este directorio contiene lógica reutilizable compartida en
  todo el backend.
    - `database.ts`: Gestiona todas las interacciones con la base de datos, abstrayendo
      las consultas SQL.
    - `crypto.ts`: Maneja todas las operaciones criptográficas, como la generación de
      claves y la firma de JWT.

Esta clara separación hace que la aplicación sea modular y más fácil de mantener.

**Nota:** La función `generateIssuerDid()` debe devolver un `did:web` válido que coincida
con tu dominio de emisor. Cuando se despliegue, el archivo `.well-known/did.json` debe
servirse a través de HTTPS en ese dominio para que los verificadores puedan validar las
credenciales.

![Vista general de la arquitectura de la aplicación Next.js](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/Mermaid_Chart_Create_complex_visual_diagrams_with_text_A_smarter_way_of_creating_diagrams_2025_07_29_151549_6a0aca6477.svg)

### 4.3 Construyendo el frontend

Nuestro frontend es una única página de React que proporciona un formulario simple para
que los usuarios soliciten una nueva credencial digital. Sus responsabilidades son:

- Capturar los datos del usuario (nombre, fecha de nacimiento, etc.).
- Enviar estos datos a nuestro backend para crear una oferta de credencial.
- Mostrar el código QR y el PIN resultantes para que el usuario los escanee con su
  billetera.

La lógica central se maneja en la función `handleSubmit`, que se activa cuando el usuario
envía el formulario.

```typescript
// src/app/issue/page.tsx

const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    setCredentialOffer(null);

    try {
        // 1. Validar campos requeridos
        if (!userData.given_name || !userData.family_name || !userData.birth_date) {
            throw new Error("Por favor, rellena todos los campos requeridos");
        }

        // 2. Solicitar una oferta de credencial al 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 || "Fallo al crear la oferta de credencial",
            );
        }

        // 3. Establecer la oferta de credencial en el estado para mostrar el código QR
        const result = await response.json();
        setCredentialOffer(result);
    } catch (err) {
        const errorMessage = (err as Error).message || "Ocurrió un error desconocido";
        setError(errorMessage);
    } finally {
        setLoading(false);
    }
};
```

Esta función realiza tres acciones clave:

1. **Valida los datos del formulario** para asegurar que todos los campos requeridos estén
   completos.
2. **Envía una solicitud `POST`** a nuestro endpoint `/api/issue/authorize` con los datos
   del usuario.
3. **Actualiza el estado del componente** con la oferta de credencial recibida del
   backend, lo que hace que la interfaz de usuario muestre el código QR y el código de
   transacción.

El resto del archivo contiene código estándar de React para renderizar el formulario y
mostrar el código QR. Puedes ver el archivo completo en el
[repositorio del proyecto](https://github.com/corbado/digital-credentials-example/blob/main/src/app/issue/page.tsx).

### 4.4 Configurando el entorno y el descubrimiento

Antes de construir la API del backend, necesitamos configurar nuestro entorno y establecer
los endpoints de descubrimiento. Estos archivos `.well-known` son cruciales para que las
billeteras encuentren nuestro emisor y entiendan cómo interactuar con él.

#### 4.4.1 Crear el archivo de entorno

Crea un archivo llamado `.env.local` en la raíz de tu proyecto y añade la siguiente línea.
Esta URL debe ser accesible públicamente para que una billetera móvil pueda llegar a ella.
Para el desarrollo local, puedes usar un servicio de túnel como
[ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok) para exponer tu `localhost`.

```
NEXT_PUBLIC_BASE_URL=http://localhost:3000
```

#### 4.4.2 Implementar los endpoints de descubrimiento

Las billeteras descubren las capacidades de un emisor consultando URLs estándar
`.well-known`. Necesitamos crear tres de estos endpoints.

**1. Metadatos del emisor (`/.well-known/openid-credential-issuer`)**

Este es el archivo de descubrimiento principal para OpenID4VCI. Le dice a la billetera
todo lo que necesita saber sobre el emisor, incluyendo sus endpoints, los tipos de
credenciales que ofrece y los algoritmos criptográficos compatibles.

Crea el archivo `src/app/.well-known/openid-credential-issuer/route.ts`:

```typescript
// 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 = {
        // El identificador único del emisor.
        issuer: baseUrl,
        // La URL del servidor de autorización. Para simplificar, nuestro emisor es su propio servidor de autorización.
        authorization_servers: [baseUrl],
        // La URL del emisor de la credencial.
        credential_issuer: baseUrl,
        // El endpoint al que la billetera hará POST para recibir la credencial real.
        credential_endpoint: `${baseUrl}/api/issue/credential`,
        // El endpoint donde la billetera intercambia un código de autorización por un token de acceso.
        token_endpoint: `${baseUrl}/api/issue/token`,
        // El endpoint para el flujo de autorización (no se usa en nuestro flujo preautorizado, pero es una buena práctica incluirlo).
        authorization_endpoint: `${baseUrl}/api/issue/authorize`,
        // Indica soporte para el flujo de código preautorizado sin requerir autenticación del cliente.
        pre_authorized_grant_anonymous_access_supported: true,
        // Información legible por humanos sobre el emisor.
        display: [
            {
                name: "Corbado Credentials Issuer",
                locale: "en-US",
            },
        ],
        // Una lista de los tipos de credenciales que este emisor puede emitir.
        credential_configurations_supported: {
            "eu.europa.ec.eudi.pid.1": {
                // El formato de la credencial (ej., jwt_vc, mso_mdoc).
                format: "jwt_vc",
                // El tipo de documento específico, conforme a los estándares ISO mDoc.
                doctype: "eu.europa.ec.eudi.pid.1",
                // El scope de OAuth 2.0 asociado con este tipo de credencial.
                scope: "eu.europa.ec.eudi.pid.1",
                // Métodos que la billetera puede usar para probar la posesión de su clave.
                cryptographic_binding_methods_supported: ["jwk"],
                // Algoritmos de firma que el emisor soporta para esta credencial.
                credential_signing_alg_values_supported: ["ES256"],
                // Tipos de prueba de posesión que la billetera puede usar.
                proof_types_supported: {
                    jwt: {
                        proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"],
                    },
                },
                // Propiedades de visualización para la credencial.
                display: [
                    {
                        name: "Corbado Credential Issuer",
                        locale: "en-US",
                        logo: {
                            uri: `${baseUrl}/logo.png`,
                            alt_text: "EU Digital Identity",
                        },
                        background_color: "#003399",
                        text_color: "#FFFFFF",
                    },
                ],
                // Una lista de las claims (atributos) en la credencial.
                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étodos de autenticación soportados por el token endpoint. 'none' significa cliente público.
        token_endpoint_auth_methods_supported: ["none"],
        // Métodos de desafío de código PKCE soportados.
        code_challenge_methods_supported: ["S256"],
        // Tipos de concesión de OAuth 2.0 que el emisor soporta.
        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. Configuración de OpenID (`/.well-known/openid-configuration`)**

Este es un documento de descubrimiento estándar de OIDC que proporciona un conjunto más
amplio de detalles de configuración.

Crea el archivo `src/app/.well-known/openid-configuration/route.ts`:

```typescript
// 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 = {
        // El identificador único del emisor.
        credential_issuer: baseUrl,
        // El endpoint al que la billetera hará POST para recibir la credencial real.
        credential_endpoint: `${baseUrl}/api/issue/credential`,
        // El endpoint para el flujo de autorización.
        authorization_endpoint: `${baseUrl}/api/issue/authorize`,
        // El endpoint donde la billetera intercambia un código de autorización por un token de acceso.
        token_endpoint: `${baseUrl}/api/issue/token`,
        // Una lista de los tipos de credenciales que este emisor puede emitir.
        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"],
                    },
                },
            },
        },
        // Tipos de concesión de OAuth 2.0 que el emisor soporta.
        grant_types_supported: [
            "authorization_code",
            "urn:ietf:params:oauth:grant-type:pre-authorized_code",
        ],
        // Indica soporte para el flujo de código preautorizado.
        pre_authorized_grant_anonymous_access_supported: true,
        // Métodos de desafío de código PKCE soportados.
        code_challenge_methods_supported: ["S256"],
        // Métodos de autenticación soportados por el token endpoint.
        token_endpoint_auth_methods_supported: ["none"],
        // Scopes de OAuth 2.0 que el emisor soporta.
        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. Documento DID (`/.well-known/did.json`)**

Este archivo publica la clave pública del emisor utilizando el método `did:web`,
permitiendo a cualquiera verificar la firma de las credenciales emitidas por él.

Crea el archivo `src/app/.well-known/did.json/route.ts`:

```typescript
// 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 se encontró una clave de emisor activa" },
            { status: 404 },
        );
    }

    const publicKeyJWK = JSON.parse(issuerKey.public_key);
    const didId = generateIssuerDid();
    const didDocument = {
        // El contexto define el vocabulario utilizado en el documento.
        "@context": [
            "https://www.w3.org/ns/did/v1",
            "https://w3id.org/security/suites/jws-2020/v1",
        ],
        // La URI del DID, que es el identificador único del emisor.
        id: didId,
        // El controlador del DID, que es la entidad que controla el DID. Aquí, es el propio emisor.
        controller: didId,
        // Una lista de claves públicas que se pueden usar para verificar las firmas del emisor.
        verificationMethod: [
            {
                // Un identificador único para la clave, dentro del ámbito del DID.
                id: `${didId}#${issuerKey.key_id}`,
                // El tipo de la clave.
                type: "JsonWebKey2020",
                // El DID del controlador de la clave.
                controller: didId,
                // La clave pública en formato JWK.
                publicKeyJwk: publicKeyJWK,
            },
        ],
        // Especifica qué claves se pueden usar para la autenticación (probar el control del DID).
        authentication: [`${didId}#${issuerKey.key_id}`],
        // Especifica qué claves se pueden usar para crear credenciales verificables.
        assertionMethod: [`${didId}#${issuerKey.key_id}`],
        // Una lista de servicios proporcionados por el sujeto del DID, como el endpoint del emisor.
        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",
        },
    });
}
```

> **¿Por qué no usar caché?** Notarás que estos tres endpoints devuelven cabeceras que
> evitan agresivamente el almacenamiento en caché (`Cache-Control: no-cache`,
> `Pragma: no-cache`, `Expires: 0`). Esta es una práctica de seguridad crítica para los
> documentos de descubrimiento. Las configuraciones del emisor pueden cambiar; por
> ejemplo, una clave criptográfica podría ser rotada. Si una billetera o un cliente
> almacenara en caché una versión antigua del archivo `did.json` o
> `openid-credential-issuer`, no podría validar nuevas credenciales o interactuar con
> endpoints actualizados. Al forzar a los clientes a obtener una copia nueva en cada
> solicitud, nos aseguramos de que siempre tengan la información más actualizada.

#### 4.4.3 Implementar el endpoint del esquema de la credencial

La última pieza de nuestra [infraestructura](https://www.corbado.com/passkeys-for-critical-infrastructure)
pública es el endpoint del esquema de la credencial. Esta ruta sirve un esquema JSON que
define formalmente la estructura, los tipos de datos y las restricciones de la credencial
PID que estamos emitiendo. Las billeteras y los verificadores pueden usar este esquema
para validar el contenido de la credencial.

Crea el archivo `src/app/api/schemas/pid/route.ts` con el siguiente contenido:

```typescript
// 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", // Reemplaza con tu dominio real
        title: "Credencial PID",
        description:
            "Un esquema para una Credencial Verificable que representa un Documento de Identificación Personal (PID).",
        type: "object",
        properties: {
            credentialSubject: {
                type: "object",
                properties: {
                    given_name: { type: "string" },
                    family_name: { type: "string" },
                    birth_date: { type: "string", format: "date" },
                    // ... otras propiedades del sujeto de la credencial
                },
                required: ["given_name", "family_name", "birth_date"],
            },
            // ... otras propiedades de nivel superior de una Credencial Verificable
        },
    };

    return NextResponse.json(schema, {
        headers: {
            "Content-Type": "application/schema+json",
            "Access-Control-Allow-Origin": "*", // Permitir solicitudes de origen cruzado
        },
    });
}
```

> **Nota:** El esquema JSON para una credencial PID puede ser bastante grande y detallado.
> Por brevedad, el esquema completo ha sido truncado. Puedes encontrar el archivo completo
> en el
> [repositorio del proyecto](https://github.com/corbado/digital-credentials-example/blob/main/src/app/api/schemas/pid/route.ts).

### 4.5 Construyendo los endpoints del backend

Con el frontend en su lugar, ahora necesitamos la lógica del lado del servidor para
manejar el flujo de OpenID4VCI. Empezaremos con el primer endpoint que el frontend llama:
`/api/issue/authorize`.

#### 4.5.1 `/api/issue/authorize`: Crear la oferta de credencial

Este endpoint es responsable de tomar los datos del usuario, generar un código seguro de
un solo uso y construir una `credential_offer` que la billetera del usuario pueda
entender.

Aquí está la lógica central:

```typescript
// 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. Validar los datos del usuario
        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. Generar un código preautorizado y un PIN
        const code = uuidv4();
        const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutos
        const txCode = Math.floor(1000 + Math.random() * 9000).toString(); // PIN de 4 dígitos

        // 3. Almacenar el código y los datos del usuario
        await createAuthorizationCode(uuidv4(), code, expiresAt);
        // Nota: Esto utiliza un almacén en memoria solo para fines de demostración.
        // En producción, persiste los datos de forma segura en una base de datos con una expiración adecuada.
        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. Crear el objeto de oferta de credencial
        const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
        const credentialOffer = {
            // El identificador del emisor, que es su URL base.
            credential_issuer: baseUrl,
            // Un array de tipos de credenciales que el emisor está ofreciendo.
            credential_configuration_ids: ["eu.europa.ec.eudi.pid.1"],
            // Especifica los tipos de concesión que la billetera puede usar.
            grants: {
                // Estamos usando el flujo de código preautorizado.
                "urn:ietf:params:oauth:grant-type:pre-authorized_code": {
                    // El código de un solo uso que la billetera intercambiará por un token.
                    "pre-authorized_code": code,
                    // Indica que el usuario debe introducir un PIN (tx_code) para canjear el código.
                    user_pin_required: true,
                },
            },
        };

        // 5. Crear la URI completa de la oferta de credencial (un deep link para billeteras)
        const credentialOfferUri = `openid-credential-offer://?credential_offer=${encodeURIComponent(
            JSON.stringify(credentialOffer),
        )}`;

        // La respuesta final al frontend.
        return NextResponse.json({
            // El deep link para el código QR.
            credential_offer_uri: credentialOfferUri,
            // El código preautorizado en crudo, para mostrarlo o para entrada manual.
            pre_authorized_code: code,
            // El PIN de 4 dígitos que el usuario debe introducir en su billetera.
            tx_code: txCode,
        });
    } catch (error) {
        console.error("Error de autorización:", error);
        return NextResponse.json({ error: "server_error" }, { status: 500 });
    }
}
```

Pasos clave en este endpoint:

1. **Validar datos:** Primero se asegura de que los datos de usuario requeridos estén
   presentes.
2. **Generar códigos:** Crea un `pre-authorized_code` único (un UUID) y un `tx_code` de 4
   dígitos (PIN) para una capa extra de seguridad.
3. **Persistir datos:** El `pre-authorized_code` se almacena en la base de datos con un
   tiempo de expiración corto. Los datos del usuario y el PIN se almacenan en memoria,
   vinculados al código.
4. **Construir oferta:** Construye el objeto `credential_offer` según la especificación de
   OpenID4VCI. Este objeto le dice a la billetera dónde está el emisor, qué credenciales
   ofrece y el código necesario para obtenerlas.
5. **Devolver URI:** Finalmente, crea una URI de enlace profundo
   (`openid-credential-offer://...`) y la devuelve al frontend, junto con el `tx_code`
   para que el usuario lo vea.

#### 4.5.2 `/api/issue/token`: Intercambiar el código por un token

Una vez que el usuario escanea el código QR e introduce su PIN, la billetera realiza una
solicitud `POST` a este endpoint. Su trabajo es validar el `pre-authorized_code` y el
`user_pin` (PIN), y si son válidos, emitir un token de acceso de corta duración.

Crea el archivo `src/app/api/issue/token/route.ts` con el siguiente contenido:

```typescript
// 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. Validar el tipo de concesión
        if (grant_type !== "urn:ietf:params:oauth:grant-type:pre-authorized_code") {
            return NextResponse.json(
                { error: "unsupported_grant_type" },
                { status: 400 },
            );
        }

        // 2. Validar el código preautorizado
        const authCode = await getAuthorizationCode(code);
        if (!authCode) {
            return NextResponse.json(
                {
                    error: "invalid_grant",
                    error_description: "Código inválido o expirado",
                },
                { status: 400 },
            );
        }

        // 3. Validar el PIN (tx_code)
        const expectedTxCode = (global as any).txCodeStore?.get(code);
        if (expectedTxCode !== user_pin) {
            return NextResponse.json(
                { error: "invalid_grant", error_description: "PIN inválido" },
                { status: 400 },
            );
        }

        // 4. Generar token de acceso y c_nonce
        const accessToken = uuidv4();
        const cNonce = uuidv4();
        const cNonceExpiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutos

        // 5. Crear una nueva sesión de emisión
        const userData = (global as any).userDataStore?.get(code);
        await createIssuanceSession(
            uuidv4(),
            authCode.id,
            accessToken,
            cNonce,
            cNonceExpiresAt,
            userData,
        );

        // 6. Marcar el código como usado y limpiar los datos temporales
        await markAuthorizationCodeAsUsed(code);
        (global as any).txCodeStore?.delete(code);
        (global as any).userDataStore?.delete(code);

        // 7. Devolver la respuesta del token de acceso
        return NextResponse.json({
            access_token: accessToken,
            token_type: "Bearer",
            expires_in: 3600, // 1 hora
            c_nonce: cNonce,
            c_nonce_expires_in: 300, // 5 minutos
        });
    } catch (error) {
        console.error("Error en el endpoint del token:", error);
        return NextResponse.json({ error: "server_error" }, { status: 500 });
    }
}
```

Pasos clave en este endpoint:

1. **Validar tipo de concesión:** Se asegura de que la billetera esté utilizando el tipo
   de concesión `pre-authorized_code` correcto.
2. **Validar código:** Comprueba que el `pre-authorized_code` exista en la base de datos,
   no haya expirado y no se haya utilizado antes.
3. **Validar PIN:** Compara el `user_pin` de la billetera con el `tx_code` que almacenamos
   anteriormente para asegurar que el usuario autorizó la transacción.
4. **Generar tokens:** Crea un `access_token` seguro y un `c_nonce` (nonce de credencial),
   que es un valor de un solo uso para prevenir ataques de repetición en el endpoint de la
   credencial.
5. **Crear sesión:** Crea un nuevo registro `issuance_sessions` en la base de datos,
   vinculando el token de acceso a los datos del usuario.
6. **Marcar código como usado:** Para evitar que la misma oferta se use dos veces, marca
   el `pre-authorized_code` como usado.
7. **Devolver token:** Devuelve el `access_token` y el `c_nonce` a la billetera.

#### 4.5.3 `/api/issue/credential`: Emitir la credencial firmada

Este es el endpoint final y más importante. La billetera utiliza el token de acceso que
recibió del endpoint `/token` para realizar una solicitud `POST` autenticada a esta ruta.
El trabajo de este endpoint es realizar la validación final, crear la credencial firmada
criptográficamente y devolverla a la billetera.

Crea el archivo `src/app/api/issue/credential/route.ts` con el siguiente contenido:

```typescript
// 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. Validar el token 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. Obtener los datos del usuario de la sesión
        const userData = session.user_data;
        if (!userData) {
            return NextResponse.json({ error: "missing_user_data" }, { status: 400 });
        }

        // 3. Obtener la clave de emisor activa
        const issuerKey = await getActiveIssuerKey();
        if (!issuerKey) {
            // En una aplicación real, tendrías un sistema de gestión de claves más robusto.
            // Para esta demostración, podemos generar una clave sobre la marcha si no existe.
            // Esta parte se omite por brevedad, pero está en el repositorio.
            return NextResponse.json(
                {
                    error: "server_error",
                    error_description: "Fallo al obtener la clave del emisor",
                },
                { status: 500 },
            );
        }

        // 4. Crear el 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. Almacenar la credencial emitida en la base de datos
        await createIssuedCredential(/* ... detalles de la credencial ... */);
        await updateIssuanceSession(session.id, "credential_issued");

        // 6. Devolver la credencial firmada
        return NextResponse.json({
            format: "jwt_vc",
            credential: credentialData,
            c_nonce: uuidv4(), // Un nuevo nonce para solicitudes posteriores
            c_nonce_expires_in: 300,
        });
    } catch (error) {
        console.error("Error en el endpoint de la credencial:", error);
        return NextResponse.json({ error: "server_error" }, { status: 500 });
    }
}
```

Pasos clave en este endpoint:

1. **Validar token:** Comprueba si hay un token `Bearer` válido en la cabecera
   `Authorization` y lo utiliza para buscar la sesión de emisión activa.
2. **Recuperar datos de usuario:** Recupera los datos de las afirmaciones del usuario, que
   se almacenaron en la sesión cuando se creó el token.
3. **Cargar clave del emisor:** Carga la clave de firma activa del emisor desde la base de
   datos. En un escenario real, esto sería gestionado por un sistema de gestión de claves
   seguro.
4. **Crear credencial:** Llama a nuestra función de ayuda `createJWTVerifiableCredential`
   de `src/lib/crypto.ts` para construir y firmar el JWT-VC.
5. **Registrar emisión:** Guarda un registro de la credencial emitida en la base de datos
   para fines de auditoría y revocación.
6. **Devolver credencial:** Devuelve la credencial firmada a la billetera en una respuesta
   JSON. La billetera es entonces responsable de almacenarla de forma segura.

## 5. Ejecutando el emisor y próximos pasos

Ahora tienes una implementación completa de extremo a extremo de un emisor de credenciales
digitales. A continuación, te explicamos cómo ejecutarlo localmente y qué necesitas
considerar 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:**

    ```bash
    git clone https://github.com/corbado/digital-credentials-example.git
    cd digital-credentials-example
    ```

2. **Instala las dependencias:**

    ```bash
    npm install
    ```

3. **Inicia la base de datos:** Asegúrate de que Docker esté en ejecución, luego inicia el
   contenedor de MySQL:

    ```bash
    docker-compose up -d
    ```

4. **Configura el entorno y ejecuta el túnel:** Este es el paso más crítico para las
   pruebas locales. Dado que tu billetera móvil necesita conectarse a tu máquina de
   desarrollo a través de Internet, debes exponer tu servidor local con una URL HTTPS
   pública. Usaremos `ngrok` para esto.

    a. **Inicia ngrok:**

    ```bash
    ngrok http 3000
    ```

    b. **Copia la URL HTTPS** de la salida de
    [ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok) (por ejemplo,
    `https://random-string.ngrok.io`). c. **Crea un archivo `.env.local`** y establece la
    URL:

    ```
    NEXT_PUBLIC_BASE_URL=https://<tu-url-de-ngrok>
    ```

5. **Ejecuta la aplicación:**

    ```bash
    npm run dev
    ```

    Abre tu navegador en `http://localhost:3000/issue`. Ahora puedes rellenar el
    formulario, y el código QR generado apuntará correctamente a tu URL pública de
    [ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok), permitiendo que tu billetera
    móvil se conecte y reciba la credencial.

### 5.2 La importancia de HTTPS y `ngrok`

Los protocolos de credenciales digitales se construyen con la seguridad como máxima
prioridad. Por esta razón, las billeteras casi siempre se negarán a conectarse a un emisor
a través de una conexión insegura (`http://`). Todo el proceso se basa en una conexión
**HTTPS** segura, que es habilitada por un **certificado SSL**.

Un servicio de túnel como `ngrok` resuelve ambos problemas creando una URL HTTPS segura y
pública (con un certificado SSL válido) que reenvía todo el tráfico a tu servidor de
desarrollo local. Las billeteras requieren HTTPS y se negarán a conectarse a endpoints
inseguros (`http://`). Esta es una herramienta esencial para probar cualquier servicio web
que necesite interactuar con dispositivos móviles o webhooks externos.

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

Este ejemplo se centra intencionadamente en el flujo de emisión principal para que sea
fácil de entender. Los siguientes temas se consideran fuera de alcance:

- **Seguridad lista para producción:** El emisor es para fines educativos. Un sistema de
  producción requeriría un Sistema de Gestión de Claves (KMS) seguro en lugar de almacenar
  claves en una base de datos, un manejo robusto de errores, limitación de velocidad y un
  registro de auditoría completo.
- **Revocación de credenciales:** Esta guía no implementa un mecanismo para revocar las
  credenciales emitidas. Aunque el esquema incluye una bandera `revoked` para uso futuro,
  no se proporciona lógica de revocación aquí.
- **Flujo de código de autorización:** Nos centramos exclusivamente en el flujo de
  `pre-authorized_code`. Una implementación completa del flujo de `authorization_code`
  requeriría una pantalla de consentimiento del usuario y una lógica de
  [OAuth 2.0](https://www.corbado.com/glossary/oauth2) más compleja.
- **Gestión de usuarios:** La guía no incluye ninguna [autenticación](https://www.corbado.com/es/glossary/jwks) o
  gestión de usuarios para el propio emisor. Se asume que el usuario ya está autenticado y
  autorizado para recibir una credencial.

## 6. Conclusión

¡Eso es todo! Con unas pocas páginas de código, ahora tenemos un emisor de credenciales
digitales completo y de extremo a extremo que:

1. Proporciona un frontend fácil de usar para solicitar credenciales.
2. Implementa el flujo completo de OpenID4VCI `pre-authorized_code`.
3. Expone todos los endpoints de descubrimiento necesarios para la interoperabilidad de
   las billeteras.
4. Genera y firma una credencial verificable JWT segura y conforme a los estándares.

Aunque esta guía proporciona una base sólida, un emisor listo para producción requeriría
características adicionales como una gestión robusta de claves, almacenamiento persistente
en lugar de almacenes en memoria, revocación de credenciales y un endurecimiento de la
seguridad completo. La compatibilidad de las billeteras también varía; se recomienda
Sphereon [Wallet](https://www.corbado.com/blog/digital-wallet-assurance) para las pruebas, pero es posible que
otras billeteras no admitan el flujo preautorizado tal como se implementa aquí. Sin
embargo, los componentes básicos y el flujo de interacción seguirían siendo los mismos.
Siguiendo estos patrones, puedes construir un emisor seguro e interoperable para cualquier
tipo de credencial digital.

## 7. Recursos

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

- **Repositorio del proyecto:**
    - [Código fuente completo en GitHub](https://github.com/corbado/digital-credentials-example)

- **Especificaciones clave:**
    - [OpenID for Verifiable Credential Issuance (OpenID4VCI)](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html):
      El protocolo de emisión principal.
    - [W3C Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/): El
      estándar fundamental para las VCs.
    - [The `did:web` Method](https://w3c-ccg.github.io/did-method-web/): El método DID
      utilizado para la clave pública de nuestro emisor.

- **Herramientas:**
    - [Sphereon Wallet](https://sphereon.com/wallet/): La billetera de prueba utilizada en
      esta guía.
    - ngrok: Para crear un túnel seguro hacia tu entorno de desarrollo local.

- **Librerías:**
    - Next.js: El framework de React para construir el frontend y el backend.
    - [jose](https://github.com/panva/jose): Para crear y firmar JSON Web Tokens (JWTs).
    - [mysql2](https://github.com/sidorares/node-mysql2): El cliente de MySQL para
      [Node.js](https://www.corbado.com/blog/nodejs-passkeys).
