auth-ui: rewrite ui

Change-Id: I6f00867015ec77aa7e336e89da4dc1b081e330c6
diff --git a/core/auth/ui/main_test.go b/core/auth/ui/main_test.go
new file mode 100644
index 0000000..904bdd4
--- /dev/null
+++ b/core/auth/ui/main_test.go
@@ -0,0 +1,1191 @@
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"html"
+	"html/template"
+	"io/fs"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"reflect"
+	"regexp"
+	"strings"
+	"sync/atomic"
+	"testing"
+)
+
+const (
+	sanitizedRetryFlowFixture = `{
+		"id":"retry-flow",
+		"obsolete_state":"obsolete-upstream-state",
+		"ui":{
+			"action":"https://upstream.invalid/do-not-render",
+			"nodes":[
+				{"attributes":{"name":"traits.username","value":"upstream-value-must-not-render"}},
+				{"attributes":{"name":"csrf_token","value":"retry-csrf"}}
+			],
+			"messages":[{"id":4000007,"text":"upstream prose must not render"}]
+		}
+	}`
+	sanitizedOtherRetryFlowFixture = `{
+		"id":"other-flow",
+		"obsolete_state":"obsolete-upstream-state",
+		"ui":{"action":"https://upstream.invalid/other-action","nodes":[{"attributes":{"name":"csrf_token","value":"other-csrf"}}],"messages":[{"id":1234,"text":"unsafe upstream guidance"}]}
+	}`
+	sanitizedExpiredFlowFixture = `{"error":{"id":"self_service_flow_expired","message":"unsafe expiry prose"}}`
+	sanitizedCSRFFixture        = `{"error":{"id":"security_csrf_violation","message":"unsafe CSRF prose"}}`
+
+	obsoleteFlowSentinel      = "obsolete-submitted-flow"
+	obsoleteCSRFSentinel      = "obsolete-submitted-csrf"
+	submittedPasswordSentinel = "Submitted-Password-Sentinel-9!"
+)
+
+func TestStage3LocalAssetAndPaletteContracts(t *testing.T) {
+	baseTemplate, err := fs.ReadFile(tmpls, "templates/base.html")
+	if err != nil {
+		t.Fatal(err)
+	}
+	markup := string(baseTemplate)
+	for _, expected := range []string{`href="/static/base.css?v=0.0.1"`, `href="/static/main.css?v=0.0.3"`} {
+		if strings.Count(markup, expected) != 1 {
+			t.Fatalf("base template local stylesheet %q count=%d, want 1", expected, strings.Count(markup, expected))
+		}
+	}
+	for _, forbidden := range []string{"pico", "cdnjs", "<script", "http://", "https://"} {
+		if strings.Contains(strings.ToLower(markup), forbidden) {
+			t.Fatalf("base template contains forbidden asset reference %q", forbidden)
+		}
+	}
+	if links := strings.Count(markup, `<link rel="stylesheet"`); links != 2 {
+		t.Fatalf("stylesheet link count=%d, want 2", links)
+	}
+
+	baseCSS, err := fs.ReadFile(static, "static/base.css")
+	if err != nil {
+		t.Fatal(err)
+	}
+	mainCSS, err := fs.ReadFile(static, "static/main.css")
+	if err != nil {
+		t.Fatal(err)
+	}
+	allCSS := string(baseCSS) + "\n" + string(mainCSS)
+	opaqueColor := regexp.MustCompile(`(?i)#[0-9a-f]{3,8}\b|rgba?\(`)
+	colors := opaqueColor.FindAllString(allCSS, -1)
+	wantColors := []string{"#d6d6d6", "#3a3a3a", "#7f9f7f", "#d4888d"}
+	if !reflect.DeepEqual(colors, wantColors) {
+		t.Fatalf("opaque CSS colors=%v, want exact palette once in base.css", colors)
+	}
+	for _, forbidden := range []string{"--pico-", "box-shadow", "@font-face", "url("} {
+		if strings.Contains(strings.ToLower(allCSS), forbidden) {
+			t.Fatalf("CSS contains forbidden presentation primitive %q", forbidden)
+		}
+	}
+	for _, primitive := range []string{"box-sizing: border-box", "border-radius: 0", "min-height: 44px", ":focus-visible", "overflow-wrap: anywhere", "prefers-reduced-motion", "--font-mono"} {
+		if !strings.Contains(string(baseCSS), primitive) {
+			t.Fatalf("base.css omitted owned primitive %q", primitive)
+		}
+	}
+	for _, presentation := range []string{"width: min(100%, 500px)", "min-height: 100vh", "min-height: 100dvh", "align-items: flex-start", "input:-webkit-autofill"} {
+		if !strings.Contains(string(mainCSS), presentation) {
+			t.Fatalf("main.css omitted auth presentation contract %q", presentation)
+		}
+	}
+	removed := []struct {
+		filesystem fs.FS
+		path       string
+	}{
+		{static, "static/" + "pico.2.0.6.min.css"},
+		{tmpls, "templates/" + "consent.html"},
+	}
+	for _, asset := range removed {
+		if _, err := fs.Stat(asset.filesystem, asset.path); err == nil || !errors.Is(err, fs.ErrNotExist) {
+			t.Fatalf("removed embedded asset %q still exists", asset.path)
+		}
+	}
+	if _, present := reflect.TypeOf(Templates{}).FieldByName("Consent"); present {
+		t.Fatal("Templates retains dormant consent storage")
+	}
+}
+
+func testTemplates(t *testing.T) *Templates {
+	t.Helper()
+	templates, err := ParseTemplates(tmpls)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return templates
+}
+
+func withKratosServer(t *testing.T, handler http.Handler) *httptest.Server {
+	t.Helper()
+	server := httptest.NewServer(handler)
+	old := *kratos
+	*kratos = server.URL
+	t.Cleanup(func() {
+		*kratos = old
+		server.Close()
+	})
+	return server
+}
+
+func testServer(t *testing.T, kratosURL string) *Server {
+	t.Helper()
+	return NewServer(0, kratosURL, nil, testTemplates(t), true, nil, "")
+}
+
+func formRequest(method, target string, values url.Values) *http.Request {
+	request := httptest.NewRequest(method, target, strings.NewReader(values.Encode()))
+	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	return request
+}
+
+func setJSONResponse(w http.ResponseWriter, status int, body string) {
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(status)
+	_, _ = w.Write([]byte(body))
+}
+
+func validFlowFixture(id, csrf string) string {
+	body, _ := json.Marshal(map[string]any{
+		"id": id,
+		"ui": map[string]any{"nodes": []any{
+			map[string]any{"attributes": map[string]any{"name": "identifier", "value": ""}},
+			map[string]any{"attributes": map[string]any{"name": "csrf_token", "value": csrf}},
+		}},
+	})
+	return string(body)
+}
+
+func whoAmIFixture() string {
+	return `{"identity":{"id":"identity-id","traits":{"username":"tester"}}}`
+}
+
+func TestParseRetryFlowPinnedFixtures(t *testing.T) {
+	flow, csrf, duplicate, err := parseRetryFlow([]byte(sanitizedRetryFlowFixture))
+	if err != nil || flow != "retry-flow" || csrf != "retry-csrf" || !duplicate {
+		t.Fatalf("duplicate fixture parsed as flow=%q csrf=%q duplicate=%v err=%v", flow, csrf, duplicate, err)
+	}
+	flow, csrf, duplicate, err = parseRetryFlow([]byte(sanitizedOtherRetryFlowFixture))
+	if err != nil || flow != "other-flow" || csrf != "other-csrf" || duplicate {
+		t.Fatalf("other fixture parsed as flow=%q csrf=%q duplicate=%v err=%v", flow, csrf, duplicate, err)
+	}
+	if got := parseOryErrorID([]byte(sanitizedExpiredFlowFixture)); got != "self_service_flow_expired" {
+		t.Fatalf("expired error id = %q", got)
+	}
+	if got := parseOryErrorID([]byte(sanitizedCSRFFixture)); got != "security_csrf_violation" {
+		t.Fatalf("CSRF error id = %q", got)
+	}
+}
+
+func TestParseRetryFlowRejectsMalformedMinimumState(t *testing.T) {
+	tests := []string{
+		`not-json`,
+		`{"id":"","ui":{"nodes":[{"attributes":{"name":"csrf_token","value":"token"}}]}}`,
+		`{"id":"flow","ui":{"nodes":[]}}`,
+		`{"id":"flow","ui":{"nodes":[{"attributes":{"name":"csrf_token","value":""}}]}}`,
+		`{"id":"flow","ui":{"nodes":[{"attributes":{"name":"csrf_token","value":"one"}},{"attributes":{"name":"csrf_token","value":"two"}}]}}`,
+	}
+	for _, fixture := range tests {
+		if _, _, _, err := parseRetryFlow([]byte(fixture)); err == nil {
+			t.Fatalf("malformed fixture accepted: %s", fixture)
+		}
+	}
+}
+
+func TestAuthNoticeAllowlistAndConsumption(t *testing.T) {
+	invalid := httptest.NewRecorder()
+	setAuthNotice(invalid, "arbitrary")
+	if len(invalid.Result().Cookies()) != 0 {
+		t.Fatal("arbitrary notice code was set")
+	}
+
+	set := httptest.NewRecorder()
+	setAuthNotice(set, authNoticeLoginInvalid)
+	cookies := set.Result().Cookies()
+	if len(cookies) != 1 || cookies[0].Value != authNoticeLoginInvalid || cookies[0].MaxAge != 120 || !cookies[0].HttpOnly {
+		t.Fatalf("notice cookie = %#v", cookies)
+	}
+
+	request := httptest.NewRequest(http.MethodGet, "/login?flow=valid", nil)
+	request.AddCookie(cookies[0])
+	if code, clear := pendingAuthNotice(request, authNoticeLoginInvalid); code != authNoticeLoginInvalid || !clear {
+		t.Fatalf("pending matching notice code=%q clear=%v", code, clear)
+	}
+
+	mismatchRequest := httptest.NewRequest(http.MethodGet, "/register?flow=valid", nil)
+	mismatchRequest.AddCookie(cookies[0])
+	if code, clear := pendingAuthNotice(mismatchRequest, authNoticeFlowExpired); code != "" || clear {
+		t.Fatalf("valid nonmatching notice code=%q clear=%v", code, clear)
+	}
+
+	arbitraryRequest := httptest.NewRequest(http.MethodGet, "/login?flow=valid", nil)
+	arbitraryRequest.AddCookie(&http.Cookie{Name: authNoticeCookieName, Value: "arbitrary-client-value"})
+	if code, clear := pendingAuthNotice(arbitraryRequest, authNoticeLoginInvalid, authNoticeFlowExpired); code != "" || !clear {
+		t.Fatalf("arbitrary notice code=%q clear=%v", code, clear)
+	}
+}
+
+func responseCookieNamed(response *http.Response, name string) *http.Cookie {
+	for _, cookie := range response.Cookies() {
+		if cookie.Name == name {
+			return cookie
+		}
+	}
+	return nil
+}
+
+func TestValidFormClearsArbitraryClientNotice(t *testing.T) {
+	upstream := withKratosServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		setJSONResponse(w, http.StatusOK, validFlowFixture("valid-flow", "valid-csrf"))
+	}))
+	server := testServer(t, upstream.URL)
+	request := httptest.NewRequest(http.MethodGet, "/login?flow=valid-flow", nil)
+	request.AddCookie(&http.Cookie{Name: authNoticeCookieName, Value: "arbitrary-client-value"})
+	recorder := httptest.NewRecorder()
+	server.loginInitiate(recorder, request)
+	if recorder.Code != http.StatusOK {
+		t.Fatalf("status = %d, want 200", recorder.Code)
+	}
+	cleared := responseCookieNamed(recorder.Result(), authNoticeCookieName)
+	if cleared == nil || cleared.MaxAge != -1 {
+		t.Fatalf("arbitrary notice was not cleared after valid form render: %#v", cleared)
+	}
+}
+
+func TestMatchingNoticeNotConsumedWithoutValidForm(t *testing.T) {
+	tests := []struct {
+		name       string
+		upstream   http.Handler
+		breakLogin bool
+		status     int
+	}{
+		{
+			name: "flow fetch failure",
+			upstream: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				setJSONResponse(w, http.StatusInternalServerError, `{"error":"unavailable"}`)
+			}),
+			status: http.StatusBadGateway,
+		},
+		{
+			name: "template execution failure",
+			upstream: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				setJSONResponse(w, http.StatusOK, validFlowFixture("valid-flow", "valid-csrf"))
+			}),
+			breakLogin: true,
+			status:     http.StatusInternalServerError,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			upstream := withKratosServer(t, tt.upstream)
+			server := testServer(t, upstream.URL)
+			if tt.breakLogin {
+				server.tmpls.Login = template.Must(template.New("broken-login").Funcs(template.FuncMap{
+					"fail": func() (string, error) { return "", http.ErrAbortHandler },
+				}).Parse(`{{fail}}`))
+			}
+			request := httptest.NewRequest(http.MethodGet, "/login?flow=valid-flow", nil)
+			request.AddCookie(&http.Cookie{Name: authNoticeCookieName, Value: authNoticeFlowExpired})
+			recorder := httptest.NewRecorder()
+			server.loginInitiate(recorder, request)
+			if recorder.Code != tt.status {
+				t.Fatalf("status = %d, want %d", recorder.Code, tt.status)
+			}
+			if cookie := responseCookieNamed(recorder.Result(), authNoticeCookieName); cookie != nil {
+				t.Fatalf("matching notice was consumed without a valid form: %#v", cookie)
+			}
+		})
+	}
+}
+
+func TestRegistrationLocalValidationUsesSharedErrorsWithoutUpstream(t *testing.T) {
+	var calls atomic.Int32
+	upstream := withKratosServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		calls.Add(1)
+		http.Error(w, "unexpected", http.StatusInternalServerError)
+	}))
+	server := testServer(t, upstream.URL)
+	recorder := httptest.NewRecorder()
+	request := formRequest(http.MethodPost, "/register?flow=current-flow", url.Values{
+		"csrf_token": {"current-csrf"}, "username": {"ab"}, "password": {"short-secret"},
+	})
+	server.register(recorder, request)
+	if recorder.Code != http.StatusUnprocessableEntity {
+		t.Fatalf("status = %d, want 422", recorder.Code)
+	}
+	body := html.UnescapeString(recorder.Body.String())
+	for _, expected := range []string{testUsernameLengthMessage, testPasswordLengthMessage, testPasswordCompositionMessage, `value="ab"`, `action="/register?flow=current-flow"`, `value="current-csrf"`} {
+		if !strings.Contains(body, expected) {
+			t.Fatalf("response omitted %q", expected)
+		}
+	}
+	if strings.Contains(body, "short-secret") {
+		t.Fatal("rejected registration password was rendered")
+	}
+	if calls.Load() != 0 {
+		t.Fatalf("local validation made %d upstream calls", calls.Load())
+	}
+}
+
+func TestSettingsLocalValidationUsesSharedErrorsWithoutSubmission(t *testing.T) {
+	var submissions atomic.Int32
+	upstream := withKratosServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/sessions/whoami" {
+			setJSONResponse(w, http.StatusOK, whoAmIFixture())
+			return
+		}
+		if r.Method == http.MethodPost {
+			submissions.Add(1)
+		}
+		http.Error(w, "unexpected", http.StatusInternalServerError)
+	}))
+	server := testServer(t, upstream.URL)
+	recorder := httptest.NewRecorder()
+	request := formRequest(http.MethodPost, "/settings?flow=current-flow", url.Values{
+		"csrf_token": {"current-csrf"}, "password": {"short-secret"},
+	})
+	server.changePassword(recorder, request)
+	if recorder.Code != http.StatusUnprocessableEntity {
+		t.Fatalf("status = %d, want 422", recorder.Code)
+	}
+	body := html.UnescapeString(recorder.Body.String())
+	for _, expected := range []string{testPasswordLengthMessage, testPasswordCompositionMessage, `action="/settings?flow=current-flow"`, `value="current-csrf"`} {
+		if !strings.Contains(body, expected) {
+			t.Fatalf("response omitted %q", expected)
+		}
+	}
+	if strings.Contains(body, "short-secret") {
+		t.Fatal("rejected settings password was rendered")
+	}
+	if submissions.Load() != 0 {
+		t.Fatalf("local validation made %d settings submissions", submissions.Load())
+	}
+}
+
+func TestRegistrationExpectedRejectionsUseReturnedRetryState(t *testing.T) {
+	tests := []struct {
+		name    string
+		fixture string
+		status  int
+		message string
+		flow    string
+		csrf    string
+	}{
+		{"duplicate", sanitizedRetryFlowFixture, http.StatusConflict, duplicateRegistrationMessage, "retry-flow", "retry-csrf"},
+		{"other", sanitizedOtherRetryFlowFixture, http.StatusUnprocessableEntity, registrationRejectedMessage, "other-flow", "other-csrf"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			upstream := withKratosServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				setJSONResponse(w, http.StatusBadRequest, tt.fixture)
+			}))
+			server := testServer(t, upstream.URL)
+			recorder := httptest.NewRecorder()
+			request := formRequest(http.MethodPost, "/register?flow="+obsoleteFlowSentinel, url.Values{
+				"csrf_token": {obsoleteCSRFSentinel}, "username": {"retained-user"}, "password": {submittedPasswordSentinel},
+			})
+			server.register(recorder, request)
+			if recorder.Code != tt.status {
+				t.Fatalf("status = %d, want %d", recorder.Code, tt.status)
+			}
+			body := recorder.Body.String()
+			for _, expected := range []string{tt.message, `action="/register?flow=` + tt.flow + `"`, `value="` + tt.csrf + `"`, `value="retained-user"`} {
+				if !strings.Contains(body, expected) {
+					t.Fatalf("response omitted %q", expected)
+				}
+			}
+			for _, forbidden := range []string{submittedPasswordSentinel, obsoleteFlowSentinel, obsoleteCSRFSentinel, "https://upstream.invalid/do-not-render", "https://upstream.invalid/other-action", "obsolete-upstream-state", "upstream prose", "upstream-value", "unsafe upstream"} {
+				if strings.Contains(body, forbidden) {
+					t.Fatalf("response rendered forbidden upstream/password text %q", forbidden)
+				}
+			}
+		})
+	}
+}
+
+func TestSettingsExpectedRejectionUsesReturnedRetryState(t *testing.T) {
+	upstream := withKratosServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		switch r.URL.Path {
+		case "/sessions/whoami":
+			setJSONResponse(w, http.StatusOK, whoAmIFixture())
+		case "/self-service/settings":
+			setJSONResponse(w, http.StatusBadRequest, sanitizedOtherRetryFlowFixture)
+		default:
+			http.NotFound(w, r)
+		}
+	}))
+	server := testServer(t, upstream.URL)
+	recorder := httptest.NewRecorder()
+	request := formRequest(http.MethodPost, "/settings?flow="+obsoleteFlowSentinel, url.Values{
+		"csrf_token": {obsoleteCSRFSentinel}, "password": {submittedPasswordSentinel},
+	})
+	server.changePassword(recorder, request)
+	if recorder.Code != http.StatusUnprocessableEntity {
+		t.Fatalf("status = %d, want 422", recorder.Code)
+	}
+	body := recorder.Body.String()
+	for _, expected := range []string{passwordChangeRejectedMessage, `action="/settings?flow=other-flow"`, `value="other-csrf"`} {
+		if !strings.Contains(body, expected) {
+			t.Fatalf("response omitted %q", expected)
+		}
+	}
+	for _, forbidden := range []string{submittedPasswordSentinel, obsoleteFlowSentinel, obsoleteCSRFSentinel, "https://upstream.invalid/other-action", "obsolete-upstream-state", "unsafe upstream"} {
+		if strings.Contains(body, forbidden) {
+			t.Fatalf("settings rejection rendered forbidden state %q", forbidden)
+		}
+	}
+}
+
+func TestSettingsResponseCookiesAreNeverForwarded(t *testing.T) {
+	const sentinelCookieName = "kratos_settings_sentinel"
+	tests := []struct {
+		name       string
+		status     int
+		fixture    string
+		wantStatus int
+		wantNotice bool
+	}{
+		{"success", http.StatusOK, `{}`, http.StatusOK, false},
+		{"expected rejection", http.StatusBadRequest, sanitizedOtherRetryFlowFixture, http.StatusUnprocessableEntity, false},
+		{"unexpected failure", http.StatusInternalServerError, `{"obsolete_state":"obsolete-upstream-state","ui":{"action":"https://upstream.invalid/generic-action"}}`, http.StatusBadGateway, false},
+		{"expiry", http.StatusGone, sanitizedExpiredFlowFixture, http.StatusSeeOther, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			upstream := withKratosServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				switch r.URL.Path {
+				case "/sessions/whoami":
+					setJSONResponse(w, http.StatusOK, whoAmIFixture())
+				case "/self-service/settings":
+					http.SetCookie(w, &http.Cookie{Name: sentinelCookieName, Value: "must-not-forward", Path: "/"})
+					setJSONResponse(w, tt.status, tt.fixture)
+				default:
+					http.NotFound(w, r)
+				}
+			}))
+			server := testServer(t, upstream.URL)
+			recorder := httptest.NewRecorder()
+			request := formRequest(http.MethodPost, "/settings?flow="+obsoleteFlowSentinel, url.Values{
+				"csrf_token": {obsoleteCSRFSentinel}, "password": {submittedPasswordSentinel},
+			})
+			server.changePassword(recorder, request)
+			if recorder.Code != tt.wantStatus {
+				t.Fatalf("status = %d, want %d", recorder.Code, tt.wantStatus)
+			}
+			response := recorder.Result()
+			responseCookies := response.Cookies()
+			if cookie := responseCookieNamed(response, sentinelCookieName); cookie != nil {
+				t.Fatalf("Kratos settings cookie was forwarded: %#v", cookie)
+			}
+			notice := responseCookieNamed(response, authNoticeCookieName)
+			if tt.wantNotice {
+				if len(responseCookies) != 1 || notice == nil || notice.Value != authNoticeFlowExpired {
+					t.Fatalf("expiry response cookies = %#v", responseCookies)
+				}
+			} else if len(responseCookies) != 0 {
+				t.Fatalf("unexpected settings response cookies = %#v", responseCookies)
+			}
+			body := recorder.Body.String()
+			for _, forbidden := range []string{submittedPasswordSentinel, obsoleteFlowSentinel, obsoleteCSRFSentinel, "https://upstream.invalid/generic-action", "obsolete-upstream-state"} {
+				if strings.Contains(body, forbidden) {
+					t.Fatalf("settings output rendered forbidden state %q", forbidden)
+				}
+			}
+		})
+	}
+}
+
+func TestRegistrationExpiredAndCSRFSubmissionsRestartWithNotice(t *testing.T) {
+	tests := []struct {
+		status  int
+		fixture string
+	}{{http.StatusGone, sanitizedExpiredFlowFixture}, {http.StatusForbidden, sanitizedCSRFFixture}}
+	for _, tt := range tests {
+		t.Run(http.StatusText(tt.status), func(t *testing.T) {
+			upstream := withKratosServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				setJSONResponse(w, tt.status, tt.fixture)
+			}))
+			server := testServer(t, upstream.URL)
+			recorder := httptest.NewRecorder()
+			request := formRequest(http.MethodPost, "/register?flow=old", url.Values{
+				"csrf_token": {"old"}, "username": {"valid-user"}, "password": {"Correct-Horse-Battery-9!"},
+			})
+			server.register(recorder, request)
+			if recorder.Code != http.StatusSeeOther || recorder.Header().Get("Location") != upstream.URL+"/self-service/registration/browser" {
+				t.Fatalf("restart response status=%d location=%q", recorder.Code, recorder.Header().Get("Location"))
+			}
+			cookies := recorder.Result().Cookies()
+			if len(cookies) != 1 || cookies[0].Name != authNoticeCookieName || cookies[0].Value != authNoticeFlowExpired {
+				t.Fatalf("restart notice cookies = %#v", cookies)
+			}
+		})
+	}
+}
+
+func TestMalformedRetryStateAndUnexpectedFailureRenderGeneric502(t *testing.T) {
+	tests := []struct {
+		name    string
+		status  int
+		fixture string
+	}{
+		{"malformed expected rejection", http.StatusBadRequest, `{"id":"flow","obsolete_state":"obsolete-upstream-state","ui":{"action":"https://upstream.invalid/generic-action","nodes":[]},"unsafe":"do not render"}`},
+		{"unexpected status", http.StatusInternalServerError, `{"error":"private upstream failure"}`},
+		{"wrong gone id", http.StatusGone, `{"error":{"id":"other","message":"private upstream failure"}}`},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			upstream := withKratosServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				setJSONResponse(w, tt.status, tt.fixture)
+			}))
+			server := testServer(t, upstream.URL)
+			recorder := httptest.NewRecorder()
+			request := formRequest(http.MethodPost, "/register?flow="+obsoleteFlowSentinel, url.Values{
+				"csrf_token": {obsoleteCSRFSentinel}, "username": {"valid-user"}, "password": {submittedPasswordSentinel},
+			})
+			server.register(recorder, request)
+			if recorder.Code != http.StatusBadGateway {
+				t.Fatalf("status = %d, want 502", recorder.Code)
+			}
+			body := recorder.Body.String()
+			for _, expected := range []string{"Registration unavailable", authenticationUnavailableMessage, `href="/register"`, "Try registration again"} {
+				if !strings.Contains(body, expected) {
+					t.Fatalf("generic page omitted %q", expected)
+				}
+			}
+			for _, forbidden := range []string{submittedPasswordSentinel, obsoleteFlowSentinel, obsoleteCSRFSentinel, "https://upstream.invalid/generic-action", "obsolete-upstream-state", "private upstream", "do not render"} {
+				if strings.Contains(body, forbidden) {
+					t.Fatalf("generic page rendered forbidden state %q", forbidden)
+				}
+			}
+		})
+	}
+}
+
+func TestSettingsExpiredAndCSRFSubmissionsRestartMatchingFlow(t *testing.T) {
+	tests := []struct {
+		status  int
+		fixture string
+	}{{http.StatusGone, sanitizedExpiredFlowFixture}, {http.StatusForbidden, sanitizedCSRFFixture}}
+	for _, tt := range tests {
+		t.Run(http.StatusText(tt.status), func(t *testing.T) {
+			upstream := withKratosServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				switch r.URL.Path {
+				case "/sessions/whoami":
+					setJSONResponse(w, http.StatusOK, whoAmIFixture())
+				case "/self-service/settings":
+					setJSONResponse(w, tt.status, tt.fixture)
+				case "/self-service/settings/flows":
+					setJSONResponse(w, http.StatusOK, validFlowFixture("fresh-settings-flow", "fresh-settings-csrf"))
+				default:
+					http.NotFound(w, r)
+				}
+			}))
+			server := testServer(t, upstream.URL)
+			recorder := httptest.NewRecorder()
+			request := formRequest(http.MethodPost, "/settings?flow=old", url.Values{
+				"csrf_token": {"old"}, "password": {"Correct-Horse-Battery-9!"},
+			})
+			server.changePassword(recorder, request)
+			if recorder.Code != http.StatusSeeOther || recorder.Header().Get("Location") != upstream.URL+"/self-service/settings/browser" {
+				t.Fatalf("restart response status=%d location=%q", recorder.Code, recorder.Header().Get("Location"))
+			}
+			cookies := recorder.Result().Cookies()
+			if len(cookies) != 1 || cookies[0].Value != authNoticeFlowExpired {
+				t.Fatalf("restart notice cookies = %#v", cookies)
+			}
+
+			freshRequest := httptest.NewRequest(http.MethodGet, "/settings?flow=fresh-settings-flow", nil)
+			freshRequest.AddCookie(cookies[0])
+			fresh := httptest.NewRecorder()
+			server.changePasswordForm(fresh, freshRequest)
+			if fresh.Code != http.StatusOK {
+				t.Fatalf("fresh settings GET status=%d, want 200", fresh.Code)
+			}
+			body := fresh.Body.String()
+			for _, expected := range []string{expiredFlowMessage, `action="/settings?flow=fresh-settings-flow"`, `value="fresh-settings-csrf"`} {
+				if !strings.Contains(body, expected) {
+					t.Fatalf("fresh settings form omitted %q", expected)
+				}
+			}
+			cleared := responseCookieNamed(fresh.Result(), authNoticeCookieName)
+			if cleared == nil || cleared.MaxAge != -1 {
+				t.Fatalf("fresh settings form did not clear notice: %#v", cleared)
+			}
+		})
+	}
+}
+
+func TestDependencyErrorContextContracts(t *testing.T) {
+	tests := []struct {
+		name         string
+		context      string
+		registration bool
+		title        string
+		href         string
+		link         string
+	}{
+		{"login", "login", true, "Authentication unavailable", "/login", "Try signing in again"},
+		{"registration", "registration", true, "Registration unavailable", "/register", "Try registration again"},
+		{"disabled registration", "registration", false, "Registration unavailable", "/login", "Go to sign in"},
+		{"account", "account", true, "Account unavailable", "/", "Back to account"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			server := NewServer(0, "http://kratos.invalid", nil, testTemplates(t), tt.registration, nil, "")
+			recorder := httptest.NewRecorder()
+			server.renderDependencyError(recorder, tt.context)
+			if recorder.Code != http.StatusBadGateway {
+				t.Fatalf("status = %d, want 502", recorder.Code)
+			}
+			body := recorder.Body.String()
+			for _, expected := range []string{tt.title, authenticationUnavailableMessage, `href="` + tt.href + `"`, tt.link} {
+				if !strings.Contains(body, expected) {
+					t.Fatalf("dependency page omitted %q", expected)
+				}
+			}
+		})
+	}
+}
+
+func TestExpiredFlowFetchRestartsAndValidRenderConsumesNotice(t *testing.T) {
+	var expired atomic.Bool
+	expired.Store(true)
+	upstream := withKratosServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if expired.Load() {
+			setJSONResponse(w, http.StatusGone, sanitizedExpiredFlowFixture)
+			return
+		}
+		setJSONResponse(w, http.StatusOK, validFlowFixture("fresh-flow", "fresh-csrf"))
+	}))
+	server := testServer(t, upstream.URL)
+	first := httptest.NewRecorder()
+	server.registerInitiate(first, httptest.NewRequest(http.MethodGet, "/register?flow=expired", nil))
+	if first.Code != http.StatusSeeOther || first.Header().Get("Location") != upstream.URL+"/self-service/registration/browser" {
+		t.Fatalf("expired fetch response status=%d location=%q", first.Code, first.Header().Get("Location"))
+	}
+	notice := first.Result().Cookies()[0]
+	expired.Store(false)
+	secondRequest := httptest.NewRequest(http.MethodGet, "/register?flow=fresh-flow", nil)
+	secondRequest.AddCookie(notice)
+	second := httptest.NewRecorder()
+	server.registerInitiate(second, secondRequest)
+	if second.Code != http.StatusOK || !strings.Contains(second.Body.String(), expiredFlowMessage) {
+		t.Fatalf("fresh render status=%d body=%q", second.Code, second.Body.String())
+	}
+	if cookies := second.Result().Cookies(); len(cookies) != 1 || cookies[0].MaxAge != -1 {
+		t.Fatalf("fresh render did not clear notice: %#v", cookies)
+	}
+}
+
+func TestInvalidLoginPreservesRedirectChallengeAndShowsFixedNotice(t *testing.T) {
+	location := "http://auth-ui.invalid/login?flow=retry-login"
+	upstream := withKratosServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.Method == http.MethodPost {
+			w.Header().Set("Location", location)
+			w.WriteHeader(http.StatusSeeOther)
+			return
+		}
+		setJSONResponse(w, http.StatusOK, validFlowFixture("retry-login", "retry-csrf"))
+	}))
+	server := testServer(t, upstream.URL)
+	post := httptest.NewRecorder()
+	postRequest := formRequest(http.MethodPost, "/login?flow=old", url.Values{
+		"csrf_token": {"old"}, "username": {"must-not-be-retained"}, "password": {"must-not-be-retained"},
+	})
+	postRequest.AddCookie(&http.Cookie{Name: "login_challenge", Value: "pending-challenge"})
+	server.login(post, postRequest)
+	if post.Code != http.StatusSeeOther || post.Header().Get("Location") != location {
+		t.Fatalf("login rejection status=%d location=%q", post.Code, post.Header().Get("Location"))
+	}
+	cookies := post.Result().Cookies()
+	if len(cookies) != 1 || cookies[0].Name != authNoticeCookieName || cookies[0].Value != authNoticeLoginInvalid {
+		t.Fatalf("login rejection cookies = %#v", cookies)
+	}
+
+	getRequest := httptest.NewRequest(http.MethodGet, "/login?flow=retry-login", nil)
+	getRequest.AddCookie(cookies[0])
+	getRequest.AddCookie(&http.Cookie{Name: "login_challenge", Value: "pending-challenge"})
+	get := httptest.NewRecorder()
+	server.loginInitiate(get, getRequest)
+	if get.Code != http.StatusOK {
+		t.Fatalf("final GET status = %d, want 200", get.Code)
+	}
+	body := get.Body.String()
+	if !strings.Contains(body, invalidLoginMessage) || !strings.Contains(body, `role="alert"`) || !strings.Contains(body, `action="/login?flow=retry-login"`) {
+		t.Fatal("final login GET omitted fixed notice, alert, or retry action")
+	}
+	if strings.Contains(body, "must-not-be-retained") {
+		t.Fatal("rejected login retained credentials")
+	}
+}
+
+func TestExpiredLoginSubmissionRestartsAndPreservesChallenge(t *testing.T) {
+	tests := []struct {
+		name    string
+		status  int
+		fixture string
+	}{
+		{"expired flow", http.StatusGone, sanitizedExpiredFlowFixture},
+		{"CSRF violation", http.StatusForbidden, sanitizedCSRFFixture},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			upstream := withKratosServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				if r.Method == http.MethodPost {
+					setJSONResponse(w, tt.status, tt.fixture)
+					return
+				}
+				setJSONResponse(w, http.StatusOK, validFlowFixture("fresh-login-flow", "fresh-login-csrf"))
+			}))
+			server := testServer(t, upstream.URL)
+			server.defaultReturnTo = "https://return.example/dashboard"
+			postRequest := formRequest(http.MethodPost, "/login?flow="+obsoleteFlowSentinel, url.Values{
+				"csrf_token": {obsoleteCSRFSentinel}, "username": {"submitted-user"}, "password": {submittedPasswordSentinel},
+			})
+			postRequest.AddCookie(&http.Cookie{Name: "login_challenge", Value: "pending-login-challenge"})
+			post := httptest.NewRecorder()
+			server.login(post, postRequest)
+			if post.Code != http.StatusSeeOther || post.Header().Get("Location") != upstream.URL+"/self-service/login/browser?return_to=https://return.example/dashboard" {
+				t.Fatalf("restart status=%d location=%q", post.Code, post.Header().Get("Location"))
+			}
+			notice := responseCookieNamed(post.Result(), authNoticeCookieName)
+			if notice == nil || notice.Value != authNoticeFlowExpired {
+				t.Fatalf("restart notice = %#v", notice)
+			}
+			if cookie := responseCookieNamed(post.Result(), "login_challenge"); cookie != nil {
+				t.Fatalf("restart mutated pending login challenge: %#v", cookie)
+			}
+
+			getRequest := httptest.NewRequest(http.MethodGet, "/login?flow=fresh-login-flow", nil)
+			getRequest.AddCookie(notice)
+			getRequest.AddCookie(&http.Cookie{Name: "login_challenge", Value: "pending-login-challenge"})
+			get := httptest.NewRecorder()
+			server.loginInitiate(get, getRequest)
+			if get.Code != http.StatusOK {
+				t.Fatalf("fresh GET status=%d, want 200", get.Code)
+			}
+			body := get.Body.String()
+			for _, expected := range []string{expiredFlowMessage, `action="/login?flow=fresh-login-flow"`, `value="fresh-login-csrf"`} {
+				if !strings.Contains(body, expected) {
+					t.Fatalf("fresh login form omitted %q", expected)
+				}
+			}
+			for _, forbidden := range []string{submittedPasswordSentinel, obsoleteFlowSentinel, obsoleteCSRFSentinel, "submitted-user", "pending-login-challenge"} {
+				if strings.Contains(body, forbidden) {
+					t.Fatalf("fresh login form rendered forbidden state %q", forbidden)
+				}
+			}
+			clearedNotice := responseCookieNamed(get.Result(), authNoticeCookieName)
+			if clearedNotice == nil || clearedNotice.MaxAge != -1 {
+				t.Fatalf("fresh login form did not clear notice: %#v", clearedNotice)
+			}
+			if cookie := responseCookieNamed(get.Result(), "login_challenge"); cookie != nil {
+				t.Fatalf("fresh login form mutated pending challenge: %#v", cookie)
+			}
+		})
+	}
+}
+
+func TestUnexpectedLoginOutcomeRendersGeneric502WithoutHydraDecision(t *testing.T) {
+	upstream := withKratosServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		setJSONResponse(w, http.StatusBadRequest, `{"error":"upstream private prose","obsolete_state":"obsolete-upstream-state","ui":{"action":"https://upstream.invalid/generic-action"}}`)
+	}))
+	server := testServer(t, upstream.URL)
+	recorder := httptest.NewRecorder()
+	request := formRequest(http.MethodPost, "/login?flow="+obsoleteFlowSentinel, url.Values{
+		"csrf_token": {obsoleteCSRFSentinel}, "username": {"user"}, "password": {submittedPasswordSentinel},
+	})
+	request.AddCookie(&http.Cookie{Name: "login_challenge", Value: "pending"})
+	server.login(recorder, request)
+	if recorder.Code != http.StatusBadGateway {
+		t.Fatalf("status = %d, want 502", recorder.Code)
+	}
+	body := recorder.Body.String()
+	for _, expected := range []string{"Authentication unavailable", authenticationUnavailableMessage, `href="/login"`} {
+		if !strings.Contains(body, expected) {
+			t.Fatalf("generic login page omitted %q", expected)
+		}
+	}
+	for _, forbidden := range []string{submittedPasswordSentinel, obsoleteFlowSentinel, obsoleteCSRFSentinel, "https://upstream.invalid/generic-action", "obsolete-upstream-state", "upstream private prose"} {
+		if strings.Contains(body, forbidden) {
+			t.Fatalf("generic login page rendered forbidden state %q", forbidden)
+		}
+	}
+}
+
+func TestMalformedLocalFormRequestReturns400(t *testing.T) {
+	server := testServer(t, "http://127.0.0.1:1")
+	request := httptest.NewRequest(http.MethodPost, "/register?flow=flow", strings.NewReader("username=%zz"))
+	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	recorder := httptest.NewRecorder()
+	server.register(recorder, request)
+	if recorder.Code != http.StatusBadRequest {
+		t.Fatalf("status = %d, want 400", recorder.Code)
+	}
+}
+
+func TestRenderTemplateBuffersExecutionFailure(t *testing.T) {
+	tmpl := template.Must(template.New("broken").Funcs(template.FuncMap{
+		"fail": func() (string, error) { return "", http.ErrAbortHandler },
+	}).Parse(`prefix{{fail}}suffix`))
+	recorder := httptest.NewRecorder()
+	renderTemplate(recorder, tmpl, http.StatusUnprocessableEntity, nil)
+	if recorder.Code != http.StatusInternalServerError || strings.Contains(recorder.Body.String(), "prefix") {
+		t.Fatalf("execution failure status=%d body=%q", recorder.Code, recorder.Body.String())
+	}
+}
+
+func renderedElementAttributes(body, element string) []map[string]string {
+	elementPattern := regexp.MustCompile(`(?is)<` + regexp.QuoteMeta(element) + `\b([^>]*)>`)
+	attributePattern := regexp.MustCompile(`(?i)([a-z_:][a-z0-9_:.-]*)(?:\s*=\s*("[^"]*"|'[^']*'|[^\s"'=<>]+))?`)
+	matches := elementPattern.FindAllStringSubmatch(body, -1)
+	result := make([]map[string]string, 0, len(matches))
+	for _, match := range matches {
+		attributes := map[string]string{}
+		for _, attribute := range attributePattern.FindAllStringSubmatch(match[1], -1) {
+			value := attribute[2]
+			if len(value) >= 2 && ((value[0] == '"' && value[len(value)-1] == '"') || (value[0] == '\'' && value[len(value)-1] == '\'')) {
+				value = value[1 : len(value)-1]
+			}
+			attributes[strings.ToLower(attribute[1])] = html.UnescapeString(value)
+		}
+		result = append(result, attributes)
+	}
+	return result
+}
+
+func assertNoHiddenInputsNamed(t *testing.T, body string, forbiddenNames ...string) {
+	t.Helper()
+	forbidden := make(map[string]bool, len(forbiddenNames))
+	for _, name := range forbiddenNames {
+		forbidden[strings.ToLower(name)] = true
+	}
+	for _, attributes := range renderedElementAttributes(body, "input") {
+		if strings.EqualFold(attributes["type"], "hidden") && forbidden[strings.ToLower(attributes["name"])] {
+			t.Fatalf("rendered forbidden hidden input named %q", attributes["name"])
+		}
+	}
+}
+
+func TestRenderedElementAttributesAreOrderIndependent(t *testing.T) {
+	inputs := renderedElementAttributes(`<input value="one" name='username' type="hidden"><input TYPE=hidden disabled NAME=method><input name="alternate" required type='password'>`, "input")
+	if len(inputs) != 3 {
+		t.Fatalf("input count=%d, want 3", len(inputs))
+	}
+	for index, expected := range []map[string]string{
+		{"value": "one", "name": "username", "type": "hidden"},
+		{"type": "hidden", "disabled": "", "name": "method"},
+		{"name": "alternate", "required": "", "type": "password"},
+	} {
+		if !reflect.DeepEqual(inputs[index], expected) {
+			t.Fatalf("input %d attributes=%v, want %v", index, inputs[index], expected)
+		}
+	}
+}
+
+func TestPageModelsDoNotContainPasswordValues(t *testing.T) {
+	models := []any{LoginPageData{}, RegisterPageData{}, ChangePasswordPageData{}, AccountPageData{}, ErrorPageData{}}
+	for _, model := range models {
+		typeOf := reflect.TypeOf(model)
+		for i := 0; i < typeOf.NumField(); i++ {
+			field := typeOf.Field(i)
+			if field.Type.Kind() == reflect.String && strings.Contains(strings.ToLower(field.Name), "password") {
+				t.Fatalf("%s contains password-bearing string field %s", typeOf.Name(), field.Name)
+			}
+		}
+	}
+}
+
+func TestSemanticTemplateContracts(t *testing.T) {
+	templates := testTemplates(t)
+	usernameErrors := []ValidationError{
+		{Field: "username", Message: "First username error."},
+		{Field: "username", Message: "Second username error."},
+	}
+	passwordErrors := []ValidationError{
+		{Field: "password", Message: testPasswordLengthMessage},
+		{Field: "password", Message: testPasswordCompositionMessage},
+	}
+	pages := []struct {
+		name          string
+		tmpl          *template.Template
+		data          any
+		h1            string
+		formCount     int
+		passwordCount int
+	}{
+		{"login", templates.Login, LoginPageData{FormAction: "/login?flow=flow", CSRFToken: "csrf", EnableRegistration: true}, "Sign in", 1, 1},
+		{"register", templates.Register, RegisterPageData{FormAction: "/register?flow=flow", CSRFToken: "csrf"}, "Create account", 1, 1},
+		{"register errors", templates.Register, RegisterPageData{FormAction: "/register?flow=flow", CSRFToken: "csrf", Username: "retained", UsernameErrors: usernameErrors, PasswordErrors: passwordErrors, GeneralError: registrationRejectedMessage}, "Create account", 1, 1},
+		{"change password", templates.ChangePassword, ChangePasswordPageData{FormAction: "/settings?flow=flow", CSRFToken: "csrf", Username: "tester"}, "Change password", 1, 1},
+		{"change password errors", templates.ChangePassword, ChangePasswordPageData{FormAction: "/settings?flow=flow", CSRFToken: "csrf", Username: "tester", PasswordErrors: passwordErrors, GeneralError: passwordChangeRejectedMessage}, "Change password", 1, 1},
+		{"account", templates.WhoAmI, AccountPageData{Username: "tester"}, "Account", 0, 0},
+		{"success", templates.ChangePasswordSuccess, nil, "Password changed", 0, 0},
+		{"error", templates.Error, ErrorPageData{Title: "Authentication unavailable", Message: authenticationUnavailableMessage, RecoveryHref: "/login", RecoveryText: "Try signing in again"}, "Authentication unavailable", 0, 0},
+	}
+	idPattern := regexp.MustCompile(`\bid="([^"]+)"`)
+	for _, page := range pages {
+		t.Run(page.name, func(t *testing.T) {
+			rendered, err := executeTemplate(page.tmpl, page.data)
+			if err != nil {
+				t.Fatal(err)
+			}
+			body := string(rendered)
+			if strings.Count(body, "<main") != 1 {
+				t.Fatalf("main count=%d, want 1", strings.Count(body, "<main"))
+			}
+			if strings.Count(body, "<h1") != 1 || !strings.Contains(body, "<h1>"+page.h1+"</h1>") {
+				t.Fatalf("h1 contract missing for %q", page.h1)
+			}
+			seen := map[string]bool{}
+			for _, match := range idPattern.FindAllStringSubmatch(body, -1) {
+				if match[1] == "" || seen[match[1]] {
+					t.Fatalf("empty or duplicate id %q", match[1])
+				}
+				seen[match[1]] = true
+			}
+			for _, forbidden := range []string{"autofocus", "minlength=", "maxlength=", "pattern=", "confirmation", `role="button"`, `aria-invalid="false"`, `aria-invalid="undefined"`} {
+				if strings.Contains(strings.ToLower(body), strings.ToLower(forbidden)) {
+					t.Fatalf("rendered forbidden form contract %q", forbidden)
+				}
+			}
+			forms := renderedElementAttributes(body, "form")
+			if len(forms) != page.formCount {
+				t.Fatalf("form count=%d, want %d", len(forms), page.formCount)
+			}
+			passwordCount := 0
+			for _, attributes := range renderedElementAttributes(body, "input") {
+				if !strings.EqualFold(attributes["type"], "password") {
+					continue
+				}
+				passwordCount++
+				if _, present := attributes["value"]; present {
+					t.Fatal("rendered password input has a value attribute")
+				}
+			}
+			if passwordCount != page.passwordCount {
+				t.Fatalf("password input count=%d, want %d", passwordCount, page.passwordCount)
+			}
+		})
+	}
+}
+
+func TestFormLabelsNativeAttributesAndPersistentPolicy(t *testing.T) {
+	templates := testTemplates(t)
+	login, err := executeTemplate(templates.Login, LoginPageData{FormAction: "/login?flow=flow", CSRFToken: "csrf", EnableRegistration: true})
+	if err != nil {
+		t.Fatal(err)
+	}
+	register, err := executeTemplate(templates.Register, RegisterPageData{FormAction: "/register?flow=flow", CSRFToken: "csrf"})
+	if err != nil {
+		t.Fatal(err)
+	}
+	settings, err := executeTemplate(templates.ChangePassword, ChangePasswordPageData{FormAction: "/settings?flow=flow", CSRFToken: "csrf", Username: "tester"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	contracts := []struct {
+		name      string
+		body      string
+		fragments []string
+	}{
+		{
+			"login",
+			string(login),
+			[]string{
+				`<label for="login-username">Username</label>`,
+				`id="login-username" type="text" name="username" autocomplete="username" autocapitalize="none" spellcheck="false" required`,
+				`<label for="login-password">Password</label>`,
+				`id="login-password" type="password" name="password" autocomplete="current-password" required`,
+				`<button id="login-submit" class="subbmit-button" type="submit">Sign in</button>`,
+				`<a href="/register">Create account</a>`,
+			},
+		},
+		{
+			"register",
+			string(register),
+			[]string{
+				`<label for="register-username">Username</label>`,
+				`id="register-username" type="text" name="username" value="" autocomplete="username" autocapitalize="none" spellcheck="false" required`,
+				`<label for="register-password">Password</label>`,
+				`id="register-password" type="password" name="password" autocomplete="new-password" required aria-describedby="register-password-policy register-password-symbols"`,
+				`<button id="register-submit" type="submit">Create account</button>`,
+				`<a href="/login">Sign in</a>`,
+			},
+		},
+		{
+			"settings",
+			string(settings),
+			[]string{
+				`<label for="change-password">New password</label>`,
+				`id="change-password" type="password" name="password" autocomplete="new-password" required aria-describedby="change-password-policy change-password-symbols"`,
+				`<button id="change-password-submit" type="submit">Change password</button>`,
+				`<a href="/">Back to account</a>`,
+			},
+		},
+	}
+	for _, contract := range contracts {
+		t.Run(contract.name, func(t *testing.T) {
+			for _, fragment := range contract.fragments {
+				if !strings.Contains(contract.body, fragment) {
+					t.Fatalf("missing semantic fragment %q", fragment)
+				}
+			}
+			if strings.Contains(contract.body, `aria-invalid=`) {
+				t.Fatal("pristine form rendered aria-invalid")
+			}
+		})
+	}
+
+	const policy = "Use at least 20 bytes, including an uppercase letter, lowercase letter, number, and an ASCII symbol or space."
+	const symbols = `!"#$%&'()*+,-./:;<=>?@[\]^_{|}~`
+	for name, body := range map[string]string{"register": string(register), "settings": string(settings)} {
+		unescaped := html.UnescapeString(body)
+		if strings.Count(unescaped, policy) != 1 {
+			t.Fatalf("%s policy guidance count=%d, want 1", name, strings.Count(unescaped, policy))
+		}
+		if !strings.Contains(unescaped, "Accepted ASCII symbols: <code>"+symbols+"</code>. ASCII space is also accepted.") {
+			t.Fatalf("%s omitted exact rendered ASCII symbols", name)
+		}
+	}
+}
+
+func TestValidationSummariesAndFieldAssociations(t *testing.T) {
+	templates := testTemplates(t)
+	usernameErrors := []ValidationError{
+		{Field: "username", Message: "First username error."},
+		{Field: "username", Message: "Second username error."},
+	}
+	passwordErrors := []ValidationError{
+		{Field: "password", Message: testPasswordLengthMessage},
+		{Field: "password", Message: testPasswordCompositionMessage},
+	}
+	register, err := executeTemplate(templates.Register, RegisterPageData{
+		FormAction:     "/register?flow=flow",
+		CSRFToken:      "csrf",
+		UsernameErrors: usernameErrors,
+		PasswordErrors: passwordErrors,
+		GeneralError:   registrationRejectedMessage,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	body := string(register)
+	if strings.Count(body, `role="alert"`) != 1 || strings.Index(body, `role="alert"`) > strings.Index(body, `id="register-username"`) {
+		t.Fatal("registration alert summary is not unique and source-ordered before fields")
+	}
+	unescapedBody := html.UnescapeString(body)
+	for _, message := range []string{"First username error.", "Second username error.", testPasswordLengthMessage, testPasswordCompositionMessage} {
+		if strings.Count(unescapedBody, message) != 2 {
+			t.Fatalf("error %q count=%d, want summary and field error", message, strings.Count(unescapedBody, message))
+		}
+	}
+	for _, fragment := range []string{
+		`aria-invalid="true" aria-describedby="register-username-error-0 register-username-error-1"`,
+		`id="register-username-error-0"`,
+		`id="register-username-error-1"`,
+		`aria-describedby="register-password-policy register-password-symbols register-password-error-0 register-password-error-1" aria-invalid="true"`,
+		`id="register-password-error-0"`,
+		`id="register-password-error-1"`,
+	} {
+		if !strings.Contains(body, fragment) {
+			t.Fatalf("registration errors missing association %q", fragment)
+		}
+	}
+
+	settings, err := executeTemplate(templates.ChangePassword, ChangePasswordPageData{
+		Username:       "tester",
+		FormAction:     "/settings?flow=flow",
+		CSRFToken:      "csrf",
+		PasswordErrors: passwordErrors,
+		GeneralError:   passwordChangeRejectedMessage,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	body = string(settings)
+	if strings.Count(body, `role="alert"`) != 1 || strings.Index(body, `role="alert"`) > strings.Index(body, `id="change-password"`) {
+		t.Fatal("settings alert summary is not unique and source-ordered before its field")
+	}
+	for _, fragment := range []string{
+		`aria-describedby="change-password-policy change-password-symbols change-password-error-0 change-password-error-1" aria-invalid="true"`,
+		`id="change-password-error-0"`,
+		`id="change-password-error-1"`,
+	} {
+		if !strings.Contains(body, fragment) {
+			t.Fatalf("settings errors missing association %q", fragment)
+		}
+	}
+}
+
+func TestTemplateHierarchyNavigationAndEscaping(t *testing.T) {
+	templates := testTemplates(t)
+	account, err := executeTemplate(templates.WhoAmI, AccountPageData{Username: `<script>alert("secret")</script>`})
+	if err != nil {
+		t.Fatal(err)
+	}
+	accountBody := string(account)
+	if strings.Contains(accountBody, `<script>`) || !strings.Contains(accountBody, `&lt;script&gt;`) {
+		t.Fatal("account username was not safely escaped")
+	}
+	for _, fragment := range []string{`<h1>Account</h1>`, `<a href="/settings">Change password</a>`, `<a href="/logout">Log out</a>`} {
+		if !strings.Contains(accountBody, fragment) {
+			t.Fatalf("account missing hierarchy/navigation %q", fragment)
+		}
+	}
+
+	success, err := executeTemplate(templates.ChangePasswordSuccess, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	for _, fragment := range []string{`<h1>Password changed</h1>`, `<p role="status">Password changed successfully.</p>`, `<a href="/">Back to account</a>`} {
+		if !strings.Contains(string(success), fragment) {
+			t.Fatalf("success page missing %q", fragment)
+		}
+	}
+
+	errorPage, err := executeTemplate(templates.Error, ErrorPageData{
+		Title:        "Authentication unavailable",
+		Message:      authenticationUnavailableMessage,
+		RecoveryHref: "/login",
+		RecoveryText: "Try signing in again",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	errorBody := string(errorPage)
+	if strings.Count(errorBody, "<a ") != 1 {
+		t.Fatalf("generic error recovery link count=%d, want 1", strings.Count(errorBody, "<a "))
+	}
+	for _, fragment := range []string{`<h1>Authentication unavailable</h1>`, authenticationUnavailableMessage, `<a href="/login">Try signing in again</a>`} {
+		if !strings.Contains(errorBody, fragment) {
+			t.Fatalf("generic error page missing %q", fragment)
+		}
+	}
+
+	register, err := executeTemplate(templates.Register, RegisterPageData{
+		FormAction: "/register?flow=flow&return=<unsafe>",
+		CSRFToken:  `<csrf&secret>`,
+		Username:   `<img src=x onerror=secret>`,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	registerBody := string(register)
+	for _, forbidden := range []string{`<unsafe>`, `<csrf&secret>`, `<img src=x onerror=secret>`} {
+		if strings.Contains(registerBody, forbidden) {
+			t.Fatalf("registration rendered unsafe value %q", forbidden)
+		}
+	}
+	assertNoHiddenInputsNamed(t, registerBody, "method", "username")
+
+	settings, err := executeTemplate(templates.ChangePassword, ChangePasswordPageData{Username: "tester", FormAction: "/settings?flow=flow", CSRFToken: "csrf"})
+	if err != nil {
+		t.Fatal(err)
+	}
+	settingsBody := string(settings)
+	assertNoHiddenInputsNamed(t, settingsBody, "method", "username")
+}