---
url: 'https://www.corbado.com/ja/blog/verifiable-credential-issuer-構築方法'
title: 'デジタルクレデンシャル発行者の構築方法（開発者ガイド）'
description: 'OpenID4VCIプロトコルを使用してW3C検証可能なクレデンシャル発行者を構築する方法を学びましょう。このステップバイステップガイドでは、デジタルウォレットと互換性のある暗号署名付きクレデンシャルを発行するNext.jsアプリケーションの作成方法を解説します。'
lang: 'ja'
author: 'Amine'
date: '2025-08-20T15:39:06.880Z'
lastModified: '2026-03-27T07:03:48.617Z'
keywords: 'デジタルクレデンシャル 発行者, チュートリアル 発行者, 発行者 構築'
category: 'Digital Credentials'
---

# デジタルクレデンシャル発行者の構築方法（開発者ガイド）

## 1. はじめに

デジタルクレデンシャルは、安全でプライバシーを保護する方法で身元や主張を証明するための強力な手段です。しかし、ユーザーはそもそもどのようにしてこれらのクレデンシャルを入手するのでしょうか？ここで重要になるのが**Issuer**の役割です。[Issuer](https://www.corbado.com/glossary/issuer)とは、[政府](https://www.corbado.com/passkeys-for-public-sector)機関、大学、銀行などの信頼できる事業体で、デジタル署名されたクレデンシャルを作成し、ユーザーに配布する責任を担います。

このガイドでは、デジタルクレデンシャル[Issuer](https://www.corbado.com/glossary/issuer)を構築するための包括的なステップバイステップのチュートリアルを提供します。今回は、ユーザーが[Issuer](https://www.corbado.com/glossary/issuer)からクレデンシャルを取得し、自身のデジタルウォレットに安全に保管する方法を定義する最新の標準である\*\*OpenID
for [Verifiable Credential](https://www.corbado.com/glossary/verifiable-credential) Issuance
(OpenID4VCI)\*\*プロトコルに焦点を当てます。

最終的に、以下の機能を持つ[Next.js](https://www.corbado.com/blog/nextjs-passkeys)アプリケーションが完成します。

1. シンプルなWebフォームを通じてユーザーデータを受け入れる。
2. 安全なワンタイムのクレデンシャルオファーを生成する。
3. ユーザーがモバイルウォレットでスキャンできるように、オファーをQRコードとして表示する。
4. ユーザーが保管し、検証のために提示できる暗号署名付きのクレデンシャルを発行する。

### 1.1 用語の理解：デジタルクレデンシャル vs. 検証可能なクレデンシャル

先に進む前に、関連しているものの異なる2つの概念の違いを明確にしておくことが重要です。

- **デジタルクレデンシャル（一般用語）：**
  これは、クレデンシャル、証明書、または構成証明のあらゆるデジタル形式を包括する広範なカテゴリです。これらには、単純なデジタル証明書、基本的なデジタルバッジ、または暗号化によるセキュリティ機能を持つ場合も持たない場合もある電子的に保存されたクレデンシャルが含まれます。

- **検証可能なクレデンシャル（VCs - W3C標準）：**
  これは、[W3C](https://www.corbado.com/ja/blog/digital-credentials-api)
  [Verifiable Credentials](https://www.corbado.com/glossary/microcredentials) Data
  Model標準に準拠した特定のタイプのデジタルクレデンシャルです。検証可能なクレデンシャルは、暗号署名され、改ざんが検知可能で、プライバシーを尊重するクレデンシャルであり、独立して検証できます。次のような特定の技術的要件が含まれます。
    - 真正性と完全性のための暗号署名
    - 標準化されたデータモデルとフォーマット
    - プライバシーを保護する提示メカニズム
    - 相互運用可能な検証プロトコル

\*\*このガイドでは、単なるデジタルクレデンシャルシステムではなく、[W3C](https://www.corbado.com/ja/blog/digital-credentials-api)標準に準拠した検証可能なクレデンシャル発行者を具体的に構築します。\*\*私たちが使用する[OpenID4VCI](https://www.corbado.com/glossary/openid4vci)プロトコルは、検証可能なクレデンシャルの発行専用に設計されており、実装するJWT-VCフォーマットは、検証可能なクレデンシャルのための[W3C](https://www.corbado.com/ja/blog/digital-credentials-api)準拠のフォーマットです。

### 1.2 仕組み

デジタルクレデンシャルの背後にある魔法は、3つの主要なプレイヤーが関わるシンプルかつ強力な「**信頼の三角形**」モデルにあります。

- **Issuer：**
  信頼できる機関（例：[政府](https://www.corbado.com/passkeys-for-public-sector)機関、大学、銀行）で、クレデンシャルに暗号署名してユーザーに発行します。**これがこのガイドで構築する役割です。**
- **Holder：**
  ユーザーであり、クレデンシャルを受け取り、デバイス上の個人のデジタルウォレットに安全に保管します。
- **Verifier：**
  ユーザーのクレデンシャルを確認する必要があるアプリケーションまたはサービスです。

![W3C Verifiable Credentials Ecosystem](https://www.w3.org/TR/vc-data-model/diagrams/ecosystem.svg)

発行フローは、このエコシステムの最初のステップです。Issuerはユーザーの情報を検証し、クレデンシャルを提供します。Holderがこのクレデンシャルをウォレットに持つと、Verifierに提示して身元や主張を証明し、三角形が完成します。

最終的なアプリケーションの動作を簡単に見てみましょう。

**ステップ1：ユーザーデータの入力**
ユーザーは、新しいクレデンシャルをリクエストするために個人情報をフォームに入力します。
![User Data Input Form](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_1_0733a9e1da.png)

**ステップ2：クレデンシャルオファーの生成**
アプリケーションは、安全なクレデンシャルオファーを生成し、QRコードと事前承認コードとして表示します。
![Credential Offer QR Code](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_2_3f1881c473.png)

**ステップ3：ウォレットとの対話** ユーザーは、互換性のあるウォレット（例：Sphereon
[Wallet](https://www.corbado.com/blog/digital-wallet-assurance)）でQRコードをスキャンし、PINを入力して発行を承認します。
![Credential Offer on wallet](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_3_b80d689dfe.png)
![PIN Code insertion](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_4_ca8bad8d11.png)

**ステップ4：クレデンシャルの発行**
ウォレットは、新しく発行されたデジタルクレデンシャルを受け取り、将来の使用に備えて保管します。
![Confirming the credential details](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_5_55b8150597.png)
![Credential added](https://s3.eu-central-1.amazonaws.com/corbado-cloud-staging-website-assets/issuer_step_6_7f5ac5745d.png)

## 2. Issuer構築の前提条件

コードに入る前に、必要となる基礎知識とツールについて説明します。このガイドは、Web開発の基本的な概念に精通していることを前提としていますが、クレデンシャル発行者を構築するためには以下の前提条件が不可欠です。

### 2.1 プロトコルの選択

私たちのIssuerは、ウォレットと発行サービス間の相互運用性を確保するための一連のオープンスタンダードに基づいて構築されています。このチュートリアルでは、以下に焦点を当てます。

| 標準 / プロトコル                                                 | 説明                                                                                                                                                                                                                     |
| :---------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **OpenID4VCI**                                                    | \*\*OpenID for Verifiable Credential Issuance。\*\*これが私たちが使用するコアプロトコルです。ユーザー（ウォレット経由）がIssuerにクレデンシャルを要求し、受け取るための標準的なフローを定義します。                      |
| **[JWT-VC](https://www.w3.org/TR/vc-data-model/#json-web-token)** | \*\*JWTベースの検証可能なクレデンシャル。\*\*私たちが発行するクレデンシャルのフォーマットです。これは検証可能なクレデンシャルをJSON Web Token（JWT）としてエンコードするW3C標準であり、コンパクトでWebフレンドリーです。 |
| **[ISO mDoc](https://www.iso.org/standard/69084.html)**           | \*\*ISO/IEC 18013-5。\*\*モバイル運転免許証（mDL）の国際標準です。私たちはJWT-VCを発行しますが、その中の\_claims\_はmDocデータモデル（例：`eu.europa.ec.eudi.pid.1`）と互換性があるように構成されています。              |
| **OAuth 2.0**                                                     | OpenID4VCIが使用する基盤となる認可フレームワークです。私たちは`pre-authorized_code`フローを実装します。これは、安全でユーザーフレンドリーなクレデンシャル発行のために設計された特定のグラントタイプです。                |

#### 2.1.1 認可フロー：事前承認コード vs. 認可コード

[OpenID4VCI](https://www.corbado.com/glossary/openid4vci)は、クレデンシャルを発行するための2つの主要な認可フローをサポートしています。

1. **事前承認コードフロー（Pre-Authorized Code Flow）：**
   このフローでは、Issuerは短命で一度しか使えないコード（`pre-authorized_code`）を生成し、すぐにユーザーに提供します。ユーザーのウォレットはこのコードを直接クレデンシャルと交換できます。このフローは、ユーザーが既にIssuerのウェブサイトで認証され、その場にいるシナリオに最適です。リダイレクトなしでシームレスかつ即時の発行体験を提供します。

2. **認可コードフロー（Authorization Code Flow）：**
   これは標準的な[OAuth 2.0](https://www.corbado.com/glossary/oauth2)フローで、ユーザーは同意を与えるために認可サーバーにリダイレクトされます。承認後、サーバーは登録された`redirect_uri`に`authorization_code`を送り返します。このフローは、ユーザーに代わって発行プロセスを開始するサードパーティアプリケーションに適しています。

\*\*このチュートリアルでは、`pre-authorized_code`フローを使用します。\*\*このアプローチを選択したのは、よりシンプルで、私たちの特定のユースケース（ユーザーがIssuerのウェブサイトから直接クレデンシャルを要求する）に対してより直接的なユーザー体験を提供するためです。複雑なリダイレクトやクライアント登録の必要がなくなり、コアとなる発行ロジックをより理解しやすく、実装しやすくなります。

この標準の組み合わせにより、私たちは幅広いデジタルウォレットと互換性があり、ユーザーにとって安全で標準化されたプロセスを保証するIssuerを構築することができます。

### 2.2 技術スタックの選択

Issuerを構築するために、Verifierで使用したものと同じ堅牢でモダンな技術スタックを使用し、一貫性のある高品質な開発者体験を保証します。

#### 2.2.1 言語：TypeScript

フロントエンドとバックエンドの両方のコードに**TypeScript**を使用します。その静的型付けは、Issuerのようなセキュリティが重要なアプリケーションにおいて非常に価値があり、一般的なエラーを防ぎ、コード全体の品質と保守性を向上させるのに役立ちます。

#### 2.2.2 フレームワーク：Next.js

**Next.js**は、フルスタックアプリケーションを構築するためのシームレスで統合された体験を提供するため、私たちが選んだフレームワークです。

- **フロントエンド：**
  [Next.js](https://www.corbado.com/blog/nextjs-passkeys)と[React](https://www.corbado.com/blog/react-passkeys)を使用して、ユーザーがクレデンシャルを要求するためにデータを入力するユーザーインターフェースを構築します。
- **バックエンド：** **Next.js API
  Routes**を活用して、クレデンシャルオファーの生成から最終的な署名済みクレデンシャルの発行まで、[OpenID4VCI](https://www.corbado.com/glossary/openid4vci)フローを処理するサーバーサイドのエンドポイントを作成します。

#### 2.2.3 主要ライブラリ

私たちの実装は、特定のタスクを処理するためにいくつかの主要なライブラリに依存します。

- **next**, **react**,
  **react-dom**：[Next.js](https://www.corbado.com/blog/nextjs-passkeys)アプリケーションのコアライブラリ。
- **mysql2**：[Node.js](https://www.corbado.com/blog/nodejs-passkeys)用の[MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide)クライアント。認可コードとセッションデータを保存するために使用します。
- **uuid**：一意の識別子を生成するためのライブラリ。`pre-authorized_code`の値を作成するために使用します。
- **jose**：JSON Web Signature
  (JWS)を扱うための堅牢なライブラリ。私たちが発行するクレデンシャルに暗号署名するために使用します。

### 2.3 テスト用ウォレットの入手

Issuerをテストするには、OpenID4VCIプロトコルをサポートするモバイルウォレットが必要です。このチュートリアルでは、[Android](https://www.corbado.com/blog/how-to-enable-passkeys-android)と[iOS](https://www.corbado.com/blog/how-to-enable-passkeys-ios)の両方で利用可能な**Sphereon
Wallet**をお勧めします。

**Sphereon Walletのインストール方法：**

1. [Google Playストア](https://play.google.com/store/apps/details?id=com.sphereon.ssi.wallet)または[Apple App Store](https://apps.apple.com/us/app/sphereon-wallet/id1661096796)から**ウォレットをダウンロード**します。
2. モバイルデバイスにアプリをインストールします。
3. インストールが完了すると、ウォレットはQRコードをスキャンしてクレデンシャルオファーを受け取る準備ができます。

### 2.4 暗号技術の知識

クレデンシャルの発行は、信頼性と真正性を保証するために基本的な暗号概念に依存する、セキュリティ上重要な操作です。

#### 2.4.1 デジタル署名

検証可能なクレデンシャルの核心は、Issuerによって**デジタル署名**された一連のクレーム（主張）です。この署名は2つの保証を提供します。

- **真正性：** クレデンシャルが正当なIssuerによって作成されたことを証明します。
- **完全性：** クレデンシャルが発行されてから改ざんされていないことを証明します。

#### 2.4.2 公開鍵/秘密鍵暗号

デジタル署名は、公開鍵/秘密鍵暗号を使用して作成されます。仕組みは次のとおりです。

1. **Issuerは鍵ペアを持っています：**秘密に保管される**秘密鍵**と、公開される対応する**公開鍵**です。
2. **署名：**
   Issuerはクレデンシャルを作成する際に、自身の**秘密鍵**を使用して、クレデンシャルデータに対する一意のデジタル署名を生成します。
3. **検証：**
   Verifierは後でIssuerの**公開鍵**を使用して署名を確認できます。チェックが通れば、Verifierはそのクレデンシャルが本物であり、改ざんされていないことを知ります。

私たちの実装では、楕円曲線（EC）鍵ペアを生成し、`ES256`アルゴリズムを使用してJWT-VCに署名します。公開鍵はIssuerのDID（`did:web`）に埋め込まれており、どのVerifierでもそれを発見してクレデンシャルの署名を検証できます。
**注：**
`aud`（audience）クレームは、クレデンシャルが汎用目的であり、特定のウォレットに縛られないように設計されているため、私たちのJWTでは意図的に省略されています。使用を特定のオーディエンスに制限したい場合は、`aud`クレームを含めて適切に設定してください。

## 3. アーキテクチャの概要

私たちのIssuerアプリケーションは、フロントエンドとバックエンドのロジックが明確に分離されたフルスタックのNext.jsプロジェクトとして構築されています。このアーキテクチャにより、サーバー上ですべてのセキュリティ上重要な操作を処理しながら、シームレスなユーザー体験を作り出すことができます。
**重要：**
SQLに含まれる`verification_sessions`および`verified_credentials`テーブルは、このIssuerには不要ですが、完全性のために含まれています。

- **フロントエンド (`src/app/issue/page.tsx`):**
  ユーザーがクレデンシャルを要求するためにデータを入力できる単一の[React](https://www.corbado.com/blog/react-passkeys)ページ。発行プロセスを開始するためにバックエンドAPIを呼び出します。
- **バックエンド API Routes (`src/app/api/issue/...`):**
  OpenID4VCIプロトコルを実装するサーバーサイドのエンドポイント群。
    - `/.well-known/openid-credential-issuer`: 公開メタデータエンドポイント。これはウォレットが最初にチェックするURLで、Issuerの機能（認可サーバー、トークンエンドポイント、クレデンシャルエンドポイント、提供するクレデンシャルの種類など）を発見します。
    - `/.well-known/openid-configuration`: 標準的なOpenID
      Connectディスカバリーエンドポイント。上記と密接に関連していますが、このエンドポイントはより広範なOIDC関連の設定を提供し、標準的なOpenIDクライアントとの相互運用性のためにしばしば必要とされます。
    - `/.well-known/did.json`: 私たちのIssuerのDIDドキュメント。`did:web`メソッドを使用する場合、このファイルはIssuerの公開鍵を公開するために使用され、Verifierはこれを使って発行されたクレデンシャルの署名を検証できます。
    - `authorize/route.ts`: `pre-authorized_code`とクレデンシャルオファーを作成します。
    - `token/route.ts`: `pre-authorized_code`をアクセストークンと交換します。
    - `credential/route.ts`: 最終的な暗号署名付きのJWT-VCを発行します。
    - `schemas/pid/route.ts`:
      PIDクレデンシャルのJSONスキーマを公開します。これにより、クレデンシャルの利用者はその構造とデータ型を理解できます。
- **ライブラリ (`src/lib/`):**
    - `database.ts`: 認可コードやIssuerキーの保存など、すべてのデータベース操作を管理します。
    - `crypto.ts`: 鍵生成やJWT署名など、すべての暗号操作を処理します。

以下は、発行フローを示す図です。

![Digital Credential Issuance Flow](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. Issuerの構築

標準、プロトコル、アーキテクチャについてしっかりと理解できたので、Issuerの構築を始めましょう。

> **一緒に進めるか、完成版コードを使用するか**
>
> これからセットアップとコードの実装をステップバイステップで進めていきます。すぐに完成品に進みたい場合は、GitHubリポジトリから完全なプロジェクトをクローンしてローカルで実行できます。
>
> ```bash
> git clone https://github.com/corbado/digital-credentials-example.git
> ```

### 4.1 プロジェクトのセットアップ

まず、新しいNext.jsプロジェクトを初期化し、必要な依存関係をインストールし、データベースを起動します。

#### 4.1.1 Next.jsアプリの初期化

ターミナルを開き、プロジェクトを作成したいディレクトリに移動して、次のコマンドを実行します。このプロジェクトではApp
Router、TypeScript、Tailwind CSSを使用します。

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

このコマンドは、現在のディレクトリに新しいNext.jsアプリケーションを構築します。

#### 4.1.2 依存関係のインストール

次に、JWT、データベース接続、UUID生成を処理するライブラリをインストールする必要があります。

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

このコマンドでインストールされるもの：

- `jose`: JSON Web Token (JWT) の署名と検証のため。
- `mysql2`: データベース用の[MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide)クライアント。
- `uuid`: 一意のチャレンジ文字列を生成するため。
- `@types/uuid`: `uuid`ライブラリのTypeScript型定義。

#### 4.1.3 データベースの起動

バックエンドは、認可コード、発行セッション、Issuerキーを保存するために[MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide)データベースを必要とします。これを簡単にするために`docker-compose.yml`ファイルを含めました。

リポジトリをクローンした場合は、単に`docker-compose up -d`を実行します。ゼロから構築している場合は、`docker-compose.yml`という名前のファイルを作成し、以下の内容を記述してください。

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

このDocker
Composeセットアップには、SQL初期化スクリプトも必要です。`sql`という名前のディレクトリを作成し、その中に`init.sql`という名前のファイルを作成して、VerifierとIssuerの両方に必要なテーブルをセットアップするための以下の内容を記述します。

```sql
-- Create database if not exists
CREATE DATABASE IF NOT EXISTS digital_credentials;
USE digital_credentials;

-- Table for storing challenges
CREATE TABLE IF NOT EXISTS challenges (
    id VARCHAR(36) PRIMARY KEY,
    challenge VARCHAR(255) NOT NULL UNIQUE,
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    used BOOLEAN DEFAULT FALSE,
    INDEX idx_challenge (challenge),
    INDEX idx_expires_at (expires_at)
);

-- Table for storing verification sessions
CREATE TABLE IF NOT EXISTS verification_sessions (
    id VARCHAR(36) PRIMARY KEY,
    challenge_id VARCHAR(36),
    status ENUM('pending', 'verified', 'failed', 'expired') DEFAULT 'pending',
    presentation_data JSON,
    verified_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE,
    INDEX idx_challenge_id (challenge_id),
    INDEX idx_status (status)
);

-- Table for storing verified credentials data (optional)
CREATE TABLE IF NOT EXISTS verified_credentials (
    id VARCHAR(36) PRIMARY KEY,
    session_id VARCHAR(36),
    credential_type VARCHAR(255),
    issuer VARCHAR(255),
    subject VARCHAR(255),
    claims JSON,
    verified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (session_id) REFERENCES verification_sessions(id) ON DELETE CASCADE,
    INDEX idx_session_id (session_id),
    INDEX idx_credential_type (credential_type)
);

-- ISSUER TABLES

-- Table for storing authorization codes in OpenID4VCI flow
CREATE TABLE IF NOT EXISTS authorization_codes (
    id VARCHAR(36) PRIMARY KEY,
    code VARCHAR(255) NOT NULL UNIQUE,
    client_id VARCHAR(255),
    scope VARCHAR(255),
    code_challenge VARCHAR(255),
    code_challenge_method VARCHAR(50),
    redirect_uri TEXT,
    user_pin VARCHAR(10),
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    used BOOLEAN DEFAULT FALSE,
    INDEX idx_code (code),
    INDEX idx_expires_at (expires_at)
);

-- Table for storing issuance sessions
CREATE TABLE IF NOT EXISTS issuance_sessions (
    id VARCHAR(36) PRIMARY KEY,
    authorization_code_id VARCHAR(36),
    access_token VARCHAR(255),
    token_type VARCHAR(50) DEFAULT 'Bearer',
    expires_in INT DEFAULT 3600,
    c_nonce VARCHAR(255),
    c_nonce_expires_at TIMESTAMP,
    status ENUM('pending', 'authorized', 'credential_issued', 'expired', 'failed') DEFAULT 'pending',
    user_data JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (authorization_code_id) REFERENCES authorization_codes(id) ON DELETE CASCADE,
    INDEX idx_access_token (access_token),
    INDEX idx_c_nonce (c_nonce),
    INDEX idx_status (status)
);

-- Table for storing issued credentials
CREATE TABLE IF NOT EXISTS issued_credentials (
    id VARCHAR(36) PRIMARY KEY,
    session_id VARCHAR(36),
    credential_id VARCHAR(255),
    credential_type VARCHAR(255) DEFAULT 'jwt_vc',
    doctype VARCHAR(255) DEFAULT 'eu.europa.ec.eudi.pid.1',
    credential_data LONGTEXT, -- Base64 encoded mDoc
    credential_claims JSON,
    issuer_did VARCHAR(255),
    subject_id VARCHAR(255),
    issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP,
    revoked BOOLEAN DEFAULT FALSE,
    revoked_at TIMESTAMP NULL,
    FOREIGN KEY (session_id) REFERENCES issuance_sessions(id) ON DELETE CASCADE,
    INDEX idx_credential_id (credential_id),
    INDEX idx_session_id (session_id),
    INDEX idx_doctype (doctype),
    INDEX idx_subject_id (subject_id),
    INDEX idx_issued_at (issued_at)
);

-- Table for storing issuer keys (simplified for demo)
CREATE TABLE IF NOT EXISTS issuer_keys (
    id VARCHAR(36) PRIMARY KEY,
    key_id VARCHAR(255) NOT NULL UNIQUE,
    key_type VARCHAR(50) NOT NULL, -- 'EC', 'RSA'
    algorithm VARCHAR(50) NOT NULL, -- 'ES256', 'RS256', etc.
    public_key TEXT NOT NULL, -- JWK format
    private_key TEXT NOT NULL, -- JWK format (encrypted in production)
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_key_id (key_id),
    INDEX idx_is_active (is_active)
);
```

両方のファイルが配置されたら、プロジェクトのルートでターミナルを開き、実行します。

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

このコマンドは、バックグラウンドでMySQLコンテナを起動し、アプリケーションが使用できる状態にします。

### 4.2 共通ライブラリの実装

APIエンドポイントを構築する前に、コアなビジネスロジックを処理する共有ライブラリを作成しましょう。このアプローチにより、APIルートはHTTPリクエストの処理に集中し、クリーンな状態を保つことができ、複雑な作業はこれらのモジュールに委任されます。

#### 4.2.1 データベースライブラリ (`src/lib/database.ts`)

このファイルは、すべてのデータベース操作の唯一の情報源です。`mysql2`ライブラリを使用してMySQLコンテナに接続し、テーブルのレコードを作成、読み取り、更新するための一連のエクスポートされた関数を提供します。この抽象化レイヤーにより、コードはよりモジュール化され、保守が容易になります。

`src/lib/database.ts`ファイルを作成し、以下の内容を記述します。

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

// Database connection configuration
const dbConfig = {
    host: process.env.DATABASE_HOST || "localhost",
    port: parseInt(process.env.DATABASE_PORT || "3306"),
    user: process.env.DATABASE_USER || "app_user",
    password: process.env.DATABASE_PASSWORD || "app_password",
    database: process.env.DATABASE_NAME || "digital_credentials",
    timezone: "+00:00",
};

let connection: mysql.Connection | null = null;

export async function getConnection(): Promise<mysql.Connection> {
    if (!connection) {
        connection = await mysql.createConnection(dbConfig);
    }
    return connection;
}

// Data-Access-Object (DAO) functions for each table
// ... (e.g., createChallenge, getChallenge, createAuthorizationCode, etc.)
```

> **注：**
> 簡潔にするため、DAO関数の完全なリストは省略されています。完全なコードは[プロジェクトリポジトリ](https://github.com/corbado/digital-credentials-example/blob/main/src/lib/database.ts)で確認できます。このファイルには、チャレンジ、検証セッション、認可コード、発行セッション、Issuerキーを管理するための関数が含まれています。

#### 4.2.2 暗号ライブラリ (`src/lib/crypto.ts`)

このファイルは、セキュリティ上重要なすべての暗号操作を処理します。`jose`ライブラリを使用して、鍵ペアを生成し、JSON
Web Token (JWT) に署名します。

**鍵生成**
`generateIssuerKeyPair`関数は、クレデンシャルに署名するために使用される新しい楕円曲線鍵ペアを作成します。公開鍵はJSON
Web Key
(JWK) フォーマットでエクスポートされ、`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; // Assign a unique key ID

    // ... (private key export and other setup)

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

**JWTクレデンシャルの作成**
`createJWTVerifiableCredential`関数は、発行プロセスの中心です。ユーザーのクレーム、Issuerの鍵ペア、その他のメタデータを受け取り、それらを使用して署名付きのJWT-VCを作成します。

```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 = {
        // The issuer's DID
        iss: issuerKeyPair.issuerDid,
        // The subject's (holder's) DID
        sub: subjectId,
        // The time the credential was issued (iat) and when it expires (exp)
        iat: now,
        exp: now + oneYear,
        // The Verifiable Credential data model
        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,
            },
        },
    };

    // Sign the payload with the issuer's private key
    return await new SignJWT(vcPayload)
        .setProtectedHeader({
            alg: issuerKeyPair.algorithm,
            kid: issuerKeyPair.keyId,
            typ: "JWT",
        })
        .sign(issuerKeyPair.privateKey);
}
```

この関数は、W3C検証可能クレデンシャルデータモデルに従ってJWTペイロードを構築し、Issuerの秘密鍵で署名して、安全で検証可能なクレデンシャルを生成します。

### 4.2 Next.jsアプリのアーキテクチャ概要

私たちのNext.jsアプリケーションは、同じプロジェクトの一部でありながら、フロントエンドとバックエンドの関心事を分離するように構成されています。これは、UIページとAPIエンドポイントの両方にApp
Routerを活用することで実現されます。

- **フロントエンド (`src/app/issue/page.tsx`):**
  `/issue`ルートのUIを定義する単一の[React](https://www.corbado.com/blog/react-passkeys)ページコンポーネント。ユーザー入力を処理し、バックエンドAPIと通信します。

- **バックエンド API Routes (`src/app/api/...`):**
    - **ディスカバリー (`.well-known/.../route.ts`):**
      これらのルートは、ウォレットや他のクライアントがIssuerの機能や公開鍵を発見できるようにする公開メタデータエンドポイントを公開します。
    - **発行 (`issue/.../route.ts`):**
      これらのエンドポイントは、クレデンシャルオファーの作成、トークンの発行、最終的なクレデンシャルの署名など、コアとなるOpenID4VCIロジックを実装します。
    - **スキーマ (`schemas/pid/route.ts`):**
      このルートは、クレデンシャルのJSONスキーマを提供し、その構造を定義します。

- **ライブラリ (`src/lib/`):**
  このディレクトリには、バックエンド全体で共有される再利用可能なロジックが含まれています。
    - `database.ts`: SQLクエリを抽象化し、すべてのデータベース操作を管理します。
    - `crypto.ts`: 鍵生成やJWT署名など、すべての暗号操作を処理します。

この明確な分離により、アプリケーションはモジュール化され、保守が容易になります。

**注：**
`generateIssuerDid()`関数は、Issuerのドメインに一致する有効な`did:web`を返す必要があります。デプロイ時には、Verifierがクレデンシャルを検証できるように、`.well-known/did.json`はそのドメインでHTTPS経由で提供される必要があります。

![Architectural overview of Next.js App](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 フロントエンドの構築

フロントエンドは、ユーザーが新しいデジタルクレデンシャルを要求するためのシンプルなフォームを提供する単一のReactページです。その責務は以下の通りです。

- ユーザーデータ（名前、生年月日など）をキャプチャする。
- このデータをバックエンドに送信してクレデンシャルオファーを作成する。
- 結果として得られたQRコードとPINをユーザーがウォレットでスキャンできるように表示する。

コアロジックは、ユーザーがフォームを送信したときにトリガーされる`handleSubmit`関数で処理されます。

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

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

    try {
        // 1. Validate required fields
        if (!userData.given_name || !userData.family_name || !userData.birth_date) {
            throw new Error("Please fill in all required fields");
        }

        // 2. Request a credential offer from the 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 || "Failed to create credential offer",
            );
        }

        // 3. Set the credential offer in state to display the QR code
        const result = await response.json();
        setCredentialOffer(result);
    } catch (err) {
        const errorMessage = (err as Error).message || "Unknown error occurred";
        setError(errorMessage);
    } finally {
        setLoading(false);
    }
};
```

この関数は3つの主要なアクションを実行します。

1. **フォームデータの検証**：すべての必須フィールドが入力されていることを確認します。
2. **`POST`リクエストの送信**：ユーザーデータとともに`/api/issue/authorize`エンドポイントにリクエストを送信します。
3. **コンポーネントの状態を更新**：バックエンドから受け取ったクレデンシャルオファーで状態を更新し、UIがQRコードとトランザクションコードを表示するようにトリガーします。

ファイルの残りの部分は、フォームとQRコード表示をレンダリングするための標準的なReactコードです。完全なファイルは[プロジェクトリポジトリ](https://github.com/corbado/digital-credentials-example/blob/main/src/app/issue/page.tsx)で確認できます。

### 4.4 環境とディスカバリーの設定

バックエンドAPIを構築する前に、環境を設定し、ディスカバリーエンドポイントをセットアップする必要があります。これらの`.well-known`ファイルは、ウォレットが私たちのIssuerを見つけ、対話方法を理解するために不可欠です。

#### 4.4.1 環境ファイルの作成

プロジェクトのルートに`.env.local`という名前のファイルを作成し、次の行を追加します。このURLは、モバイルウォレットがアクセスできるように公開されている必要があります。ローカル開発では、[ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok)のようなトンネリングサービスを使用して`localhost`を公開できます。

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

#### 4.4.2 ディスカバリーエンドポイントの実装

ウォレットは、標準の`.well-known`URLをクエリすることで、Issuerの機能を発見します。これらのエンドポイントを3つ作成する必要があります。

**1. Issuerメタデータ (`/.well-known/openid-credential-issuer`)**

これはOpenID4VCIの主要なディスカバリーファイルです。Issuerのエンドポイント、提供するクレデンシャルの種類、サポートされている暗号アルゴリズムなど、ウォレットが必要とするすべての情報を伝えます。

`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 = {
        // The issuer's unique identifier.
        issuer: baseUrl,
        // The URL of the authorization server. For simplicity, our issuer is its own authorization server.
        authorization_servers: [baseUrl],
        // The URL of the credential issuer.
        credential_issuer: baseUrl,
        // The endpoint where the wallet will POST to receive the actual credential.
        credential_endpoint: `${baseUrl}/api/issue/credential`,
        // The endpoint where the wallet exchanges an authorization code for an access token.
        token_endpoint: `${baseUrl}/api/issue/token`,
        // The endpoint for the authorization flow (not used in our pre-authorized flow, but good practice to include).
        authorization_endpoint: `${baseUrl}/api/issue/authorize`,
        // Indicates support for the pre-authorized code flow without requiring client authentication.
        pre_authorized_grant_anonymous_access_supported: true,
        // Human-readable information about the issuer.
        display: [
            {
                name: "Corbado Credentials Issuer",
                locale: "en-US",
            },
        ],
        // A list of the credential types this issuer can issue.
        credential_configurations_supported: {
            "eu.europa.ec.eudi.pid.1": {
                // The format of the credential (e.g., jwt_vc, mso_mdoc).
                format: "jwt_vc",
                // The specific document type, conforming to ISO mDoc standards.
                doctype: "eu.europa.ec.eudi.pid.1",
                // The OAuth 2.0 scope associated with this credential type.
                scope: "eu.europa.ec.eudi.pid.1",
                // Methods the wallet can use to prove possession of its key.
                cryptographic_binding_methods_supported: ["jwk"],
                // Signing algorithms the issuer supports for this credential.
                credential_signing_alg_values_supported: ["ES256"],
                // Proof-of-possession types the wallet can use.
                proof_types_supported: {
                    jwt: {
                        proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"],
                    },
                },
                // Display properties for the credential.
                display: [
                    {
                        name: "Corbado Credential Issuer",
                        locale: "en-US",
                        logo: {
                            uri: `${baseUrl}/logo.png`,
                            alt_text: "EU Digital Identity",
                        },
                        background_color: "#003399",
                        text_color: "#FFFFFF",
                    },
                ],
                // A list of the claims (attributes) in the credential.
                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" }],
                        },
                    },
                },
            },
        },
        // Authentication methods supported by the token endpoint. 'none' means public client.
        token_endpoint_auth_methods_supported: ["none"],
        // PKCE code challenge methods supported.
        code_challenge_methods_supported: ["S256"],
        // OAuth 2.0 grant types the issuer supports.
        grant_types_supported: [
            "authorization_code",
            "urn:ietf:params:oauth:grant-type:pre-authorized_code",
        ],
    };

    return NextResponse.json(issuerMetadata, {
        headers: {
            "Content-Type": "application/json",
            "Cache-Control": "no-cache, no-store, must-revalidate",
            Pragma: "no-cache",
            Expires: "0",
        },
    });
}
```

**2. OpenID設定 (`/.well-known/openid-configuration`)**

これは、より広範な設定詳細を提供する標準的なOIDCディスカバリー文書です。

`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 = {
        // The issuer's unique identifier.
        credential_issuer: baseUrl,
        // The endpoint where the wallet will POST to receive the actual credential.
        credential_endpoint: `${baseUrl}/api/issue/credential`,
        // The endpoint for the authorization flow.
        authorization_endpoint: `${baseUrl}/api/issue/authorize`,
        // The endpoint where the wallet exchanges an authorization code for an access token.
        token_endpoint: `${baseUrl}/api/issue/token`,
        // A list of the credential types this issuer can issue.
        credential_configurations_supported: {
            "eu.europa.ec.eudi.pid.1": {
                format: "jwt_vc",
                scope: "eu.europa.ec.eudi.pid.1",
                cryptographic_binding_methods_supported: ["jwk"],
                credential_signing_alg_values_supported: ["ES256", "ES384", "ES512"],
                proof_types_supported: {
                    jwt: {
                        proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"],
                    },
                },
            },
        },
        // OAuth 2.0 grant types the issuer supports.
        grant_types_supported: [
            "authorization_code",
            "urn:ietf:params:oauth:grant-type:pre-authorized_code",
        ],
        // Indicates support for the pre-authorized code flow.
        pre_authorized_grant_anonymous_access_supported: true,
        // PKCE code challenge methods supported.
        code_challenge_methods_supported: ["S256"],
        // Authentication methods supported by the token endpoint.
        token_endpoint_auth_methods_supported: ["none"],
        // OAuth 2.0 scopes the issuer supports.
        scopes_supported: ["eu.europa.ec.eudi.pid.1"],
    };

    return NextResponse.json(openidConfiguration, {
        headers: {
            "Content-Type": "application/json",
            "Cache-Control": "no-cache, no-store, must-revalidate",
            Pragma: "no-cache",
            Expires: "0",
        },
    });
}
```

**3. DIDドキュメント (`/.well-known/did.json`)**

このファイルは、`did:web`メソッドを使用してIssuerの公開鍵を公開し、誰でもそれによって発行されたクレデンシャルの署名を検証できるようにします。

`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 active issuer key found" },
            { status: 404 },
        );
    }

    const publicKeyJWK = JSON.parse(issuerKey.public_key);
    const didId = generateIssuerDid();
    const didDocument = {
        // The context defines the vocabulary used in the document.
        "@context": [
            "https://www.w3.org/ns/did/v1",
            "https://w3id.org/security/suites/jws-2020/v1",
        ],
        // The DID URI, which is the unique identifier for the issuer.
        id: didId,
        // The DID controller, which is the entity that controls the DID. Here, it's the issuer itself.
        controller: didId,
        // A list of public keys that can be used to verify signatures from the issuer.
        verificationMethod: [
            {
                // A unique identifier for the key, scoped to the DID.
                id: `${didId}#${issuerKey.key_id}`,
                // The type of the key.
                type: "JsonWebKey2020",
                // The DID of the key's controller.
                controller: didId,
                // The public key in JWK format.
                publicKeyJwk: publicKeyJWK,
            },
        ],
        // Specifies which keys can be used for authentication (proving control of the DID).
        authentication: [`${didId}#${issuerKey.key_id}`],
        // Specifies which keys can be used for creating verifiable credentials.
        assertionMethod: [`${didId}#${issuerKey.key_id}`],
        // A list of services provided by the DID subject, such as the issuer endpoint.
        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",
        },
    });
}
```

> **なぜキャッシュしないのか？**
> これら3つのエンドポイントがすべて、積極的にキャッシュを防ぐヘッダー（`Cache-Control: no-cache`、`Pragma: no-cache`、`Expires: 0`）を返していることにお気づきでしょう。これはディスカバリー文書にとって重要なセキュリティプラクティスです。Issuerの設定は変更される可能性があります。例えば、暗号鍵がローテーションされるかもしれません。ウォレットやクライアントが古いバージョンの`did.json`や`openid-credential-issuer`ファイルをキャッシュしてしまうと、新しいクレデンシャルの検証や更新されたエンドポイントとの対話に失敗してしまいます。クライアントに各リクエストで新しいコピーを取得させることで、常に最新の情報を持っていることを保証します。

#### 4.4.3 クレデンシャルスキーマエンドポイントの実装

公開[インフラ](https://www.corbado.com/passkeys-for-critical-infrastructure)の最後のピースは、クレデンシャルスキーマエンドポイントです。このルートは、私たちが発行しているPIDクレデンシャルの構造、データ型、制約を正式に定義するJSONスキーマを提供します。ウォレットやVerifierは、このスキーマを使用してクレデンシャルの内容を検証できます。

`src/app/api/schemas/pid/route.ts`ファイルに以下の内容を記述して作成します。

```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", // Replace with your actual domain
        title: "PID Credential",
        description:
            "A schema for a Verifiable Credential representing a Personal Identification Document (PID).",
        type: "object",
        properties: {
            credentialSubject: {
                type: "object",
                properties: {
                    given_name: { type: "string" },
                    family_name: { type: "string" },
                    birth_date: { type: "string", format: "date" },
                    // ... other properties of the credential subject
                },
                required: ["given_name", "family_name", "birth_date"],
            },
            // ... other top-level properties of a Verifiable Credential
        },
    };

    return NextResponse.json(schema, {
        headers: {
            "Content-Type": "application/schema+json",
            "Access-Control-Allow-Origin": "*", // Allow cross-origin requests
        },
    });
}
```

> **注：**
> PIDクレデンシャルのJSONスキーマは非常に大きく詳細になることがあります。簡潔にするため、完全なスキーマは省略されています。完全なファイルは[プロジェクトリポジトリ](https://github.com/corbado/digital-credentials-example/blob/main/src/app/api/schemas/pid/route.ts)で確認できます。

### 4.5 バックエンドエンドポイントの構築

フロントエンドができたので、次にOpenID4VCIフローを処理するサーバーサイドのロジックが必要です。まず、フロントエンドが最初に呼び出すエンドポイントである`/api/issue/authorize`から始めましょう。

#### 4.5.1 `/api/issue/authorize`: クレデンシャルオファーの作成

このエンドポイントは、ユーザーのデータを受け取り、安全なワンタイムユースコードを生成し、ユーザーのウォレットが理解できる`credential_offer`を構築する責任があります。

これがコアロジックです。

```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. Validate user data
        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. Generate a pre-authorized code and a PIN
        const code = uuidv4();
        const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
        const txCode = Math.floor(1000 + Math.random() * 9000).toString(); // 4-digit PIN

        // 3. Store the code and user data
        await createAuthorizationCode(uuidv4(), code, expiresAt);
        // Note: This uses an in-memory store for demo purposes only.
        // In production, persist data securely in a database with proper expiry.
        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. Create the credential offer object
        const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
        const credentialOffer = {
            // The issuer's identifier, which is its base URL.
            credential_issuer: baseUrl,
            // An array of credential types the issuer is offering.
            credential_configuration_ids: ["eu.europa.ec.eudi.pid.1"],
            // Specifies the grant types the wallet can use.
            grants: {
                // We are using the pre-authorized code flow.
                "urn:ietf:params:oauth:grant-type:pre-authorized_code": {
                    // The one-time code the wallet will exchange for a token.
                    "pre-authorized_code": code,
                    // Indicates that the user must enter a PIN (tx_code) to redeem the code.
                    user_pin_required: true,
                },
            },
        };

        // 5. Create the full credential offer URI (a deep link for wallets)
        const credentialOfferUri = `openid-credential-offer://?credential_offer=${encodeURIComponent(
            JSON.stringify(credentialOffer),
        )}`;

        // The final response to the frontend.
        return NextResponse.json({
            // The deep link for the QR code.
            credential_offer_uri: credentialOfferUri,
            // The raw pre-authorized code, for display or manual entry.
            pre_authorized_code: code,
            // The 4-digit PIN the user must enter in their wallet.
            tx_code: txCode,
        });
    } catch (error) {
        console.error("Authorization error:", error);
        return NextResponse.json({ error: "server_error" }, { status: 500 });
    }
}
```

このエンドポイントの主要なステップ：

1. **データの検証：** 最初に必要なユーザーデータが存在することを確認します。
2. **コードの生成：**
   一意の`pre-authorized_code`（UUID）と、追加のセキュリティ層のための4桁の`tx_code`（PIN）を作成します。
3. **データの永続化：**
   `pre-authorized_code`は短い有効期限付きでデータベースに保存されます。ユーザーデータとPINは、コードにリンクされてメモリ内に保存されます。
4. **オファーの構築：**
   OpenID4VCI仕様に従って`credential_offer`オブジェクトを構築します。このオブジェクトは、ウォレットにIssuerの場所、提供するクレデンシャル、そしてそれらを取得するために必要なコードを伝えます。
5. **URIの返却：**
   最後に、ディープリンクURI（`openid-credential-offer://...`）を作成し、ユーザーが見るための`tx_code`とともにフロントエンドに返します。

#### 4.5.2 `/api/issue/token`: コードをトークンに交換

ユーザーがQRコードをスキャンしてPINを入力すると、ウォレットはこのエンドポイントに`POST`リクエストを送信します。その仕事は、`pre-authorized_code`と`user_pin`（PIN）を検証し、それらが有効であれば、短命のアクセストークンを発行することです。

`src/app/api/issue/token/route.ts`ファイルに以下の内容を記述して作成します。

```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. Validate the grant type
        if (grant_type !== "urn:ietf:params:oauth:grant-type:pre-authorized_code") {
            return NextResponse.json(
                { error: "unsupported_grant_type" },
                { status: 400 },
            );
        }

        // 2. Validate the pre-authorized code
        const authCode = await getAuthorizationCode(code);
        if (!authCode) {
            return NextResponse.json(
                {
                    error: "invalid_grant",
                    error_description: "Invalid or expired code",
                },
                { status: 400 },
            );
        }

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

        // 4. Generate access token and c_nonce
        const accessToken = uuidv4();
        const cNonce = uuidv4();
        const cNonceExpiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes

        // 5. Create a new issuance session
        const userData = (global as any).userDataStore?.get(code);
        await createIssuanceSession(
            uuidv4(),
            authCode.id,
            accessToken,
            cNonce,
            cNonceExpiresAt,
            userData,
        );

        // 6. Mark the code as used and clean up temporary data
        await markAuthorizationCodeAsUsed(code);
        (global as any).txCodeStore?.delete(code);
        (global as any).userDataStore?.delete(code);

        // 7. Return the access token response
        return NextResponse.json({
            access_token: accessToken,
            token_type: "Bearer",
            expires_in: 3600, // 1 hour
            c_nonce: cNonce,
            c_nonce_expires_in: 300, // 5 minutes
        });
    } catch (error) {
        console.error("Token endpoint error:", error);
        return NextResponse.json({ error: "server_error" }, { status: 500 });
    }
}
```

このエンドポイントの主要なステップ：

1. **グラントタイプの検証：**
   ウォレットが正しい`pre-authorized_code`グラントタイプを使用していることを確認します。
2. **コードの検証：**
   `pre-authorized_code`がデータベースに存在し、有効期限が切れておらず、まだ使用されていないことを確認します。
3. **PINの検証：**
   ウォレットからの`user_pin`を、以前に保存した`tx_code`と比較して、ユーザーがトランザクションを承認したことを確認します。
4. **トークンの生成：**
   安全な`access_token`と、クレデンシャルエンドポイントへのリプレイ攻撃を防ぐためのワンタイム値である`c_nonce`（クレデンシャルノンス）を作成します。
5. **セッションの作成：**
   データベースに新しい`issuance_sessions`レコードを作成し、アクセストークンをユーザーのデータにリンクします。
6. **コードを使用済みにマーク：**
   同じオファーが二度使用されるのを防ぐために、`pre-authorized_code`を使用済みとしてマークします。
7. **トークンの返却：** `access_token`と`c_nonce`をウォレットに返します。

#### 4.5.3 `/api/issue/credential`: 署名済みクレデンシャルの発行

これが最後の、そして最も重要なエンドポイントです。ウォレットは`/token`エンドポイントから受け取ったアクセストークンを使用して、このルートに認証済みの`POST`リクエストを送信します。このエンドポイントの仕事は、最終的な検証を行い、暗号署名されたクレデンシャルを作成し、それをウォレットに返すことです。

`src/app/api/issue/credential/route.ts`ファイルに以下の内容を記述して作成します。

```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. Validate the Bearer token
        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. Get the user data from the session
        const userData = session.user_data;
        if (!userData) {
            return NextResponse.json({ error: "missing_user_data" }, { status: 400 });
        }

        // 3. Get the active issuer key
        const issuerKey = await getActiveIssuerKey();
        if (!issuerKey) {
            // In a real application, you would have a more robust key management system.
            // For this demo, we can generate a key on the fly if one doesn't exist.
            // This part is omitted for brevity but is in the repository.
            return NextResponse.json(
                {
                    error: "server_error",
                    error_description: "Failed to get issuer key",
                },
                { status: 500 },
            );
        }

        // 4. Create the 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. Store the issued credential in the database
        await createIssuedCredential(/* ... credential details ... */);
        await updateIssuanceSession(session.id, "credential_issued");

        // 6. Return the signed credential
        return NextResponse.json({
            format: "jwt_vc",
            credential: credentialData,
            c_nonce: uuidv4(), // A new nonce for subsequent requests
            c_nonce_expires_in: 300,
        });
    } catch (error) {
        console.error("Credential endpoint error:", error);
        return NextResponse.json({ error: "server_error" }, { status: 500 });
    }
}
```

このエンドポイントの主要なステップ：

1. **トークンの検証：**
   `Authorization`ヘッダーに有効な`Bearer`トークンがあるかを確認し、それを使用してアクティブな発行セッションを検索します。
2. **ユーザーデータの取得：**
   トークンが作成されたときにセッションに保存されたユーザーのクレームデータを取得します。
3. **Issuerキーの読み込み：**
   Issuerのアクティブな署名キーをデータベースから読み込みます。実際のシナリオでは、これは安全な鍵管理システムによって管理されます。
4. **クレデンシャルの作成：**
   `src/lib/crypto.ts`の`createJWTVerifiableCredential`ヘルパーを呼び出して、JWT-VCを構築し、署名します。
5. **発行のログ記録：**
   監査および失効目的のために、発行されたクレデンシャルの記録をデータベースに保存します。
6. **クレデンシャルの返却：**
   署名されたクレデンシャルをJSONレスポンスでウォレットに返します。その後、ウォレットはそれを安全に保存する責任を負います。

## 5. Issuerの実行と次のステップ

これで、デジタルクレデンシャルIssuerの完全なエンドツーエンドの実装が完成しました。これをローカルで実行する方法と、概念実証から本番対応のアプリケーションに移行するために考慮すべき点について説明します。

### 5.1 サンプルの実行方法

1. **リポジトリのクローン：**

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

2. **依存関係のインストール：**

    ```bash
    npm install
    ```

3. **データベースの起動：**
   Dockerが実行されていることを確認し、MySQLコンテナを起動します。

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

4. **環境設定とトンネルの実行：**
   これはローカルテストで最も重要なステップです。モバイルウォレットはインターネット経由で開発マシンに接続する必要があるため、ローカルサーバーを公開HTTPS
   URLで公開する必要があります。ここでは`ngrok`を使用します。

    a. **ngrokの起動：**

    ```bash
    ngrok http 3000
    ```

    b. [ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok)の出力から**HTTPS
    URLをコピー**します（例：`https://random-string.ngrok.io`）。c.
    **`.env.local`ファイルを作成**し、URLを設定します。

    ```
    NEXT_PUBLIC_BASE_URL=https://<your-ngrok-url>
    ```

5. **アプリケーションの実行：**

    ```bash
    npm run dev
    ```

    ブラウザで`http://localhost:3000/issue`を開きます。フォームに入力すると、生成されたQRコードは公開[ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok)
    URLを正しく指し示し、モバイルウォレットが接続してクレデンシャルを受け取ることができます。

### 5.2 HTTPSと`ngrok`の重要性

デジタルクレデンシャルプロトコルは、セキュリティを最優先に構築されています。このため、ウォレットはほとんどの場合、安全でない（`http://`）接続を介してIssuerに接続することを拒否します。プロセス全体は、**SSL証明書**によって有効化される安全な**HTTPS**接続に依存しています。

`ngrok`のようなトンネルサービスは、すべてのトラフィックをローカル開発サーバーに転送する、公開された安全なHTTPS
URL（有効なSSL証明書付き）を作成することで、両方の問題を解決します。ウォレットはHTTPSを要求し、安全でない（`http://`）エンドポイントへの接続を拒否します。これは、モバイルデバイスや外部のwebhookと対話する必要があるWebサービスをテストするための不可欠なツールです。

### 5.3 このチュートリアルの対象外

このサンプルは、理解を容易にするために、意図的にコアな発行フローに焦点を当てています。以下のトピックは対象外と見なされます。

- **本番レベルのセキュリティ：**
  このIssuerは教育目的です。本番システムでは、データベースにキーを保存する代わりに安全な鍵管理システム（KMS）、堅牢なエラーハンドリング、レート制限、包括的な監査ログが必要です。
- **クレデンシャルの失効：**
  このガイドでは、発行されたクレデンシャルを失効させるメカニズムは実装していません。スキーマには将来の使用のために`revoked`フラグが含まれていますが、失効ロジックは提供されていません。
- **認可コードフロー：**
  `pre-authorized_code`フローにのみ焦点を当てました。`authorization_code`フローの完全な実装には、ユーザーの同意画面とより複雑なOAuth
  2.0ロジックが必要です。
- **ユーザー管理：**
  このガイドには、Issuer自体のユーザー認証や管理は含まれていません。ユーザーは既に認証され、クレデンシャルを受け取る権限があると想定されています。

## 6. まとめ

以上です！数ページのコードで、以下の機能を持つ完全なエンドツーエンドのデジタルクレデンシャルIssuerが完成しました。

1. クレデンシャルを要求するためのユーザーフレンドリーなフロントエンドを提供する。
2. 完全なOpenID4VCI `pre-authorized_code`フローを実装する。
3. ウォレットの相互運用性のために必要なすべてのディスカバリーエンドポイントを公開する。
4. 安全で標準に準拠したJWT-検証可能なクレデンシャルを生成し、署名する。

このガイドは堅固な基盤を提供しますが、本番対応のIssuerには、堅牢な鍵管理、インメモリストアの代わりに永続ストレージ、クレデンシャルの失効、包括的なセキュリティ強化などの追加機能が必要です。また、ウォレットの互換性も様々です。テストにはSphereon
[Wallet](https://www.corbado.com/blog/digital-wallet-assurance)が推奨されますが、他のウォレットではここで実装されている事前承認フローがサポートされていない場合があります。しかし、コアとなる構成要素とインタラクションフローは同じです。これらのパターンに従うことで、あらゆる種類のデジタルクレデンシャルに対して安全で相互運用可能なIssuerを構築できます。

## 7. リソース

このチュートリアルで使用または参照された主要なリソース、仕様、ツールを以下に示します。

- **プロジェクトリポジトリ：**
    - [GitHubの完全なソースコード](https://github.com/corbado/digital-credentials-example)

- **主要な仕様：**
    - [OpenID for Verifiable Credential Issuance (OpenID4VCI)](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html): コアな発行プロトコル。
    - [W3C Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/):
      VCsの基礎となる標準。
    - [The `did:web` Method](https://w3c-ccg.github.io/did-method-web/): 私たちのIssuerの公開鍵に使用されるDIDメソッド。

- **ツール：**
    - [Sphereon Wallet](https://sphereon.com/wallet/): このガイドで使用したテストウォレット。
    - ngrok: ローカル開発環境への安全なトンネルを作成するため。

- **ライブラリ：**
    - Next.js: フロントエンドとバックエンドを構築するためのReactフレームワーク。
    - [jose](https://github.com/panva/jose): JSON Web Token
      (JWT) を作成および署名するため。
    - [mysql2](https://github.com/sidorares/node-mysql2):
      [Node.js](https://www.corbado.com/blog/nodejs-passkeys)用のMySQLクライアント。
