package connect import ( "amadan.net/oficina/home/handoff/seal" "bytes" "net/http" "net/http/httptest" "testing" "time" ) const navigationOrigin = "https://slides.example.com" func navigationFixture(t *testing.T) (*Session, *localLinkNavigation) { t.Helper() s := &Session{Subject: "existing@example.com", Method: "magiclink", At: time.Now()} sealer, err := seal.New(bytes.Repeat([]byte{19}, 32), "oficina/slides") if err != nil { t.Fatal(err) } n := newLocalLinkNavigation(sealer, RoutesConfig{Namespace: "oficina/slides", Origin: navigationOrigin, SessionCookie: "__Host-rastrillo_session", ReadSession: func(r *http.Request) (*Session, error) { c, err := r.Cookie("__Host-rastrillo_session") if err != nil || c.Value != "verified" { return nil, nil } return s, nil }, Render: func(w http.ResponseWriter, r *http.Request, state, label string, status int) { w.WriteHeader(status) }}) if n == nil { t.Fatal("navigation rejected") } return s, n } func TestNavigationRequiresActualFreshLocalSession(t *testing.T) { for _, tc := range []struct { method string age time.Duration auth bool want string }{{"magiclink", 0, false, "/account/home"}, {"magiclink", 6 * time.Minute, false, "/"}, {"keymail", 0, false, "/"}, {"keymail", 0, true, "/account/home"}, {HomeMethod, 0, true, "/"}} { t.Run(tc.method+tc.age.String()+tc.want, func(t *testing.T) { s, n := navigationFixture(t) now := time.Now() n.now = func() time.Time { return now } s.Method = tc.method s.At = now.Add(-tc.age) if tc.auth { s.AuthTime = s.At } w := httptest.NewRecorder() n.Begin(w, httptest.NewRequest("GET", navigationOrigin+"/account/home", nil)) if w.Code != 303 || w.Header().Get("Location") != "/signin/local" { t.Fatal(w.Code, w.Header()) } r := httptest.NewRequest("GET", navigationOrigin+localLinkCompletePath, nil) for _, c := range w.Result().Cookies() { if c.Name == localLinkNavigationCookie && c.MaxAge > 0 { r.AddCookie(c) } } r.AddCookie(&http.Cookie{Name: n.sessionCookie, Value: "verified"}) done := httptest.NewRecorder() n.Complete(done, r) if done.Header().Get("Location") != tc.want { t.Fatal(done.Header()) } r.AddCookie(&http.Cookie{Name: n.sessionCookie, Value: "verified"}) duplicate := httptest.NewRecorder() n.Complete(duplicate, r) if duplicate.Header().Get("Location") != "/" { t.Fatal("duplicate session accepted") } }) } } func TestNavigationPurposeIsolationAndFixedAction(t *testing.T) { _, n := navigationFixture(t) w := httptest.NewRecorder() n.Begin(w, httptest.NewRequest("GET", navigationOrigin+"/account/home", nil)) r := httptest.NewRequest("GET", navigationOrigin+localLinkCompletePath, nil) for _, c := range w.Result().Cookies() { if c.MaxAge > 0 { r.AddCookie(c) } } if !n.marker(r, n.now().Unix()) { t.Fatal("valid marker refused") } other := *n other.namespace = "oficina/calls" if other.marker(r, n.now().Unix()) { t.Fatal("cross-app marker accepted") } for _, target := range []string{navigationOrigin + "/account/home?return_to=https://bad.test", "https://bad.test/account/home"} { bad := httptest.NewRecorder() n.Begin(bad, httptest.NewRequest("GET", target, nil)) for _, c := range bad.Result().Cookies() { if c.MaxAge > 0 { t.Fatal("wrong host/query minted navigation") } } } } func TestNavigationLocalLandingPathIsSafeAndDoesNotOverrideLink(t *testing.T) { _, n := navigationFixture(t) config := RoutesConfig{Namespace: n.namespace, Origin: n.origin, SessionCookie: n.sessionCookie, ReadSession: n.readSession, Render: func(w http.ResponseWriter, r *http.Request, state, label string, status int) { w.WriteHeader(status) }} for _, target := range []string{"https://other.example/", "//other.example/", "/\\other.example/"} { config.LocalLandingPath = target if newLocalLinkNavigation(n.sealer, config) != nil { t.Fatal("unsafe local landing accepted", target) } } config.LocalLandingPath = "/continue" land := newLocalLinkNavigation(n.sealer, config) if land == nil { t.Fatal("safe local landing refused") } r := navigationRequest(localLinkCompletePath) w := httptest.NewRecorder() land.Complete(w, r) if w.Header().Get("Location") != "/continue" { t.Fatal("local landing ignored") } marker := navigationMarker(t, land) r.AddCookie(marker) r.AddCookie(&http.Cookie{Name: land.sessionCookie, Value: "verified"}) w = httptest.NewRecorder() land.Complete(w, r) if w.Header().Get("Location") != "/account/home" { t.Fatal("landing replaced explicit link navigation") } }