ユースケース — カスタムログイン UI
同梱のサインイン画面でも動きますが、マークアップはライブラリのものです。op.HTMLUI の Login フィールドを使うと、画面を自前のテンプレートに差し替えられます。state、CSRF、認証情報の検証、RP へのリダイレクトは引き続き OP が担当します。
1 つのテンプレートが、同意・アカウントチューザ・パスキーを除くすべてのサインイン prompt を描画します。パスワード、TOTP、メール OTP、リカバリコード、factor 選択、captcha、自前の Step の prompt、op.WithInteractions で宣言した prompt が対象です。同意には HTMLUI.Consent、チューザには HTMLUI.Chooser があり、同じ op.HTMLUI に並べられます。パスキーの prompt とエラーページは、組み込みのドライバのままです。
tmpl := template.Must(template.ParseFiles("login.html"))
op.New(
/* 必須オプション */
op.WithUI(op.HTMLUI{
Login: op.PageTemplate{
Template: tmpl,
ContentSecurityPolicy: "default-src 'none'; style-src 'self'; img-src 'self' data:",
},
}),
)op.WithUI は 1 回だけ呼べ、受け取るモードも 1 つなので、op.HTMLUI は op.SPAUI と択一です。テンプレートは組み込みの HTML ページに重ねられます。自前のドライバに重ねるときは、op.DriverUI{Driver: interaction.TemplateOverlayDriver{Inner: 自前のドライバ, LoginTemplate: tmpl}} を渡します。Template を指定せずに ContentSecurityPolicy だけ指定すると op.New が失敗します。
テンプレートが受け取るもの
interaction.LoginTemplateData です。
| フィールド | 意味 |
|---|---|
Type | 描画中の prompt の種別(例: auth.password) |
Data | 組み込み Step の型付きペイロード。自前の Step と op.WithInteractions の prompt では nil |
Fields | 描画する入力欄(Name、Kind、Label、LabelText など)。OP が文言を解決できなかった場合、LabelText にはラベルキーが入ります |
FactorField | factor 選択 prompt が送信するフォームのフィールド名 |
Error | 拒否された送信に対する一度きりの通知。なければ nil |
Locale | OP が解決した BCP 47 タグ |
StateRef、CSRFToken | そのまま戻す continuation と CSRF の値 |
SubmitMethod、SubmitAction | フォームの送信先 |
テンプレートが守ること
- hidden の state を戻す。
state_refとcsrf_tokenを、この名前のまま hidden 入力として出力します。どちらかが欠けたフォームは、送信時に拒否されます。 Dataをガードする。 型付きの参照はwith .Dataブロックで囲むか、.Typeで分岐します。nil のDataのフィールドを読むと、描画が失敗します。- factor の選択を送る。 factor 選択 prompt では、
.Data.Optionsを radio 入力として並べ、選ばれた option のKindを.FactorFieldの名前で送信します。
<form method="{{.SubmitMethod}}" action="{{.SubmitAction}}">
<input type="hidden" name="state_ref" value="{{.StateRef}}">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
{{range .Fields}}
<label>{{.LabelText}} <input name="{{.Name}}" type="{{inputType .Kind}}"></label>
{{end}}
{{if eq .Type "auth.choose_factor"}}{{with .Data}}{{range .Options}}
<label><input type="radio" name="{{$.FactorField}}" value="{{.Kind}}"> {{.LabelText}}</label>
{{end}}{{end}}{{end}}
<button type="submit">Continue</button>
</form>inputType はテンプレートの parse 時に template.FuncMap で渡す関数で、interaction.FieldPassword を password、interaction.FieldEmail を email、interaction.FieldHidden を hidden、それ以外を text に対応させます。テンプレートからはこれらの定数と直接比較できないためです。定義は examples/37-custom-login-ui にあります。
自前の driver を書く場合
自前の driver(op.DriverUI)は interaction.Prompt を受け取り、描画を自分で行います。拒否された送信のあとに再表示される prompt は Prompt.Error(SPA の通信路上では error)を持ち、その中身は Code と Message を持つ interaction.PromptError です。interaction.PromptErrorCredentialsRejected は認証情報が拒否されたときの code で、アカウントの有無にかかわらず同じ値になります。Message は Prompt.Locale での code の文言で、OP にロケール解決器がない場合は空になるため、その場合は Code で代用してください。未知の code は汎用の失敗として扱います。interaction.PromptErrorMessageKey(code) は code の i18n カタログキーを返し、閉じた集合の外の code では空文字列を返します。この通知は一度きりで、新しいリクエストから作られた prompt には付きません。
factor 選択 prompt の Type は interaction.PromptTypeChooseFactor、Data は interaction.FactorChoicePromptData です。Options を描画し、選ばれた option の Kind をフォームのフィールド interaction.FactorChoiceField(factor)で送信します。interaction.FactorOption は Label キーと、prompt のロケールで解決された LabelText を持ちます。FieldSpec.LabelText も同様です。
すべてのログインページで共通の CSP
Login.ContentSecurityPolicy の規則は PageTemplate.ContentSecurityPolicy と同じです。空ならライブラリの既定値で、サブリソースはすべてブロックされます。空でない値は op.New の実行時に検証されます。
このポリシーはすべてのログイン prompt で共通です。captcha のウィジェットを読み込むために script-src を緩めると、パスワードや TOTP のページも緩みます。指定するのはホストだけにし、'unsafe-inline' は使わないでください。ページには CSRF トークンと continuation の state が載っているためです。
動かしてみる
examples/37-custom-login-ui は、パスワードログインを自前のテンプレートで描画します。スタイルシートは example 自身が配信するので、CSP を style-src 'self' にできます。
次に読む
- カスタム同意 UI — 同意画面についての同じパターンです。
- SPA / カスタムインタラクション — フロントエンドがすべての画面を担う場合です。