development RPをstaging IdPへ向けるのをやめた — OAuthの環境境界は直交する
勤怠アプリのproductionとdevelopmentには、別々のOAuth client setが必要だった。最初は名前をそろえて、development RPをstagingのOpenID Providerへ向けていた。しかしSign in with Appleの設定まで進めると、RPの環境分離と認証基盤の環境分離は同じ境界ではないと分かった。client setは分けたまま、どちらもproductionの認証基盤へ集約した。
development RPをstaging OPへ向けると、変更周期が結合した
当初の対応は単純だった。production RPはproductionのHashirah、development RPはstagingのHashirahを使う。環境名がそろうので、構成として自然に見える。
ところが、この対応は別々に動くものを結合していた。RPのenvironmentが分けたいのは、callback、tokenのaudience、Web client secret、アプリ側のデータだ。一方、Hashirahのstagingが試したいのは、認証基盤そのものの新しいruntimeやmigrationである。development RPをstaging OPへ固定すると、アプリを開発したいだけなのに、認証基盤の実験中の変更まで受ける。
Sign in with Appleでは、この結合が外部設定にも広がった。staging OPを通常のdevelopment RPで使い続けるなら、Services ID、return URL、署名鍵をstaging用にも持つ必要がある。Google credentialも同じだ。downstream clientを分離したいだけなのに、upstream IdPの登録と鍵管理まで複製される。
そこで「development RPはstaging OPへ向け続ける」案を捨てた。staging OPはHashirah自身を試す場所として残し、通常のアプリ開発ではproduction OPを使うことにした。
client environmentとHashirah targetを別の値にした
実装では、曖昧だった environment を二つの値に分けた。 clientEnvironment はdownstream RPの論理環境、 hashirahEnvironment は書き込み先の物理環境を表す。development clientのtargetもproductionになる。
type ProvisioningTarget = {
clientEnvironment: "production" | "development";
hashirahEnvironment: "production" | "development";
};
const targets = {
production: {
clientEnvironment: "production",
hashirahEnvironment: "production",
},
development: {
clientEnvironment: "development",
hashirahEnvironment: "production",
},
};これはproductionとdevelopmentを統合したわけではない。manifest、opaqueなclient ID、callback、audienceは別のままだ。変えたのは、それらをどのOPが発行・検証するかという配置だけである。
provisioning CLIのdry-runは、論理環境、物理target、issuerを別々に報告する。書き込み時には --target を必須にし、development clientをdevelopment D1へapplyする指定は、外部resourceやD1へ触れる前に拒否するようにした。名前が同じだから同じ環境へ書く、という古い操作を無言で通さないためだ。
issuerは共有し、callbackとaudienceは分けた
両client setが共有するのは、issuer https://auth.hashirah.com、production D1、upstreamのApple Services IDとreturn URLと署名鍵、Google credentialである。Appleから見えるRPはHashirah なので、downstreamに環境が複数あってもupstream登録まで増やす必要はない。
分離を維持したのは、各platformのclient ID、callbackとlogout callback、API audience、Web secret、themeだ。たとえばdevelopmentのWeb callbackは https://app-dev.example.com/auth/callbackのようにproductionとは別に固定する。Web clientだけはconfidentialで、平文secretも環境ごとに別管理する。native clientはpublicのまま、secretを持たせない。
「環境を分ける」は、すべてを複製することではない。脅威境界と変更理由が違う値は分け、同じ主体を表すupstream設定は共有する。
新しいsetを先に検証し、旧staging setはあとで退役した
移行は、既存setのissuerを書き換える操作にはしなかった。development manifestをproduction targetへdry-runし、新しいsetとしてapplyする。再applyがunchangedになることを確かめたあと、D1をread-backした。production D1にはactive setが2件、memberが合計6件あり、client IDはすべて一意だった。既存production setのIDとmanifest digestも変わっていなかった。
旧staging setは同時に消していない。OAuth requestとconsent grantが空であることを先に確認し、legacy targetを明示したrollbackだけで削除した。rollbackの原子性やreservationの仕組みは別の記事で扱ったため、ここでの要点は「新しい配置の検証」と「古い配置の退役」を別の操作にしたことだ。
deploy後、両client setのauthorizeがproduction Hashirahのloginへredirectし、Apple startがAppleのauthorizeへ進むこと、両Hashirah環境のhealthを確認した。Apple token endpointには短命client secretと架空codeを送り、credential不正の invalid_client ではなくcode不正の invalid_grant が返るところまで確認できた。
一方、Appleアカウントを使う対話的なcallbackは未実施である。勤怠アプリ側のcallback受け口もまだdeployしていないため、end-to-endのcallbackも確認していない。upstream credentialとOPの開始経路が動いたことと、ユーザーが最後までログインできたことは分けて記録した。
まとめ
productionやdevelopmentというラベルは、システム全体で単一の軸ではない。downstream RPの論理環境と、OPを動かす物理環境は直交している。ここを機械的に対応させると、アプリの分離に不要なupstream credentialと運用経路まで増える。
今回はclient setの境界を残し、issuerとupstream IdP設定だけを共有した。分離したい値と共有したい値をmanifestとCLIの型に落とし、read-backで両方を検証できれば、「devだからstaging」という名前合わせから離れても境界は弱くならない。