本記事は生成 AI と共同で執筆しています。事実関係は可能な範囲で公式ドキュメント等と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。

本記事ではアカウント名や ID をプレースホルダで表記しています。例:

  • GitHub Pages 配信先ドメイン: <gh-user>.github.io (本リポジトリでは nakamura196.github.io)
  • Firebase プロジェクト ID: <firebase-pid>
  • JSONkeeper のエンドポイント: <jsonkeeper-base> (本ミラーでは Cloudflare Workers 版を本流とした)

自身の環境では、すべて自分の値に置き換えて読んでください。

デモ

実際に動いているミラーは以下の URL で公開しています。本記事の手順を読みながらブラウザで挙動を確かめていただけます。

ログイン (Google など) → Curation の編集 → 保存 → 再オープンまで、ブラウザだけで一通り動きます。バックエンドは本記事の構成で立てた Cloudflare Workers + D1 版 (本流) と PythonAnywhere の Flask 版 (フォールバック) の二系統が並走しています。

デモ運用としてのお願い

上記のバックエンド (Cloudflare Workers + D1 / PythonAnywhere) は デモ用途 で運用しているもので、データの長期保存を保証するものではありません。以下のいずれかのタイミングで 予告なく停止する可能性があります

  • CODH のサービスが再開したとき (本ミラー自体の役目が終わるため)
  • 運用者の都合 (運用負荷の見直し、Firebase プロジェクトの整理、料金プラン変更など)

したがって:

  • 保存したキュレーション JSON は、こまめにエクスポート (ダウンロード) してご自身の手元に保管してください
  • 継続的・本格的な利用が必要な方は、本記事の手順を参考に ご自身の環境で同等のバックエンドを立てる ことを推奨します (Cloudflare Workers + D1 / PythonAnywhere いずれも個人の無料枠で完結します)

codh-mirror インデックス (https://nakamura196.github.io/codh-mirror/)

はじめに — 前回からの方針転換

別記事 CODH ツール群の暫定ミラー専用リポジトリを GitHub Pages で立てる では、codh.rois.ac.jp の長期停止に対応する暫定ミラー codh-mirror を立てた一方で、

落とし穴 5: 認証・保存バックエンドは結局動かない

として、IIIF Curation Viewer/Manager/Editor/Board の ログイン (Firebase)キュレーション保存 (mp.ex.nii.ac.jp/api/curation/json 経由) は復旧の対象外、と書きました。閲覧用途に絞った暫定ミラーとして運用コストを抑える、という判断です。

その後、別途 JSONkeeper 互換のサーバを 2 系統 (PythonAnywhere に上流 Flask、Cloudflare Workers + D1 に自作再実装) で立てたタイミングで、「サーバ側は揃っているのだから、ミラー本体も認証・保存を有効化してしまえばよい」 という流れに方針転換しました。本記事はその転換と、codh-mirror リポジトリ側で行った具体的な変更点をまとめます。

サーバ側 (JSONkeeper) のデプロイ詳細は別記事 2 本に分けてあります。

本記事は、これら 2 つを クライアント側 (codh-mirror) からどう繋ぎ込むか の話です。

本記事の作業後の codh-mirror の Manager トップ — ログイン待機状態の UI まで到達している (実際のログイン完了状態の撮影は読者の Firebase プロジェクトに依存するため省略)

出発点 / 着地点

出発点

項目
ミラーの配信元GitHub Pages https://<gh-user>.github.io/codh-mirror/
Curation Viewer / Manager / Editor / Board の認証authFirebase.js にオリジナルの codh-81041 プロジェクトがハードコード。現状この OAuth 認可は通らない
Curation Viewer / Manager / Editor / Player / Board の保存先index.js 内 計 9 箇所 (Viewer 内だけで 5 箇所) に curationJsonExportUrl: 'https://mp.ex.nii.ac.jp/api/curation/json'。現在エンドポイントは応答せず 404/503
iiif-curation-manager のサブツールリンクcurationViewerUrlcurationBoardUrl の 4 つに http://codh.rois.ac.jp/software/iiif-curation-*/demo/ の絶対 URL がハードコード

着地点

項目
Firebase プロジェクト既存の kunshujo-c を再利用。Authorized domains に <gh-user>.github.io を追加
Firebase Web SDK config4 ツール (Viewer / Manager / Editor / Board) の authFirebase.jsfirebaseConfigkunshujo-c に差し替え
FirebaseUI の挙動signInFlow: 'popup' を Manager / Editor / Board の authFirebase.js にも明示 (Viewer は元から popup)
JSONkeeper エンドポイント5 ツール (Viewer / Manager / Editor / Player / Board) の curationJsonExportUrl 計 9 箇所を Cloudflare Workers + D1 版 (<jsonkeeper-base>) に書き換え
Manager のサブツールリンク../iiif-curation-viewer/ などの 相対 URL に変更し、GitHub Pages 配下の同階層を指すように

1. Firebase プロジェクトの差し替え

純正版が参照していた Firebase プロジェクト codh-81041 はオリジナル側の運用であり、ミラー作業者側からは触れません。よって新たに自前のプロジェクトを当てる必要があります。本ミラーでは 既存の kunshujo-c プロジェクトを再利用 しました (別用途で立てていた個人プロジェクトに同居させる方針)。

1.1 Firebase Console での準備

Firebase Console (https://console.firebase.google.com/project/<firebase-pid>/) で次の 2 点を設定します。

Authentication > Settings > Authorized domains<gh-user>.github.io を追加。デフォルトでは localhost / <firebase-pid>.firebaseapp.com / <firebase-pid>.web.app のみ許可されているため、GitHub Pages から OAuth 認可エンドポイントを叩くにはここに 明示追加が必須

Authentication > Sign-in method で必要なプロバイダ (本ミラーでは Google / Facebook / Twitter / Email — 元の純正版が authFirebase.jssignInOptions で挙げているもの) をそれぞれ Enabled に。プロジェクトが新規なら全部 Disabled なので、ここを忘れるとログインボタンを押しても何も出ません。

1.2 authFirebase.js の Web SDK config 差し替え

各ツールに同名 (authFirebase.js) のファイルがあり、ここに Firebase Web SDK の firebaseConfig がハードコードされています。apiKey 等は Firebase Web SDK の仕組み上クライアントに公開される識別子 (秘密鍵ではない) なので、Console > プロジェクトの設定 > マイアプリ で発行された値をそのまま貼れば OK です。

-    apiKey: 'AIzaSyAcsAVeIJ5l2HCWY0OlCMxP-OVXodONYqA',
-    authDomain: 'codh-81041.firebaseapp.com',
-    databaseURL: 'https://codh-81041.firebaseio.com',
-    projectId: 'codh-81041',
-    storageBucket: 'codh-81041.appspot.com',
-    messagingSenderId: '230970439562'
+    apiKey: '<firebase-apiKey>',
+    authDomain: '<firebase-pid>.firebaseapp.com',
+    projectId: '<firebase-pid>',
+    storageBucket: '<firebase-pid>.firebasestorage.app',
+    messagingSenderId: '<firebase-mid>',
+    appId: '<firebase-appId>'

差し替え対象は 4 ファイル。

ファイル
iiif-curation-viewer/authFirebase.jsfirebaseConfig ブロック
iiif-curation-manager/authFirebase.js同上
iiif-curation-editor/authFirebase.js同上
iiif-curation-board/authFirebase.js同上

Player はクライアントから保存しない (再生専用) のため authFirebase.js を持っていません。

⚠ なお、databaseURL は Realtime Database を使わない場合は省略可で、新プロジェクトでは firebasestorage.app 形式が正となります。元のキーをそのまま流用せず Firebase Console が出している値を貼ること。

2. signInFlow: 'popup' の落とし穴 — Viewer は動くのに Manager/Editor/Board が完了しない

ここが一番ハマったところです。firebaseConfig の差し替えだけでは Manager/Editor/Board のログインが完了しません。ログインボタンを押すと別タブに OAuth 同意画面が出て、認可が成功した後にタブが閉じ、しかし元のページの認証状態がログイン済みに切り替わらない、という挙動です。

原因は Viewer と他 3 ツールの authFirebase.js1 行の差 にありました。

 var uiConfig = {
+    signInFlow: 'popup',
     signInOptions: [
         firebase.auth.GoogleAuthProvider.PROVIDER_ID,
         firebase.auth.FacebookAuthProvider.PROVIDER_ID,
         firebase.auth.TwitterAuthProvider.PROVIDER_ID,
         firebase.auth.EmailAuthProvider.PROVIDER_ID,
     ],
     tosUrl: ''
 };

FirebaseUI 3.x の signInFlow指定しないと federated provider はリダイレクトモード になります。リダイレクトモードでは、認可後に https://<firebase-pid>.firebaseapp.com/__/auth/handler 経由で 元の URL に戻る ことで認証状態を受け取りますが、GitHub Pages のサブパス (/codh-mirror/iiif-curation-manager/) + クエリパラメータ (?lang=ja) の組み合わせだと、戻ったあとの getRedirectResult() 処理がうまく走らず、見た目はログイン完了したのに onAuthStateChanged が発火しない、という症状に見えます。

Viewer の authFirebase.js だけが signInFlow: 'popup' を明示しており (上流での Viewer 個別アップデート時に入ったものと思われる)、その差で Viewer は最初から動く・Manager/Editor/Board は動かない、という分布になっていました。

Viewer は公開 IIIF Manifest をクエリパラメータ経由で読み込めば、ログイン無しでもこのように描画が成立する (本記事では Harvard Art Museums のゴッホ自画像マニフェストを参照)

3 ファイルに同じ 1 行を追加して、ようやく全ツール揃ってログインが通るようになりました。

ログインボタンを押下した直後の Manager: FirebaseUI のプロバイダ選択が popup として in-page 展開されている (signInFlow: 'popup' を明示した効果)

切り分けに役立つ視点

似た症状で詰まったときの判断ポイント:

観察される挙動原因の当たり
OAuth 同意画面に行かずに即エラーauth/unauthorized-domain (Authorized domains 未設定)
別タブに飛ぶが永遠に戻ってこないOAuth プロバイダ側 (Google/Twitter 等) のリダイレクト URI 未登録
別タブが閉じるが、元タブの状態が変わらないsignInFlow 未指定 (=redirect 動作) + サブパス URL の組合せ ← 今回のケース
ログインボタンを押しても何も起こらないプロバイダが Firebase 側で Disabled

3. JSONkeeper エンドポイントの差し替え

保存先 https://mp.ex.nii.ac.jp/api/curation/json を、JSONkeeper 互換 API (本ミラーでは Cloudflare Workers + D1 版を本流) に向け替えます。書き換え対象は 5 ツール × curationJsonExportUrl 出現箇所で、計 9 箇所。

ファイル出現数
iiif-curation-viewer/index.js5 (各 configExample.* ブロック)
iiif-curation-manager/index.js1
iiif-curation-editor/index.js2
iiif-curation-player/index.js1
iiif-curation-board/index.js1

書き換えは sed -i 1 発で済みます (撤収時もこの逆方向に 1 発で戻せるのがミソ)。

find iiif-curation-* -name 'index.js' -exec \
  sed -i '' -E "s|https://mp\.ex\.nii\.ac\.jp/api/curation/json|<jsonkeeper-base>|g" {} +

サーバ側の二系統運用

本ミラーでは Cloudflare Workers + D1 版 を本流、PythonAnywhere 上流 Flask 版 を「塩漬けの上流互換参照実装」として並走させています。クライアント側からの切り替えは <jsonkeeper-base> を別 URL に差し替えるだけで、ロールバックも sed 1 発で完了します。

Viewer のエクスポートワークフローが使う API は POST/GET/PUT/DELETE と Location ヘッダ返却、X-Firebase-ID-Token の検証だけなので、Workers 版が未実装としている X-Unlisted / /<id>/status PATCH / Activity Stream ページネーション / GC / Range サブ URL は触りません。ここの判断根拠は Workers 版の記事に詳しく書いています。

Workers 版で Firebase Admin SDK を使わず に ID トークン検証を成立させている設計 (Google x509 公開鍵を securetoken.googleapis.com から取得・キャッシュし josejwtVerify に渡す) の実装は、Workers 記事 §4「設計判断 1: 認可 — Firebase Admin SDK は使わない」 を参照してください。サービスアカウント鍵をサーバに置かなくて済むため、本記事側の Firebase 設定が Web SDK config (公開前提) だけで完結する のはこの設計に依拠しています。

動作確認

ログイン後に Viewer で適当なキュレーションを 1 件保存し、レスポンスヘッダ Location が JSONkeeper 側で発行された URI を返してくることを確認します。

D1 (Workers 版) 側で実際に行が積まれているかは wrangler d1 execute jsonkeeper --remote --command "select id, owner_uid, created_at from documents order by created_at desc limit 5" で覗けます。

4. iiif-curation-manager の外部リンク相対化 (副次)

Manager のメニューには「このキュレーションを Viewer / Editor / Player / Board で開く」リンクがあり、これらがオリジナルの絶対 URL (http://codh.rois.ac.jp/software/iiif-curation-viewer/demo/ など) でハードコードされていました。現在エンドポイントは応答しないのでクリックすると 404 になります。

GitHub Pages 配下では同じツール群が 兄弟ディレクトリ として並んでいるので、相対 URL に書き換えるのが最も素直です。

-curationViewerUrl: 'http://codh.rois.ac.jp/software/iiif-curation-viewer/demo/',
-curationEditorUrl: 'http://codh.rois.ac.jp/software/iiif-curation-editor/demo/',
-curationPlayerUrl: 'http://codh.rois.ac.jp/software/iiif-curation-player/demo/',
-curationBoardUrl:  'http://codh.rois.ac.jp/software/iiif-curation-board/demo/'
+curationViewerUrl: '../iiif-curation-viewer/',
+curationEditorUrl: '../iiif-curation-editor/',
+curationPlayerUrl: '../iiif-curation-player/',
+curationBoardUrl:  '../iiif-curation-board/'

オリジナル URL の復旧後はここも sed 1 発で元に戻せるよう、相対パスで自己ホスト先を指す形にしています。

トラブルシューティング

実際に詰まったポイントを症状別に。

「ログインボタンは出るが押しても OAuth 同意画面に行かない」

→ Firebase Console で当該プロバイダが Disabled。§1.1 で全部 Enabled に

auth/unauthorized-domain が DevTools コンソールに出る」

→ Authorized domains 未追加。§1.1 で <gh-user>.github.io を追加

「OAuth 同意画面まで進むが、戻ってきたらログイン状態にならない」

§2 の signInFlow: 'popup' 未指定が最有力。これを明示するだけで直る。

「ログインは通るのに保存ボタンで 404 が返る」

curationJsonExportUrlmp.ex.nii.ac.jp のまま。§3 の書き換え漏れgrep -r curationJsonExportUrl iiif-curation-* で再確認。

「保存はできるが、Activity Stream を開くと 404」

→ 本ミラーは Workers 版を本流にしており、上流の Activity Stream ページネーション は未実装。表示用 collection エンドポイント (/as/collection.json) のみ実装。Viewer の通常ワークフローには影響しないが、Activity Stream を頻繁に巡回するクライアントを別途繋ぐ場合は PA 上流版に切り替える方が無難。

撤収手順

オリジナル URL への切り戻しは次の 3 ステップで行えます。

  1. クライアント側 URL の巻き戻し

    find iiif-curation-* -name 'index.js' -exec \
      sed -i '' -E "s|<jsonkeeper-base>|https://mp.ex.nii.ac.jp/api/curation/json|g" {} +
    find iiif-curation-manager -name 'index.js' -exec \
      sed -i '' -E "s|\.\./iiif-curation-(viewer\|editor\|player\|board)/|http://codh.rois.ac.jp/software/iiif-curation-\1/demo/|g" {} +
    
  2. authFirebase.jsfirebaseConfig をオリジナル (codh-81041) に戻す — オリジナルの公開ブランチからの取得が可能になったら、各 authFirebase.jsgit checkout で巻き戻し。signInFlow: 'popup' を残すかどうかは任意 (残しても上流での挙動は変わらない)。

  3. JSONkeeper サーバ側の停止 — Workers 版は wrangler delete jsonkeeper、PA 版は Web App 削除 API 1 発 (詳細は別記事)。Firebase プロジェクト kunshujo-c は別用途で残すなら触らず、専用に立てたなら Console から削除。

セキュリティ後片付け

  • Firebase Web SDK の apiKey は公開前提の識別子だが、想定外の埋め込み利用を抑止したい場合は App Check の利用が第一候補。GCP Console > API & Services > Credentials で API キーに HTTP リファラ制限を掛ける手もあるが、Firebase JS SDK の fetch 実装によっては Referer ヘッダが乗らず Auth 呼び出しが 403 で破綻する既知不具合がある (firebase-js-sdk#5657)。本ミラーは古い SDK v5 系のため直接影響は受けにくいが、リファラ制限を掛けるなら必ず staging で identitytoolkit.googleapis.com 呼び出しが通ることを検証してから本番に適用すること。
  • Firebase Authentication の API 利用ログ は Firebase Console > Authentication > Users で監査可能。撤収後に残ったユーザレコードを削除するかどうかは保存ポリシー次第。
  • Workers + D1 側は D1 の SQL 直叩き (wrangler d1 execute) で documents テーブルを delete できる。撤収時はテーブルごと drop してから wrangler delete で 1 行も残らない状態に。

おわりに

前回の記事で「動かない」と書いた認証・保存系を、後から「やっぱり動かす」に倒した記録でした。振り返ると次の 3 点が要点です。

  • 暫定ミラーの『どこまでやるか』は復旧見込みと運用負荷のバランス。上流の再開時期未定が続くと、見るだけでなく書き込みを必要とする用途が出てきて、結局サーバ側まで自前で持つほうが現実的になる。
  • FirebaseUI 3.x の signInFlow デフォルトはリダイレクト。GitHub Pages のサブパス + クエリパラメータ環境では popup 明示が事実上必須。1 行差で全部直るが、最初の切り分けは Viewer との比較で初めて気付いた。
  • クライアント側の差し替え点は最終的に 1 種類の URL だけ (curationJsonExportUrl + Firebase Web config)。これだけのために 360 行の Workers 実装と PA への HTTP API デプロイを並走させる、というのは過剰に見えるかもしれないが、撤収のしやすさ・ロールバックの容易さを考えると分割は妥当だった。

上流の早期復旧を願いつつ、再開後にこの後付けレイヤを 1 コマンドで剥がせる状態を維持していきます。