メインコンテンツまでスキップ

Issuer機能のセットアップと使用方法

このガイドでは、VCKnotsのIssuer機能のセットアップと使用方法について説明します。

1. 前提条件

  • OpenID for Verifiable Credential Issuance - draft 13 に対応(OpenID for Verifiable Credential Issuance - draft 13)
    なお、以下は現時点では未実装ですが、今後対応予定です。
    • 現在対応しているフローは 事前認可コードフロー(Pre-Authorized Code Flow)のみです
    • Credential Offerのtx_codeは未対応(今後対応予定)
    • Credential Requestのcredential_response_encryptionは未対応(今後対応予定)
  • Node.js v14以降がインストールされていること
  • TypeScriptが設定されていること
  • 本ドキュメントはserverのサンプル実装に基づいて説明します
  • HonoのWebフレームワークを使用していますが、他のフレームワークでも利用可能です

2. 初期設定

必要な依存関係のインストール

npm install @trustknots/vcknots
npm install hono @hono/node-server

ライブラリを使うための準備

import { Hono } from 'hono'
import { initializeContext } from '@trustknots/vcknots'
import { initializeIssuerFlow, CredentialIssuer, CredentialIssuerMetadata } from '@trustknots/vcknots/issuer'
import { initializeAuthzFlow, AuthorizationServerIssuer, AuthorizationServerMetadata, AuthzTokenRequest } from '@trustknots/vcknots/authz'

const app = new Hono();

// VcknotsContextを作成
const context = initializeContext({
debug: process.env.NODE_ENV !== "production",
});

// IssuerFlowとAuthzFlowインスタンスを作成
const issuerFlow = initializeIssuerFlow(context);
const authzFlow = initializeAuthzFlow(context);

3. Issuer機能のサンプル実装

パラメータ

:issuer パラメータ

Issuerのエンドポイントで使用される:issuerパラメータは、Issuerの識別子を表します。

形式: CredentialIssuer型のURI文字列

:

// HTTPS URI形式
const issuerId = "https://issuer.example.com"

用途:

  • Issuerのメタデータの管理
  • クレデンシャルオファーの作成
  • クレデンシャルの発行
  • 認可サーバーの管理

注意事項:

  • URL形式である必要がある(z.string().url()でバリデーション)
  • HTTPSスキームを使用することを推奨
  • 特殊文字を含む場合は適切にエンコードする

1. デフォルトメタデータの初期化

サーバー起動時にデフォルトのIssuer, 認可サーバーのメタデータを初期化する例:

import issuerMetadataConfigRaw from '../samples/issuer_metadata.json' with { type: 'json' }
import authorizationMetadataConfigRaw from '../samples/authorization_metadata.json' with {
type: 'json',
}

const issuerMetadataConfig = CredentialIssuerMetadata(issuerMetadataConfigRaw)
const authorizationMetadataConfig = AuthorizationServerMetadata(authorizationMetadataConfigRaw)

serve({ fetch: app.fetch, port: Number.parseInt(process.env.PORT ?? '8080') }, async (info) => {
console.log(`Server is running on http://localhost:${info.port}`)

// 初期化実行(デフォルト設定を使用)
const issuerMetadata = CredentialIssuerMetadata({
...issuerMetadataConfig,
credential_issuer: CredentialIssuer(baseUrl),
authorization_servers: [baseUrl],
credential_endpoint: `${baseUrl}/issue/credentials`,
batch_credential_endpoint: `${baseUrl}/batch_credential`,
deferred_credential_endpoint: `${baseUrl}/deferred_credential`,
})

await initializeIssuerMetadata(issuerMetadata);

authorizationMetadataConfig.issuer = AuthorizationServerIssuer(baseUrl);
authorizationMetadataConfig.authorization_endpoint = `${baseUrl}/issue/authorize`;
authorizationMetadataConfig.token_endpoint = `${baseUrl}/issue/token`;
await initializeAuthzMetadata(authorizationMetadataConfig)
})

async function initializeIssuerMetadata(issuerMetadata: CredentialIssuerMetadata) {
try {
await issuerFlow.createIssuerMetadata(issuerMetadata)
return true
} catch (error) {
console.error('Error initializing issuer metadata:', error)
return false
}
}


async function initializeAuthzMetadata(authzMetadata: AuthorizationServerMetadata) {
try {
await authzFlow.createAuthzServerMetadata(authzMetadata)
return true
} catch (error) {
console.error('Error initializing authz metadata:', error)
return false
}
}

2. Issuerメタデータの取得

Issuerのメタデータを取得するエンドポイント:

app.get('.well-known/openid-credential-issuer', async (c) => {
try {
const issuer = CredentialIssuer(baseUrl)
const metadata = await issuerFlow.findIssuerMetadata(issuer)

if (!metadata) {
return c.notFound()
}

return c.json(metadata)
} catch (err) {
return c.json(handleError(err), 400)
}
})

:

リクエスト

curl http://localhost:8080/.well-known/openid-credential-issuer

レスポンス

{
"credential_issuer": "http://localhost:8080",
"authorization_servers": [
"http://localhost:8080"
],
"credential_endpoint": "http://localhost:8080/issue/credentials",
"batch_credential_endpoint": "http://localhost:8080/issue/batch_credential",
"deferred_credential_endpoint": "http://localhost:8080/issue/deferred_credential",
"credential_configurations_supported": {
"UniversityDegreeCredential": {
"format": "jwt_vc_json",
"scope": "UniversityDegree",
"cryptographic_binding_methods_supported": [
"did:example"
],
"credential_definition": {
"type": [
"VerifiableCredential",
"UniversityDegreeCredential"
],
"credentialSubject": {
"given_name": {
"mandatory": true,
"value_type": "string",
"display": [
{
"name": "Given Name",
"locale": "en-US"
}
]
},
"family_name": {
"display": [
{
"name": "Surname",
"locale": "en-US"
}
]
},
"degree": {},
"gpa": {
"display": [
{
"name": "GPA"
}
]
}
}
},
"proof_types_supported": {
"jwt": {
"proof_signing_alg_values_supported": [
"ES256"
]
}
},
"credential_signing_alg_values_supported": [
"ES256"
],
"display": [
{
"name": "University Credential",
"locale": "en-US",
"logo": {
"uri": "https://university.example.edu/public/logo.png",
"alt_text": "a square logo of a university"
},
"background_color": "#12107c",
"text_color": "#FFFFFF"
}
]
}
},
"display": [
{
"name": "Example University",
"locale": "en-US"
},
{
"name": "Example Université",
"locale": "fr-FR"
}
]
}

3. クレデンシャルオファーの作成

クレデンシャルオファーを作成するエンドポイント:

app.post('issue/configurations/:configuration/offer', async (c) => {
try {
const issuer = CredentialIssuer(baseUrl)
const configurations = [CredentialConfigurationId(c.req.param('configuration'))]

const offer = await issuerFlow.offerCredential(issuer, configurations, {
usePreAuth: true,
})
console.log('offer:', offer)

return c.text(
`openid-credential-offer://?credential_offer=${encodeURIComponent(JSON.stringify(offer))}`
)
} catch (err) {
const errorResponse = handleError(err)
return c.json(errorResponse, 400)
}
})

:

リクエスト

curl -X POST http://localhost:8080/issue/configurations/UniversityDegreeCredential/offer

レスポンス

openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22http%3A%2F%2Flocalhost%3A8080%22%2C%22credential_configuration_ids%22%3A%5B%22UniversityDegreeCredential%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22343ce17f1d274aa8bb3d19c140484889%22%7D%7D%7D

4. 認可サーバーメタデータの取得

認可サーバーのメタデータを取得するエンドポイント:

app.get("/.well-known/oauth-authorization-server", async (c) => {
try {
const authz = AuthorizationServerIssuer(baseUrl)
const metadata = await authzFlow.findAuthzServerMetadata(authz)

if (!metadata) {
return c.notFound()
}

return c.json(metadata)
} catch (err) {
return c.json(handleError(err), 400)
}
})

:

リクエスト

curl  http://localhost:8080/.well-known/oauth-authorization-server

レスポンス

{
"pre-authorized_grant_anonymous_access_supported": true,
"issuer": "http://localhost:8080",
"authorization_endpoint": "http://localhost:8080/authz/authorize",
"token_endpoint": "http://localhost:8080/authz/token",
"scopes_supported": [
"openid"
],
"response_types_supported": [
"code"
]
}

5. アクセストークンの発行

アクセストークンを発行するエンドポイント:

app.post("authz/token", async (c) => {
const request = await c.req.formData();
const tokenRequest = AuthzTokenRequest(Object.fromEntries(request.entries()));
console.log("tokenRequest:", tokenRequest);
const issuer = AuthorizationServerIssuer(issuerId);

const accessToken = await authzFlow.createAccessToken(issuer, tokenRequest);
return c.json(accessToken);
});


:

リクエスト

curl -X POST http://localhost:8080/authz/token \
-H "Content-Type: application/json" \
-d ' {
"grant_type": "urn:ietf:params:oauth:grant-type:pre-authorized_code",
"pre-authorized_code": "343ce17f1d274aa8bb3d19c140484889"
}'

レスポンス

{
"access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJzdWIiOiIzNDNjZTE3ZjFkMjc0YWE4YmIzZDE5YzE0MDQ4NDg4OSIsImV4cCI6MTc2MTk3NjE1NiwiaWF0IjoxNzYxODg5NzU2fQ.vsV71EEtAo36jcb9N8un2cn36Oo_H1qtKuIp0uerdvI2jNcBhN7ltGeqmk1AVZhpk5kQZcfbkSiHje-j1Iv1zg",
"token_type": "bearer",
"expires_in": 86400,
"c_nonce": "3ccc7973abef4102ad70a871e200304b",
"c_nonce_expires_in": 300000
}

6. クレデンシャルの発行

クレデンシャルを発行するエンドポイント:

app.post('issue/credentials', async (c) => {
try {
const issuer = AuthorizationServerIssuer(baseUrl)

const request = await c.req.json()
const parsedReq = CredentialRequest(request)

// AccessToken 検証
const accessToken = c.req.header('Authorization')?.replace('Bearer ', '')
if (!accessToken) {
return c.json(
{
error: 'invalid_token',
error_description: 'Access token is required.',
},
401
)
}
const isValid = await authzFlow.verifyAccessToken(issuer, accessToken)
console.log('isValid:', isValid)
if (!isValid) {
return c.json(
{
error: 'invalid_token',
error_description: 'Access token is invalid.',
},
401
)
}
// Credential 発行
const credential = await issuerFlow.issueCredential(CredentialIssuer(baseUrl), parse, {
alg: 'ES256',
cnonce: {
c_nonce_expires_in: 60 * 5 * 1000,
},
claims: {
given_name: 'Test',
family_name: 'Smith',
degree: '5',
gpa: 'test',
}
,
})

return c.json(credential)
} catch (err) {
const errorResponse = handleError(err)
return c.json(errorResponse, 400)
}
})

:

リクエスト

curl -X POST http://localhost:8080/issue/credentials \
-H "Authorization: eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJzdWIiOiJmZGMzMzIzYmM3MTg0ZmJkYWE0NTc2YTgwODU2OGE0MSIsImV4cCI6MTc2MTk3ODAwNSwiaWF0IjoxNzYxODkxNjA1fQ.PBKg31GJbIIKqtQL6gpZYoIM_PGlY681u4Rjjhxek38Kzl3prEBggXcqjUq3l-cBRYC1KS1fcJY6jUiUllwyJw" \
-H "Content-Type: application/json" \
--data '{
"format": "jwt_vc_json",
"credential_definition": {
"type": ["VerifiableCredential", "UniversityDegreeCredential"]
},
"proof": {
"proof_type": "jwt",
"jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ekRuYWVZaXdITmVNWWFqMjFXbzlqUENvd3RuQnJZOGhlOFVDSzhaWk4xbWhoeDhQTSJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiYXVkIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20ifQ.zgj0A19Zo9EMMYtvGJtIehcq6eSmr_VEmiCMz-1ZM0yepvh8pqaSBdU83jXWr7Mgy2BRzVuGQL3WcY55GljjlQ"
}
}'

レスポンス

{
"credential": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6IjM4YzEwMWQ2LTEwZDktNGU0Mi05MDlkLWY1N2Y0OWIyMTZjNiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJVbml2ZXJzaXR5RGVncmVlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJpc3N1YW5jZURhdGUiOiIyMDI1LTEwLTMxVDA3OjAzOjA4LjUzN1oiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ekRuYWVZaXdITmVNWWFqMjFXbzlqUENvd3RuQnJZOGhlOFVDSzhaWk4xbWhoeDhQTSIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ0YXJvIiwiZGVncmVlIjoiNSIsImdwYSI6InRlc3QifX0sImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsInN1YiI6ImRpZDprZXk6ekRuYWVZaXdITmVNWWFqMjFXbzlqUENvd3RuQnJZOGhlOFVDSzhaWk4xbWhoeDhQTSJ9.LwcUtOS0b2sEEKp-c1CpLZorqDF0heRUuJm_zPSuZVSa7XRWkghkvzq7olr2E4BOcoZryn-QCbGVugcZTPs4LA",
"c_nonce_expires_in": 300000
}

4. 型定義の説明

CredentialIssuer

Issuerの識別子を表す型です。URI形式の文字列で、Issuerの一意な識別に使用されます。

定義はissuer+verifier/src/credential-issuer.types.tsを参照してください。

CredentialIssuerMetadata

Issuerのメタデータを定義する型です。クライアント名、サポートするクレデンシャル形式、エンドポイントなどの情報を含みます。

定義はissuer+verifier/src/credential-issuer.types.tsを参照してください。

CredentialResponse

発行されたクレデンシャルのレスポンスを表す型です。JWT形式のクレデンシャルやメタデータなどの情報を含みます。

定義はissuer+verifier/src/credential-response.types.tsを参照してください。

AuthorizationServerIssuer

認可サーバーの識別子を表す型です。URI形式の文字列で、認可サーバーの一意な識別に使用されます。

定義はissuer+verifier/src/authorization-server.types.tsを参照してください。

AuthorizationServerMetadata

認可サーバーのメタデータを定義する型です。Issuer情報、サポートする形式、エンドポイントなどの情報を含みます。

定義はissuer+verifier/src/authorization-server.types.tsを参照してください。

AuthzTokenRequest

アクセストークンリクエストを表す型です。タイプが認可コード、事前認可コードかなどの情報を含みます。

定義はissuer+verifier/src/token-request.types.tsを参照してください。

5. IssuerFlowの各メソッド

findIssuerMetadata

Issuerのメタデータを取得します。

findIssuerMetadata(id: CredentialIssuer): Promise<CredentialIssuerMetadata | null>

パラメータ:

戻り値: メタデータオブジェクト(CredentialIssuerMetadata)またはnullを返します。

createIssuerMetadata

Issuerのメタデータを作成・保存します。

createIssuerMetadata(issuer: CredentialIssuerMetadata): Promise<void>

パラメータ:

戻り値: なし

エラーケース:

  • PROVIDER_NOT_FOUND: 未対応のalgが設定された

offerCredential

クレデンシャルオファーを作成します。

offerCredential(
issuer: CredentialIssuer,
configurations: CredentialConfigurationId[],
options?: OfferOptions
): Promise<CredentialOffer>

パラメータ:

戻り値: クレデンシャルオファーを返します。

クレデンシャルオファーの型定義はissuer+verifier/src/credential-offer.types.tsを参照してください。

エラーケース:

  • FEATURE_NOT_IMPLEMENTED_YET: 未対応のフローが設定された(認可コードフローには未対応です)
  • ISSUER_NOT_FOUND: 未登録のIssuerが設定された

CredentialConfigurationId

クレデンシャル構成IDを定義する型です。

定義はissuer+verifier/src/credential-issuer.types.tsを参照してください。

OfferOptions

クレデンシャルオファー作成時のオプションを定義する型です。事前認可フローを使用するかを設定できます。 定義は下記のとおりです。

type OfferOptions =
| {
usePreAuth: false
state?: unknown
}
| {
usePreAuth: true
txCode?: {
inputMode?: 'numeric' | 'text'
length?: number
description?: string
}
}

issueCredential

クレデンシャルを発行します。

issueCredential(
issuer: CredentialIssuer,
credentialRequest: CredentialRequest,
options?: IssueOptions
): Promise<CredentialResponse>

パラメータ:

戻り値: クレデンシャルレスポンスを返します。

クレデンシャルレスポンスの型定義はissuer+verifier/src/credential-response.types.tsを参照してください。

エラーケース:

  • ISSUER_NOT_FOUND: 未登録のIssuerが設定された
  • PROVIDER_NOT_FOUND: 未対応のformatが設定された
  • INVALID_REQUEST: formatが未設定
  • UNSUPPORTED_CREDENTIAL_TYPE: 指定されたcredential_definitionもしくはproof_typeがサポートされていない
  • INVALID_CREDENTIAL_REQUES: proofが見つからないかサポートされていない
  • INVALID_PROOF: proofが検証できない、未サポートのheaderが設定された、nonceが見つからない
  • UNSUPPORTED_ISSUER_KEY_ALG: Issuerの署名アルゴリズムがサポートされていない
  • AUTHZ_ISSUER_KEY_NOT_FOUND: Issuerの鍵が見つからない
  • INTERNAL_SERVER_ERROR: 署名に失敗した

CredentialRequest

クレデンシャル発行リクエストを定義する型です。クレデンシャルの識別子などを設定できます。

定義はissuer+verifier/src/credential-request.types.tsを参照してください。

IssueOptions

クレデンシャル発行オプションを定義する型です。アルゴリズムやクレームなどを設定できます。 定義は下記のとおりです。

type IssueOptions = {
alg: string
cnonce?: {
c_nonce_expires_in: number
}
claims?: Record<string, unknown>
}

6. AuthzFlowの各メソッド

findAuthzServerMetadata

認可サーバーのメタデータを取得します。

findAuthzServerMetadata(issuer: AuthorizationServerIssuer): Promise<AuthorizationServerMetadata | null>

パラメータ:

戻り値: メタデータオブジェクト(AuthorizationServerMetadata)またはnullを返します。

AuthorizationServerIssuer

認可サーバーのIssuerを定義する型です。

定義はissuer+verifier/src/authorization-server.types.tsを参照してください。

createAuthzServerMetadata

認可サーバーのメタデータを作成・保存します。

createAuthzServerMetadata(
metadata: AuthorizationServerMetadata,
options?: { alg?: 'ES256' }
): Promise<void>

パラメータ:

戻り値: なし

createAccessToken

アクセストークンを発行します。

createAccessToken<T extends GrantType>(
authz: AuthorizationServerIssuer,
tokenRequest: TokenRequest,
options?: TokenRequestOptions[T]
): Promise<Object>

パラメータ:

  • authz: 認可サーバーの識別子(AuthorizationServerIssuer

  • tokenRequest: トークンリクエスト(TokenRequest

  • options: トークンリクエストのオプション

    type TokenRequestOptions = {
    [GrantType.AuthorizationCode]: {
    // 認可コードフローは未対応
    }
    [GrantType.PreAuthorizedCode]: {
    ttlSec?: number
    c_nonce_expire_in?: number
    }
    }

戻り値: アクセストークンは下記のような形式で戻されます:

// grant_typeで事前認可コードが選択された場合
{
access_token: `${encode(jwtHeader)}.${encode(jwtPayload)}.${signature}`,
token_type: 'bearer',
expires_in: option?.ttlSec ?? 86400,
c_nonce: cnonce,
c_nonce_expires_in: option?.c_nonce_expire_in ?? 60 * 5 * 1000, // 5 minutes
}

エラーケース:

  • PROVIDER_NOT_FOUND: 秘密鍵で未対応のアルゴリズムが設定された
  • PRE_AUTHORIZED_CODE_NOT_FOUND: 有効でない事前認可コードが設定された
  • INVALID_REQUEST: 認可サーバーの鍵が未登録、アルゴリズムが未設定、グラントタイプがサポートされていない
  • INTERNAL_SERVER_ERROR: 署名に失敗した
  • FEATURE_NOT_IMPLEMENTED_YET: 認可コードフローを設定(現在未対応)

TokenRequest

クレデンシャル発行リクエストを定義する型です。クレデンシャルの識別子などを設定できます。

定義はissuer+verifier/src/token-request.types.tsを参照してください。

TokenRequestOptions

トークンリクエスト時のオプションを定義する型です。使用するフローなどを設定できます。(認可コードフローは未対応です) 定義は下記のとおりです。

type TokenRequestOptions = {
[GrantType.AuthorizationCode]: {
//TODO: Implement options for authorization code flow
}
[GrantType.PreAuthorizedCode]: {
ttlSec?: number
c_nonce_expire_in?: number
}
}

verifyAccessToken

アクセストークンを検証します。

verifyAccessToken(authz: AuthorizationServerIssuer, accessToken: string): Promise<boolean>

パラメータ:

戻り値: アクセストークンが有効をbooleanで返します。

エラーケース:

  • INVALID_ACCESS_TOKEN: アクセストークンが有効なjwtでないか、authzが期待されたものでない
  • AUTHZ_ISSUER_KEY_NOT_FOUND: 認可サーバーの鍵が見つからない
  • PROVIDER_NOT_FOUND: 署名アルゴリズムが未サポート

7. 注意事項

  1. アクセストークンの検証: クレデンシャル発行時には必ずアクセストークンを検証してください。

  2. セキュリティ: 本番環境では、適切な認証・認可の仕組みを実装してください。

    • 秘密鍵の管理には特に注意を払ってください
    • HTTPSを使用して通信を暗号化してください
  3. URLエンコード: issuer IDにURLエンコードが必要な文字(例::/)が含まれる場合は、適切にエンコードしてください。

8. トラブルシューティング

よくある問題

  • Q:メタデータのバリデーションエラー:

    • A: 提供されたメタデータがCredentialIssuerMetadataスキーマ、AuthorizationServerMetadataスキーマに適合しているかを確認してください。
  • Q:クレデンシャルオファーの作成エラー:FEATURE_NOT_IMPLEMENTED_YET

    • A: 未実装のフローを呼び出していないか確認してください。現在対応しているのは事前認可コードフローです。
  • Q:クレデンシャル発行エラー:INVALID_PROOF

    • A: クレデンシャルリクエストのprooj.jwtのheaderがkidを含んでいるかを確認してください。