oficina / home Public

Resume expired team choices through fixed Home sign-in

Paul Campbell pushed by paul@keymail.dev 171f49a90b39a2c9138bd077d7d8adcc4040fd89 parent 90b58bd
3 files changed, +89 −4
  • internal/home/directory_runtime_test.go +67 −0
  • internal/home/docs_chooser.go +14 −1
  • internal/home/docs_continue_test.go +8 −3

CI passed — run details

diff --git a/internal/home/directory_runtime_test.go b/internal/home/directory_runtime_test.go
index ec59f4a..98b466e 100644
--- a/internal/home/directory_runtime_test.go
+++ b/internal/home/directory_runtime_test.go
@@ -130,3 +130,70 @@ func TestDirectoryRuntimeAnonymousReturnAndRoleSchemaSeparation(t *testing.T) {
t.Fatal("registry shared mutable backing")
}
}
+
+func TestDirectoryRuntimeExpiredChoiceReturnsToFreshTeamSelection(t *testing.T) {
+ f := newMountedChooser(t)
+ cookie := f.session(t, chooserPerson)
+ f.team(t, "A1", "alpha", "Alpha", chooserPerson)
+ f.team(t, "B2", "beta", "Beta", chooserPerson)
+ for _, metadata := range []string{"same-origin", "none"} {
+ req := httptest.NewRequest("POST", directoryOrigin+docsChoicePath, strings.NewReader("team=B2"))
+ req.AddCookie(cookie)
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Set("Sec-Fetch-Site", metadata)
+ rec := httptest.NewRecorder()
+ f.handler.ServeHTTP(rec, req)
+ assertDocsContinuation(t, rec, "https://beta.docs.oficina.works/auth/start?return_to=%2F")
+ }
+ if _, err := f.p.directory.db.Writer().Exec(`UPDATE sessions SET expires_at='2000-01-01T00:00:00Z'`); err != nil {
+ t.Fatal(err)
+ }
+ for _, c := range []*http.Cookie{cookie, nil} {
+ r := f.request("POST", docsChoicePath, directoryOrigin, "team=SecretSubmittedTeamXYZ&return_to=https%3A%2F%2Fevil.example%2Fsecret-navigation", c)
+ cleared := ""
+ if c != nil {
+ cleared = c.Name
+ }
+ assertChooserSigninContinuation(t, r, cleared)
+ for _, forbidden := range []string{"SecretSubmittedTeamXYZ", "evil.example", "secret-navigation"} {
+ if strings.Contains(r.Body.String(), forbidden) {
+ t.Fatal("submitted selection reflected")
+ }
+ }
+ }
+ r := f.request("GET", docsEntryPath, "", "", cookie)
+ if r.Code != 303 || r.Header().Get("Location") != "/auth/start?return_to=%2Fapps%2Fdocs" {
+ t.Fatal("anonymous GET changed", r.Code, r.Header())
+ }
+ invalid := f.session(t, "email@example.invalid")
+ for _, method := range []string{"GET", "POST"} {
+ path := docsEntryPath
+ if method == "POST" {
+ path = docsChoicePath
+ }
+ r = f.request(method, path, directoryOrigin, "team=A1", invalid)
+ if r.Code != 404 || strings.Contains(r.Body.String(), "http-equiv") {
+ t.Fatal("invalid subject entered authentication continuation", r.Code)
+ }
+ noStore(t, r)
+ }
+}
+
+func assertChooserSigninContinuation(t *testing.T, r *httptest.ResponseRecorder, cleared string) {
+ t.Helper()
+ cookies := r.Result().Cookies()
+ if cleared == "" {
+ if len(cookies) != 0 {
+ t.Fatal("anonymous continuation minted state")
+ }
+ } else {
+ if len(cookies) != 1 {
+ t.Fatal("expired continuation did not only clear its session")
+ }
+ c := cookies[0]
+ if c.Name != cleared || c.Value != "" || c.MaxAge >= 0 || c.Path != "/" || !c.Secure || !c.HttpOnly || c.SameSite != http.SameSiteLaxMode {
+ t.Fatal("expired continuation replaced session state")
+ }
+ }
+ assertContinuationPage(t, r, directoryOrigin+"/auth/start?return_to=%2Fapps%2Fdocs")
+}
diff --git a/internal/home/docs_chooser.go b/internal/home/docs_chooser.go
index b664a5f..53c4136 100644
--- a/internal/home/docs_chooser.go
+++ b/internal/home/docs_chooser.go
@@ -116,7 +116,14 @@ func (c *docsChooser) choose(w http.ResponseWriter, r *http.Request) {
func (c *docsChooser) subject(w http.ResponseWriter, r *http.Request) (string, bool) {
session, ok := sessions.Current(r)
if !ok {
- http.Redirect(w, r, handoff.StartPath+"?"+url.Values{"return_to": {docsEntryPath}}.Encode(), http.StatusSeeOther)
+ target := handoff.StartPath + "?" + url.Values{"return_to": {docsEntryPath}}.Encode()
+ if r.Method == http.MethodPost {
+ // Start a new GET navigation after the form response. The submitted
+ // selection is discarded; sign-in returns to current team choices.
+ c.renderContinuation(w, c.app.origins.Directory+target)
+ } else {
+ http.Redirect(w, r, target, http.StatusSeeOther)
+ }
return "", false
}
if !ValidPersonRef(session.Subject) {
@@ -140,6 +147,12 @@ func (c *docsChooser) continueToDocs(w http.ResponseWriter, choice directorydata
return
}
destination := choice.Origin + handoff.StartPath + "?" + url.Values{"return_to": {"/"}}.Encode()
+ c.renderContinuation(w, destination)
+}
+
+// Callers supply either a validated stored app origin plus the fixed app path,
+// or the validated directory origin plus the fixed sign-in continuation path.
+func (c *docsChooser) renderContinuation(w http.ResponseWriter, destination string) {
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Referrer-Policy", "no-referrer")
c.app.render(w, http.StatusOK, "docs_continue.html", struct{ Destination string }{destination})
diff --git a/internal/home/docs_continue_test.go b/internal/home/docs_continue_test.go
index 8b7b237..549afd2 100644
--- a/internal/home/docs_continue_test.go
+++ b/internal/home/docs_continue_test.go
@@ -50,6 +50,13 @@ func continuationTargets(t *testing.T, body string) (refresh, links []string) {
func assertDocsContinuation(t *testing.T, r *httptest.ResponseRecorder, want string) {
t.Helper()
noStore(t, r)
+ assertContinuationPage(t, r, want)
+}
+func assertContinuationPage(t *testing.T, r *httptest.ResponseRecorder, want string) {
+ t.Helper()
+ if r.Header().Get("Cache-Control") != "no-store" {
+ t.Fatal("cacheable continuation")
+ }
if r.Code != 200 || r.Header().Get("Location") != "" || r.Header().Get("Refresh") != "" || r.Header().Get("Content-Type") != "text/html; charset=utf-8" || r.Header().Get("Referrer-Policy") != "no-referrer" {
t.Fatal(r.Code, r.Header())
}
@@ -138,7 +145,5 @@ func TestDocsContinueExactHostAndExpiredSession(t *testing.T) {
t.Fatal(err)
}
r := f.request(http.MethodPost, docsChoicePath, directoryOrigin, "team=A1", cookie)
- if r.Code != 303 || r.Header().Get("Location") != "/auth/start?return_to=%2Fapps%2Fdocs" {
- t.Fatal(r.Code, r.Header())
- }
+ assertChooserSigninContinuation(t, r, cookie.Name)
}