開発環境で試す

ローカル開発用API

Public Key based authentication as a Service (PKaaS)は、FIDO認証機能をクラウド上で利用できるようにした認証サービスです。

FIDO認証との関わりについての詳しい解説はこちらを御覧ください。

お手元のパソコンなどの開発環境(以後、ローカル環境と呼びます)上に構築した認証サーバーから呼び出すための「ローカル開発用API」を使って、利便性の高いFIDO認証を試すことができます。

このページでは、「ローカル開発用API」を利用する開発者の方に必要な情報を提供します。

必ず下記の事項についてご確認の上、ご利用ください。

  • 本ページで紹介する「ローカル開発用API」はローカル開発・検証目的のみに利用し、本番サービスや実アプリケーションへの組み込みには使用しないでください。
  • 「ローカル開発用API」では、登録のプロセスが存在しますが、認証に用いる公開鍵をPKaaS内に保存しません。認証に必要な情報は利用者のローカル環境やアプリケーション側で管理してください。
  • 「ローカル開発用API」はFIDO認証を体験できるテスト環境向けAPIです。実在ユーザーや本番相当データでの利用を禁じます。
  • PKaaSのリダイレクト版やAPI版とは実装が異なりますのでご注意ください。

ローカル開発用 API 利用の流れ

1. Yahoo! JAPAN IDを取得する

最初に「Yahoo! JAPAN ID」を取得してください。

Yahoo! JAPAN IDを登録するには

2. アプリケーションを登録する

手順1で取得した「Yahoo! JAPAN ID」でログインして、下記ページからアプリケーションを登録してください。

アプリケーションの登録

3. Client IDを利用して認証用エンドポイントを開発する

手順2で登録したClient ID(アプリケーションID)はローカル環境に構築した認証サーバーに設置した認証用エンドポイントからAPIをリクエストする際に利用します。

ローカル開発用APIリファレンス

ローカル開発用APIは以下の機能を提供します。

PKaaSはFIDO登録とFIDO認証に関する2対のAPIを提供しています。必ず「登録のチャレンジ発行」→「登録の署名検証」もしくは「認証のチャレンジ発行」→「認証の署名検証」の順番で実行してください。

エンドポイント 概要
/api/attestation/options 登録のチャレンジ発行
/api/attestation/result 登録の署名検証
/api/assertion/options 認証のチャレンジ発行
/api/assertion/result 認証の署名検証

共通ヘッダー

これらすべてのAPIリクエストには下記のヘッダーを付与してください。

パラメータ 必須 説明
User-agent string "Yahoo AppID:"に続けて、前の手続きで取得したClientIDを入力してください。

登録のチャレンジ生成

リクエストURL
https://pkaas.yahooapis.jp/api/attestation/options
メソッド
POST
Content-Type
application/json

リクエストパラメータ

標準的なブラウザの実装により対応状況が異なります。MDNなどを参考にしてください。

パラメータ 必須 説明
username string PKaaSがユーザーを識別するための識別子です。ローカル開発用APIでは、ユーザー名が以下の規則に従う必要があります。例えばユーザー名が「sample1234」だとしたら、sample1234#localhostとしてください。
displayName string ユーザーの表示に用いるためのニックネームです。
authenticatorSelection array(string) MDNを参照してください。
attestation string MDNを参照してください。

サンプルリクエスト

curl 'https://pkaas.yahooapis.jp/api/attestation/options' \
  -H "Content-Type: application/json" \
  -H "User-agent: Yahoo AppID:<Client ID>" \
  --data-raw '{"username":"sample1234#localhost","displayName":"sample1234#localhost","authenticatorSelection":{"requireResidentKey":true,"userVerification":"preferred","authenticatorAttachment":"platform"},"attestation":"none"}'

レスポンスパラメータ

エラー時のレスポンスはYahoo!デベロッパーネットワークのエラーメッセージもしくは、LINE FIDO2 serverのエラーメッセージに準じます。

パラメータ 説明
status string 処理結果を示します。"ok": 処理に成功、"failed":処理に失敗
errorMessage array(string) LINE FIDO2 serverの内部エラーに準じたエラーメッセージです。
sessionId string セッションIDです。
(rp以下) Navigator.credentials.create()のパラメータに準じます。登録するサイトのドメインやチャレンジなどが含まれています。詳しくはMDNなどを参照してください。

サンプルレスポンス

{
    "status": "ok",
    "errorMessage": "",
    "sessionId": "",
    "rp": {
        "name": "PKaaS Demo",
        "icon": "",
        "id": "localhost"
    },
    "user": {
        "name": "sample1234#localhost",
        "icon": "",
        "id": "n99PZyE5b_muRkeyRUyvCgg4XsXg5YA816PVVO0UqBI",
        "displayName": "sample1234#localhost"
    },
    "challenge": "PTlwETmKOkcS1m3__3__1LDf1kWX8OZIZCZYSqdWzoJRWUAkvRb9velN98Wvdgd0VwNCwBpX_-UKEA3Fv4dRRA",
    "pubKeyCredParams": [
        {
            "type": "public-key",
            "alg": -65535
        },
        {
            "type": "public-key",
            "alg": -257
        }
        // ... (その他のパラメータ)
    ],
    "timeout": 180000,
    "excludeCredentials": [],
    "authenticatorSelection": {
        "authenticatorAttachment": "platform",
        "requireResidentKey": true,
        "userVerification": "preferred"
    },
    "attestation": "none",
    "extensions": {
        "credProps": true
    }
}

登録の署名検証

リクエストURL
https://pkaas.yahooapis.jp/api/attestation/result
メソッド
POST
Content-Type
application/json

リクエストパラメータ

標準的なブラウザの実装により対応状況が異なります。MDNなどを参考にしてください。

パラメータ 必須 説明
rawId MDNを参照してください。
id MDNを参照してください。
response MDNを参照してください。
type string "public-key"を指定してください。
extensions ローカル開発用APIを使うことを明示するために{”localhost”:true}を必ず含めてください。その他の標準的な拡張機能についても利用可能です。MDNを参照してください。

サンプルリクエスト

curl 'https://pkaas.yahooapis.jp/api/attestation/result' \
  -H "Content-Type: application/json" \
  -H "User-agent: Yahoo AppID:<Client ID>" \
  --data-raw '{"rawId":"sampleSample","id":"sampleSample","response":{"clientDataJSON":"eyJ....","transports":["hybrid","internal"]},"type":"public-key","extensions":{"credProps":{"rk":true}, "localhost":true}}'

レスポンスパラメータ

パラメータ 説明
status string 処理結果を示します。"ok": 処理に成功、"failed":処理に失敗
errorMessage array(string) LINE FIDO2 serverの内部エラーに準じたエラーメッセージです。詳しくはこちらを参照してください。
sessionId string セッションIDです。
localhost string ローカル開発用API専用のレスポンスです。ローカル開発用APIは、登録用署名の検証を行いますが、公開鍵を保存しません。そのため、エンコード済み鍵情報が含むこの項目をローカル上に保存してください。認証時に使うため、この情報を「*」と呼びます。

サンプルレスポンス

{"status":"ok","errorMessage":"","sessionId":"","localhost":"eyJ...."}

認証のチャレンジ発行

リクエストURL
https://pkaas.yahooapis.jp/api/assertion/options
メソッド
POST
Content-Type
application/json

リクエストパラメータ

標準的なブラウザの実装により対応状況が異なります。MDNなどを参考にしてください。

パラメータ 必須 説明
username string PKaaSがユーザーを識別するための識別子です。ローカル開発用APIでは、ユーザー名が以下の規則に従う必要があります。例えばユーザー名が「sample1234」だとしたら、sample1234#localhostとしてください。
userVerification string 認証器の検証方法を指定します。
"required" 必ずユーザー検証を伴う認証器を使用します。
"preferred" ユーザー検証が可能であれば行いますが、必須ではありません。
"discouraged" ユーザー検証を行わない認証器を優先します。

サンプルリクエスト

curl 'https://pkaas.yahooapis.jp/api/assertion/options' \
  -H "Content-Type: application/json" \
  -H "User-agent: Yahoo AppID:<Client ID>" \
  --data-raw '{"username":"sample1234#example.com","userVerification":"preferred"}'

レスポンスパラメータ

パラメータ 説明
status string 処理結果を示します。"ok": 処理に成功、"failed":処理に失敗
errorMessage array(string) LINE FIDO2 serverの内部エラーに準じたエラーメッセージです。
sessionId string セッションIDです。
(challenge以下) Navigator.credentials.get()のパラメータに準じます。登録するサイトのドメインやチャレンジなどが含まれています。詳しくはMDNなどを参照してください。

サンプルレスポンス

{
    "status": "ok",
    "errorMessage": "",
    "sessionId": "",
    "challenge": "ssSafaXDbmLDxtQnFPALcUYWDdOT180yOoay-P0EdeDVj6uKvo28chUZOU2fLc_7LJK1wlj-ebg02iRQQPgVtw",
    "timeout": 180000,
    "rpId": "...",
    "allowCredentials": [
        {
            "type": "public-key",
            "id": "ugHWbvJQEDUeqLzA5wZqO6KEnT-lRLtMHbLbboNIDDc",
            "transports": [
                "internal"
            ]
        }
    ],
    "userVerification": "preferred",
    "extensions": {}
}

認証の署名検証

リクエストURL
https://pkaas.yahooapis.jp/api/assertion/result
メソッド
POST
Content-Type
application/json

リクエストパラメータ

標準的なブラウザの実装により対応状況が異なります。MDNなどを参考にしてください。

パラメータ 必須 説明
rawId string クレデンシャル識別子(credentialId)
id string クレデンシャル識別子(credentialId)のBase64URLエンコードした文字列
response list Navigator.credentials.get()のパラメータに準じます。登録するサイトのドメインやチャレンジなどが含まれています。詳しくはMDNなどを参照してください。
type string "public-key"を指定してください。
extensions list ローカル開発用API では、PKaaSに鍵を保存していませんので、明示的に公開鍵を追加してからリクエストを送る必要があります。登録の署名検証でレスポンスとして受け取った”*”の情報を、{“localhost”:“*”}として追加してください。その他の項目に関してはMDNなどを参照してください。

サンプルリクエスト

curl 'https://pkaas.yahooapis.jp/api/assertion/result' \
  -H "Content-Type: application/json" \
  -H "User-agent: Yahoo AppID:<Client ID>" \
  --data-raw '{"rawId":"sampleSample","id":"sampleSample","response":{"clientDataJSON":"eyJ...","signature":"...","authenticatorData":"..."},"type":"public-key","extensions":{"localhost":"eyj...(*の情報)"}}'

レスポンスパラメータ

パラメータ 説明
status string 処理結果を示します。"ok": 処理に成功、"failed":処理に失敗
errorMessage array(string) LINE FIDO2 serverの内部エラーに準じたエラーメッセージです。
sessionId string セッションIDです。

サンプルレスポンス

{"status":"ok","errorMessage":"","sessionId":""}

ローカル開発用APIサンプルコード

ローカル開発用 API を利用する際のサンプルコードを示します。

PKaaSデモの対応機種・ブラウザ

  • • Android: バージョン9.0以上、画面ロックを設定済み
  • • iOS/iPadOS: バージョン16以上、画面ロックを設定済み
  • • macOS: バージョン13(macOS Ventura)以上、画面ロックを設定済み
  • • 推奨ブラウザ:
  • - Google Chrome
  • *一部のバージョンは非対応の場合があります。

開発を行う前に

  

ローカル開発用APIはPKaaS API版とは動作が異なり、どなたでも利用できる一方で、以下の制約があります。

  
  • hostnameの変更設定
  • SSL(HTTPS)による通信
  

hostnameの設定変更

fido-dev.test という開発用ドメインを、パソコンの /etc/hosts(Windowsの場合は hosts ファイル)で 127.0.0.1 に割り当ててください。

これはWebAuthnの仕様とブラウザでの実装の差分により、認証ドメインとして"localhost"が利用できないためです。fido-dev.test以外に書き換えた場合にはAPIはレスポンスを行うことができません。

127.0.0.1 fido-dev.test
		    
  

SSL(HTTPS)による通信

WebAuthnはセキュアコンテキストでの利用が前提となるため、フロントエンドを https://fido-dev.test:<port> のようにHTTPSで起動できるよう、ローカル証明書(例:mkcertなど)を用いてSSL(HTTPS)通信を設定してください。

mkcert をインストール例(macOS: Homebrew):

brew install mkcert
mkcert -install

fido-dev.test 用の証明書を作成(プロジェクト直下など、証明書を置きたいディレクトリで実行)します。

mkcert fido-dev.test localhost 127.0.0.1 ::1

実行すると、例えば次のようなファイルが生成されます(ファイル名は環境により多少異なります)。

fido-dev.test+3.pem
fido-dev.test+3-key.pem

ローカル環境に構築した認証サーバーに証明書を設定し、https://fido-dev.test でアクセスできることを確認してください。

開発者が設置するもの

ここでは、認証用ページ、認証用ページから読み込むJavaScriptと、JavaScript内から呼び出すためのエンドポイント(/attestation/*、/assertion/*)を同一ホスト(同一Origin)配下で提供際のサンプルコードを提供します。

サンプルは、認証用ページのJavaScriptによって同一ホスト(同一Origin)のエンドポイントへリクエストした後、さらにエンドポイントからローカル開発用APIへリクエストを転送して動作させます。

※「同一ホスト」とは、ページを開いているURL(例:https://fido-dev.test:3000)と、中継APIのURLが同じスキーム/ホスト/ポートであることを指します。

認証用ページ(HTML)

まず、認証用ページとして、以下のようなHTML(ファイル例: index.html)を設置します。

FIDO認証を実現するために、「ユーザー名」を入れるだけでFIDO登録・認証を行うことを目的としています。

実際に導入をご検討いただく際には、ご自身のサイトに合わせた適切なデザインや設計が必要です。

この設計は、利用者による不正を防ぐために重要な役割を果たします。例えば、既存の認証方法にPKaaSを利用したFIDO認証を追加する場合、既存の認証方法で成功したユーザーにのみFIDO登録を行わせるように設計することで、他人のアカウントに対してFIDO登録することを防ぐことが可能です。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>PKaaS WebAuthn Client</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    body { padding: 20px; }
    .form-section { margin-bottom: 30px; padding: 20px; border: 1px solid #dee2e6; border-radius: 5px; }
    .hidden { display: none; }
    .spinner-border { width: 1rem; height: 1rem; margin-left: 10px; }
    #status { margin-top: 10px; padding: 10px; border-radius: 5px; }
    .errorText { color: red; }
    pre { background-color: #f8f9fa; padding: 10px; border-radius: 5px; }
  </style>
</head>
<body>
  <div class="container">
    <h1 class="mb-4">PKaaS WebAuthn Client</h1>
    <div class="form-section">
      <h3>ユーザー情報</h3>
      <div class="mb-3">
        <label for="username" class="form-label">Username:</label>
        <input type="text" class="form-control" name="username" id="username" placeholder="ユーザー名を入力">
      </div>
      <div class="mb-3">
        <label for="userDisplayName" class="form-label">Display Name:</label>
        <input type="text" class="form-control" name="userDisplayName" id="userDisplayName" placeholder="表示名を入力">
      </div>
    </div>
    <div class="mb-4">
      <button type="button" class="btn btn-primary" id="register">
        Register
        <span class="spinner-border hidden" id="registerSpinner"></span>
      </button>
      <button type="button" class="btn btn-success" id="authenticate">
        Authenticate
        <span class="spinner-border hidden" id="authenticateSpinner"></span>
      </button>
    </div>

    <div id="status" class="alert alert-info hidden"></div>
    <div id="credentialListContainer"></div>

    <div class="form-section">
      <h3>Debug: localhostInfo</h3>
      <div class="mb-2">
        <button type="button" class="btn btn-outline-secondary" id="refreshLocalhostInfo">Refresh</button>
      </div>
      <div class="mb-2">
        <div><strong>present:</strong> <span id="localhostInfoPresent">-</span></div>
        <div><strong>at:</strong> <span id="localhostInfoAt">-</span></div>
      </div>
      <div class="mb-2">
        <div class="form-label">localhostInfo (raw)</div>
        <pre id="localhostInfoRaw" style="white-space: pre-wrap; word-break: break-all;">-</pre>
      </div>
      <div>
        <div class="form-label">localhostInfo (decoded)</div>
        <pre id="localhostInfoB64" style="white-space: pre-wrap; word-break: break-all;">-</pre>
      </div>
    </div>
  </div>

  <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
  <script src="/index.js"></script>
  <script>
    async function refreshLocalhostInfo() {
      try {
        const res = await fetch('/debug/localhostInfo', { cache: 'no-store' });
        const data = await res.json();
        document.getElementById('localhostInfoPresent').textContent = String(data.present);
        document.getElementById('localhostInfoAt').textContent = data.at || '-';
        document.getElementById('localhostInfoRaw').textContent = data.localhostInfo || '-';
        document.getElementById('localhostInfoB64').textContent = data.localhostInfoDecoded || '-';
      } catch (e) {
        document.getElementById('localhostInfoPresent').textContent = 'error';
        document.getElementById('localhostInfoAt').textContent = '-';
        document.getElementById('localhostInfoRaw').textContent = String(e);
        document.getElementById('localhostInfoB64').textContent = '-';
      }
    }

    window.addEventListener('load', () => {
      document.getElementById('refreshLocalhostInfo').addEventListener('click', refreshLocalhostInfo);
      refreshLocalhostInfo();
    });
  </script>
</body>
</html>

認証用ページから読み込むJavaScript

FIDO認証をブラウザ上で実現する仕組みは、Web Authentication(WebAuthn)と呼ばれます。WebAuthnでは、JavaScriptを用いて、ブラウザから生体認証などの認証機能を呼び出したり、生成した電子署名をFIDOサーバーへ送信したりします。今回は、オープンソースソフトウエア(OSS)として公開されている以下のライブラリに含まれるJavaScript(index.js)を利用します。

LINE FIDO2 Server

JavaScript内から呼び出すためのエンドポイント

前の手順で設置したJavaScriptからFIDOサーバーと通信する際、ブラウザは同一のドメインに設置されたエンドポイントへの通信のみを許可しています。これは、ページに埋め込まれた悪意あるJavaScriptが、ユーザーの意図しない形で攻撃者の用意した別ドメインへ通信するのを防ぐため、ブラウザによって課されているセキュリティ制限です。

そのため、JavaScriptはまずローカル環境に構築した認証サーバーへリクエストを送信する必要があります。受け取ったリクエストをPKaaSへ転送することで、チャレンジの発行や署名検証の結果を取得できます。この際、ブラウザからローカル環境に構築した認証サーバーが受け取ったリクエストに、適切なClient IDを付加する必要があります。

以下に、Node.jsを用いて開発者サイトにFIDO認証用のエンドポイント(ファイル例: server.js)を実装する際のサンプルコードを示します。

// Node.jsサンプルコード
// PKaaS WebAuthn クライアント用 HTTPS プロキシサーバ
// ローカルでUIを動かしつつ、APIリクエストを外部サーバへ中継・加工する開発用
const https = require('https'); // HTTPSサーバ用
const fs = require('fs');       // ファイル操作用
const path = require('path');   // パス操作用
const httpProxy = require('http-proxy'); // 汎用(はんよう)プロキシ用
const zlib = require('zlib');   // 圧縮データ展開用

function dropHeaderCaseInsensitive(headers, headerNameLower) {
  if (!headers) return;
  for (const key of Object.keys(headers)) {
    if (key.toLowerCase() === headerNameLower) {
      delete headers[key];
    }
  }
}

function setHeaderCaseInsensitive(headers, headerName, value) {
  if (!headers) return;
  const lower = String(headerName).toLowerCase();
  dropHeaderCaseInsensitive(headers, lower);
  headers[headerName] = value;
}

// プロキシ先APIサーバのURL(必要に応じて書き換え)
const TARGET_URL = 'https://pkaas.yahooapis.jp';

// Yahoo App ID(環境変数 YAHOO_APP_ID で上書き可)
const YAHOO_APP_ID = process.env.YAHOO_APP_ID || ;

// http-proxyインスタンス(汎用(はんよう)プロキシ用)
const proxy = httpProxy.createProxyServer({});

// /attestation/result のレスポンスから取得した localhostInfo を一時保存
let lastLocalhostInfo = null;
let lastLocalhostInfoAt = null;

// APIパスをプロキシ先用に変換(/assertion → /api/assertion など)
function mapApiPath(originalUrl) {
  if (typeof originalUrl !== 'string') return originalUrl;
  const qIndex = originalUrl.indexOf('?');
  const pathPart = qIndex >= 0 ? originalUrl.slice(0, qIndex) : originalUrl;
  const queryPart = qIndex >= 0 ? originalUrl.slice(qIndex) : '';
  if (pathPart === '/assertion' || pathPart.startsWith('/assertion/')) {
    return pathPart.replace(/^\/assertion(?=\/|$)/, '/api/assertion') + queryPart;
  }
  if (pathPart === '/attestation' || pathPart.startsWith('/attestation/')) {
    return pathPart.replace(/^\/attestation(?=\/|$)/, '/api/attestation') + queryPart;
  }
  return originalUrl;
}

// HTTPSサーバ用証明書・秘密鍵・ポート番号(環境変数で上書き可)
const PORT = process.env.PORT || 3000;
const CERT_PATH = process.env.HTTPS_CERT_PATH || './fido-dev.test+3.pem';
const KEY_PATH = process.env.HTTPS_KEY_PATH || './fido-dev.test+3-key.pem';
const options = {
  key: fs.readFileSync(KEY_PATH),
  cert: fs.readFileSync(CERT_PATH)
};

// メインHTTPSサーバ
const server = https.createServer(options, (req, res) => {
  // すべてのリクエストをここで受ける
  console.log(`[Server] Received: ${req.method} ${req.url}`);

  // 1. ルートアクセスは index.html を返す
  if (req.url === '/') {
    const filePath = path.join(__dirname, 'index.html');
    fs.readFile(filePath, (err, content) => {
      if (err) {
        res.writeHead(500, { 'Content-Type': 'text/plain' });
        res.end('サーバエラーが発生しました。');
        return;
      }
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.end(content, 'utf-8');
    });

  // 2. index.js もローカルから返す
  } else if (req.url === '/index.js') {
    const filePath = path.join(__dirname, 'index.js');
    fs.readFile(filePath, (err, content) => {
      if (err) {
        res.writeHead(500, { 'Content-Type': 'text/plain' });
        res.end('サーバエラーが発生しました。');
        return;
      }
      res.writeHead(200, { 'Content-Type': 'text/javascript' });
      res.end(content, 'utf-8');
    });

  // 3. デバッグ用: 直近の localhostInfo を返す
  } else if (req.url === '/debug/localhostInfo') {
    const present = !!(lastLocalhostInfo && lastLocalhostInfo.length > 0);

    let decoded = null;
    let decodedJson = null;
    if (present) {
      try {
        // localhostInfo は base64url の可能性があるので正規化してデコード
        const normalized = lastLocalhostInfo
          .replace(/-/g, '+')
          .replace(/_/g, '/');
        const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
        decoded = Buffer.from(padded, 'base64').toString('utf-8');
        try {
          decodedJson = JSON.parse(decoded);
        } catch {
          decodedJson = null;
        }
      } catch {
        decoded = null;
        decodedJson = null;
      }
    }

    const payload = {
      present,
      at: lastLocalhostInfoAt,
      localhostInfo: present ? lastLocalhostInfo : null,
      localhostInfoDecoded: decoded,
      localhostInfoDecodedJson: decodedJson,
    };

    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(payload));

  // 4. APIリクエストはプロキシ先へ中継
  } else if (req.url.startsWith('/assertion/') || req.url.startsWith('/attestation/') || req.url.startsWith('/credentials')) {
    const originalUrl = req.url;
    const mappedUrl = mapApiPath(originalUrl);
    if (mappedUrl !== originalUrl) {
      console.log(`[Proxy] Path rewrite: ${originalUrl} -> ${mappedUrl}`);
      req.url = mappedUrl;
    } else {
      console.log(`[Proxy] Path no rewrite: ${originalUrl}`);
    }

    // 必須ヘッダー追加(Yahoo App ID)
    // ブラウザ由来の user-agent と二重になると upstream が AppID を見ないことがあるので、必ず上書き。
    setHeaderCaseInsensitive(req.headers, 'User-Agent', `Yahoo AppID: ${YAHOO_APP_ID}`);

    // /assertion/result, /attestation/result へのPOSTはボディ加工+直接リクエスト
    if ((originalUrl === '/assertion/result' || originalUrl === '/assertion/result/' || originalUrl === '/attestation/result' || originalUrl === '/attestation/result/') && req.method === 'POST') {
      let body = '';
      req.on('data', chunk => { body += chunk.toString(); });
      req.on('end', () => {
        try {
          const data = JSON.parse(body);
          // extensionsがなければ作成
          if (!data.extensions) data.extensions = {};
          const isAttestationResult = (originalUrl === '/attestation/result' || originalUrl === '/attestation/result/');
          const isAssertionResult = (originalUrl === '/assertion/result' || originalUrl === '/assertion/result/');

          // /attestation/result の場合のみ localhost=true をセット
          if (isAttestationResult) data.extensions.localhost = true;

          // /assertion/result の場合: 直近で保存した localhostInfo をセット(なければ何もしない)
          if (isAssertionResult && lastLocalhostInfo) {
            data.extensions.localhost = lastLocalhostInfo;
          }

          // --- ここからプロキシ先APIへのPOSTリクエスト処理 ---
          // 1. クライアントから受け取ったデータをJSON文字列に変換
          const modifiedBody = JSON.stringify(data);
          console.log(`[Proxy] Forwarding: ${req.method} ${originalUrl} -> ${req.url}`);
          console.log(`[Proxy] Modified body:`, modifiedBody);

          // 2. プロキシ先APIサーバのURL情報をパース
          const targetUrlObj = new URL(TARGET_URL);

          // 3. プロキシ先APIサーバへ送るリクエストオプションを構築
          //    - Host: プロキシ先のホスト名に明示的に書き換え(API側で必須の場合がある)
          //    - Content-Type/Content-Length: JSON送信で必須
          //    - rejectUnauthorized: false で自己署名証明書なども許可(開発用)
          const options = {
            hostname: targetUrlObj.hostname,
            port: targetUrlObj.port,
            path: req.url,
            method: req.method,
            headers: {
              ...req.headers,
              'Host': targetUrlObj.hostname,
              'Content-Type': 'application/json',
              'Content-Length': Buffer.byteLength(modifiedBody)
            },
            rejectUnauthorized: false // 本番ではtrue推奨。開発用はfalseでOK
          };
          console.log(`[Proxy] Proxy request options:`, options);

          // 4. https.request でAPIサーバへPOSTリクエストを送信
          //    - レスポンスはそのままクライアントへ返す
          //    - /attestation/result の場合はレスポンスbodyからlocalhostInfoを抽出・保存
          const https = require('https');
          const proxyReq = https.request(options, (proxyRes) => {
            console.log(`[Proxy] Received response: ${proxyRes.statusCode} ${proxyRes.statusMessage}`);
            res.writeHead(proxyRes.statusCode, proxyRes.headers);
            // レスポンスbodyをバッファにためつつ、クライアントにもストリームで返す
            const chunks = [];
            proxyRes.on('data', (chunk) => {
              chunks.push(chunk);
              res.write(chunk);
            });
            proxyRes.on('end', () => {
              try {
                // /attestation/result の場合のみ、レスポンスbodyからlocalhostInfoを抽出
                if (originalUrl === '/attestation/result' || originalUrl === '/attestation/result/') {
                  const raw = Buffer.concat(chunks);
                  const encoding = String(proxyRes.headers['content-encoding'] || '').toLowerCase();
                  let decoded = raw;
                  // 圧縮されている場合は展開
                  if (encoding === 'gzip') {
                    decoded = zlib.gunzipSync(raw);
                  } else if (encoding === 'deflate') {
                    decoded = zlib.inflateSync(raw);
                  } else if (encoding === 'br') {
                    decoded = zlib.brotliDecompressSync(raw);
                  }
                  const text = decoded.toString('utf-8');
                  console.log(`[Proxy] Response body:`, text);
                  // JSONとしてパースし、localhostInfoがあれば保存
                  const json = JSON.parse(text);
                  if (json && typeof json.localhost === 'string' && json.localhost.length > 0) {
                    lastLocalhostInfo = json.localhost;
                    lastLocalhostInfoAt = new Date().toISOString();
                    console.log('[Proxy] Stored localhostInfo:', lastLocalhostInfoAt, lastLocalhostInfo);
                  }
                }
              } catch (e) {
                // レスポンスがJSONでなかった場合など
                console.warn('[Proxy] Failed to parse /attestation/result response:', e);
              }
              res.end();
            });
          });

          // プロキシ先へのリクエスト失敗時
          proxyReq.on('error', (err) => {
            console.error('プロキシエラー:', err);
            res.writeHead(500, { 'Content-Type': 'text/plain' });
            res.end('プロキシエラーが発生しました。');
          });
          proxyReq.write(modifiedBody);
          proxyReq.end();
        } catch (err) {
          console.error('JSON parse error:', err);
          res.writeHead(400, { 'Content-Type': 'text/plain' });
          res.end('Invalid JSON');
        }
      });

    } else {
      // その他のAPIリクエストは http-proxy でそのまま中継
      console.log(`[Proxy] Forwarding (other): ${req.method} ${originalUrl} -> ${req.url}`);
      console.log(`[Proxy] Headers:`, req.headers);

      proxy.web(req, res, { target: TARGET_URL, changeOrigin: true }, (err) => {
        console.error('[Proxy] proxy.web error:', err);
        res.writeHead(500, { 'Content-Type': 'text/plain' });
        res.end('プロキシエラーが発生しました。');
      });
    }

  } else {
    // 5. それ以外は404
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('ページが見つかりません。');
  }
});

// サーバ起動
server.listen(PORT, () => {
  console.log(`Server listening on https://localhost:${PORT}`);
});

ディレクトリ構成例

本ページのサンプルでは、ローカル環境に構築した認証サーバー上に「認証用ページ(HTML)」「認証用ページから読み込むJavaScript」「JavaScriptから呼び出すためのエンドポイント(プロキシ/中継)」を設置して動作させます。

※サンプルコードをコピーしただけでは動作しないため、Client IDなど適時ご自身の環境に合わせて書き換えてください。

認証用ページ(HTML)からは js/index.js を読み込み、JavaScriptは同一ホスト上の /attestation/* および /assertion/* にリクエストし、ローカル環境に構築した認証サーバーからPKaaSへ転送します。

example-app/
├─ server/			# 認証用エンドポイント(中継サーバー)
│  ├─ server.js			# Node.jsサンプル(/attestation/* /assertion/* をプロキシ)
│  ├─ package.json
│  └─ node_modules/
└─ public/			# ブラウザに配信する静的ファイル
   ├─ index.html		# 認証用ページ(HTML)
   └─ js/
      └─ index.js		# 認証用ページから読み込むJavaScript(WebAuthn呼び出しなど)