Skip to content

ユースケース — カスタムアカウント選択 UI ​

prompt=select_account には 2 つの関心事があります。

  • セッションの意味論: ブラウザが chooser group(サインイン済みの複数アカウント)を持ち、ユーザが選んだアカウントが次の sub を決める。
  • 描画面: アカウントの一覧を表示し、選ばれた SessionID を POST するページ。

マルチアカウント選択 は前者を扱います。このページは後者を扱います。ブランド付きでサーバ描画のアカウント選択画面を持ちつつ、state、CSRF、セッションの切り替えは OP に任せるには、op.HTMLUI{Chooser: ...} を使います。

ソース: examples/12-custom-chooser-ui は、組み込みの HTML ページで op.HTMLUI{Chooser: ...} を使う例です。JSON ドライバ / SPA 経路は examples/13-multi-account と対比してください。

使いどころ ​

目的使うもの
同梱 chooser をそのまま使うオプション不要。組み込みの HTML ページが描画
chooser の HTML / 文言 / レイアウトだけ変え、サーバ描画に留めるop.WithUI(op.HTMLUI{Chooser: op.PageTemplate{Template: tmpl}})
chooser を SPA の中で描画するop.WithUI(op.SPAUI{...}) または op.WithUI(op.DriverUI{Driver: interaction.JSONDriver{}})
アカウントのグループ化や切替のロジックを変えるテンプレートではなく session store / authenticator 側

Chooser テンプレートは意図的に狭い差し込み口です。差し替えるのはテンプレートだけで、テンプレートが任意の subject を選んだり、セッションを発行したり、OP の状態遷移を迂回したりすることはできません。自前の driver の上にテンプレートを重ねたい場合は、op.DriverUI へ、その driver を Inner にした interaction.TemplateOverlayDriver を渡します。

テンプレートの契約 ​

テンプレートには interaction.ChooserTemplateData が渡されます。

フィールド用途
Accountschooser group 内の有効なセッション(最後に使った順)。各要素は SessionID、Subject、DisplayName(ユーザの name claim。不明なら空)、AuthTime を持つ
StateRefそのまま返す不透明な interaction state 参照
CSRFTokenPOST 時に OP が検証するトークン
SessionIDField選択アカウント用に OP が期待するフォームフィールド名
SubmitMethod通常は POST
SubmitActioninteraction エンドポイントの URL
AddAccountURL同じ group に別のアカウントをサインインさせる /auth の URL。PAR が必須の配備では空

最小形は次のようになります。

go
tmpl := template.Must(template.New("chooser").Parse(`
{{range .Accounts}}
  <form method="{{$.SubmitMethod}}" action="{{$.SubmitAction}}">
    <input type="hidden" name="state_ref" value="{{$.StateRef}}">
    <input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
    <input type="hidden" name="{{$.SessionIDField}}" value="{{.SessionID}}">
    <button type="submit">Continue as {{if .DisplayName}}{{.DisplayName}}{{else}}{{.Subject}}{{end}}</button>
  </form>
{{end}}
{{if .AddAccountURL}}<a href="{{.AddAccountURL}}">Sign in to another account</a>{{end}}
`))

provider, err := op.New(
  /* 必須オプション */
  op.WithUI(op.HTMLUI{
    Chooser: op.PageTemplate{Template: tmpl},
  }),
)

フィールド名は OP との契約です。state_ref、csrf_token、SessionIDField が指す名前(session_id)のフィールドは、送信フォームに残してください。「アカウント追加」のリンクは、AddAccountURL が空でないときだけ表示します。RequirePAR(たとえば FAPI 2.0)の配備では、クエリ文字列だけの /auth URL は拒否されるため、OP はこのフィールドを空にします。

op.PageTemplate ごとに ContentSecurityPolicy フィールドがあるので、chooser のポリシーは同意画面のものとは別に宣言します。2 つのテンプレートが別々の origin から読み込む場合があるためです。空文字は厳格な既定値(default-src 'none'; style-src 'none'; frame-ancestors 'none'; base-uri 'none')を使い、スタイルシートや画像も読み込めません。独自のポリシーは op.New の実行時に interaction.NormalizeCSP で正規化されます。form-action は拒否され、frame-ancestors と base-uri は指定する場合は厳密に 'none' でなければなりません。欠けている保護の directive は自動で追加されます。拒否されたポリシーは、interaction.ErrCSPNotPermitted で op.New を失敗させます。テンプレートに必要なアセットの directive だけを追加し、script は無効のままにしてください。

流れ ​

chooser — リクエストから切り替えまで
prompt=select_account を伴うリクエストを受けると、OP は chooser group を読み込んで自前のテンプレートに渡します。送信された内容には CSRF トークンと state 参照が含まれ、セッションを切り替える前に両方を検証します。ブラウザユーザの手元OPgo-oidc-providerchooser テンプレート組み込み側の実装GET /authprompt=select_accountcookie が指すchooser group を読むChooserTemplateData を描画アカウント一覧および CSRFToken と StateRefPOST SubmitActionsession_id切り替え前に検証するCSRFToken · StateRefセッションを切り替えるSwitch(group, session_id)code 付きで RP へ 302
自前のテンプレートが持つのはマークアップだけです。送り返す 2 つの不透明な値が、提出された session_id を「このブラウザに今提示されたもの」だけに限定しています。

テンプレートは切り替えそのものを実行しません。選ばれたセッションの識別子を返すだけです。OP はその識別子が有効な chooser group に属することを確認し、他の group のものや期限切れの session_id は 4xx で拒否します。選ばれたセッションが、その間に期限切れになったりサインアウトされたりしていた場合、RP は login_required を受け取ります。

選ばれたセッションでリクエストを処理できない場合(リクエストに prompt=login がある、セッションが max_age より古い、acr が acr_values を満たさない)、OP はリクエストを失敗させません。そのアカウントのログイン認証要素を、もう一度実行します。

選ばれたアカウントは、戻ってきたセッションと同じ LoginFlow の rule、Decider、リスクに応じたステップアップを通ります。そのため、rule や拒否の判定は、ほかのサインインと同様に選択にも適用されます。選択によって再認証が走った場合、選ばれたセッションのレコードは新しい auth_time、acr、amr でローテーションされます。

chooser の所有者 ​

op.SPAUI を使う場合、chooser の描画は JSON の状態エンドポイントを通じて SPA が受け持ちます。op.WithUI が受け取るモードは配備ごとに 1 つなので、chooser の所有者は渡すモードで決まります。

UI の所有者op.WithUI に渡す値
OP によるサーバ描画 HTMLop.HTMLUI{Chooser: ...}
OP がマウントする SPA の入口op.SPAUI{...}
自前ルータが SPA を配信op.DriverUI{Driver: interaction.JSONDriver{}}

本番運用メモ ​

  • テンプレートは、リクエストごとではなく起動時に一度だけ parse する。
  • CSP は厳しく保つ。データには攻撃者が影響し得る表示名が入るので、html/template のエスケープに任せ、inline script を避ける。
  • SessionID は不透明な値として扱う。表示したり、他のフィールドから組み立てたりしない。
  • AddAccountURL は渡されたとおりに使う。次のサインインを既存の group に加えるための内部マーカーが付いており、手で組み立てた prompt=login の URL は、アカウントを追加せず現在のアカウントを置き換える。

続きはこちら ​