Introduction
ZITADEL をローカル環境で動作検証する際、UI にアクセスしたブラウザが /.well-known/openid-configuration
にリクエストを送るタイミングで CORS エラーが発生し、OIDC の初期化に失敗することがあります。
結論
ローカルの動作検証なので手抜きして EXTERNAL_DOMAIN
に localhost
を使っていたのを hosts ファイルで定義した別の FQDN 形式の名前に変更したところ、問題なく動作するようになりました。
発生した事象
- ZITADEL を
--tlsMode server
で 10443 ポートにて起動(rootless コンテナ運用のため) - ブラウザから
https://localhost:10443/ui/console/
にアクセス - Web UI の内部処理で
https://localhost/.well-known/openid-configuration
へリクエストが飛ぶ - 実際のポートは 10443 なので、ZITADEL に到達せず無応答(CORS エラーや 404 ではなく)
技術的背景
ZITADEL の UI(JavaScript)は environment.json
にある issuer
値を元に .well-known
の URL を組み立てています。
{
"issuer": "https://localhost:10443"
}
この値に基づいて JS 側では次のようなコードで .well-known
を参照します:
const wellKnownUrl = issuer + "/.well-known/openid-configuration";
そして実際のコード上では、loadDiscoveryDocument()
関数において、明示的に issuer を指定していない場合はこの issuer 値を使って .well-known
の URL を構成し、HTTP リクエストを送信します。
K || (
(K = this.issuer || "").endsWith("/") || (K += "/"),
K += ".well-known/openid-configuration"
);
this.http.get(K)
今回の事象では environment.json
にも issuer
にもポートが含まれているにもかかわらず、実際にブラウザが送信したリクエストではポートが省略されており、https://localhost/.well-known/openid-configuration
にアクセスしようとして失敗していました。
なお、curl による environment.json
の取得結果にも明確にポートが含まれていたことが確認できています:
$ curl -k https://auth.example.local:10443/ui/console/assets/environment.json | jq .
{
"api": "https://auth.example.local:10443",
"issuer": "https://auth.example.local:10443",
"clientid": "327587995391820388"
}
ポートがどこで消えているかについては、ZITADEL の UI バンドルされた JavaScript の中で issuer
の値が URL
オブジェクトとして解釈されたり、別の処理を経てポートが省略されている可能性はありますが、今回の検証ではその詳細までは特定に至っていません。
回避策
-
/etc/hosts
に次のように追記して:127.0.0.1 auth.example.local
-
ZITADEL の
EXTERNAL_DOMAIN
をauth.example.local
に設定 -
ブラウザから
https://auth.example.local:10443
でアクセス
これにより、issuer と実際の origin が一致し、.well-known
の取得や CORS 判定も正常に通ります。
おわりに
ZITADEL のバグというよりは、localhost
の取り扱いが特殊であることに起因する想定外の動作と思われます。
ローカル検証時にも localhost
を避けて名前付きホストを使うのが安全です。
補足:CORS とは?
CORS(Cross-Origin Resource Sharing)とは、ブラウザが異なる「オリジン」へのリクエストを制限する仕組みです。
「オリジン」とは、スキーム(http/https)・ホスト名・ポート番号の組み合わせを指し、これらがすべて一致していないと 「異なるオリジン」 とみなされます。
今回のように:
- 表示中のページが
https://localhost:10443
であるのに、 - JavaScript が
https://localhost/.well-known/...
にアクセスすると、
ポート番号の不一致によってオリジンが異なると判定され、サーバ側が CORS 許可をしていないとアクセスがブロックされます。
ZITADEL 側が .well-known
に Access-Control-Allow-Origin
ヘッダを返していれば問題になりませんが、 一般的には明示的な許可がなければ 異なるオリジン間通信は遮断されます。
さらに補足
この記事は 80% 程度 ChatGPT が書いてくれました。
いつも面倒なのでいろいろ試しても記録を残さないままになっていますが、ChatGPT とやりとりしながらした試行錯誤は「これまとめといて」でわりと使える感じのレポートができる、という点に気付いたのが今回の一番の収穫かも知れません。