oficina / home Public

Verify shared route boundaries and signout state

Paul Campbell pushed by paul@keymail.dev 9a780c687d4f1f8f5aa8770573e0c740f7796a78 parent f14c491
1 file changed, +72 −0
  • connect/routes_test.go +72 −0

CI passed — run details

diff --git a/connect/routes_test.go b/connect/routes_test.go
new file mode 100644
index 0000000..4fe0ba5
--- /dev/null
+++ b/connect/routes_test.go
@@ -0,0 +1,72 @@
+package connect
+
+import (
+ "amadan.net/oficina/home/handoff"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestRouteGuardExemptsOnlyCallbackAndPreservesPolicy(t *testing.T) {
+ b := &Bridge{render: func(w http.ResponseWriter, r *http.Request, state, label string, status int) { w.WriteHeader(status) }}
+ h := &Routes{bridge: b, cfg: RoutesConfig{Origin: "https://slides.example.com", Issuer: "https://home.example.com", SessionCookie: "__Host-session", FormPolicy: "default-src 'none'; form-action 'self' https://home.example.com", Protect: func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "POST" {
+ w.WriteHeader(403)
+ return
+ }
+ next.ServeHTTP(w, r)
+ })
+ }}}
+ called := 0
+ handler := h.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called++; w.WriteHeader(204) }))
+ for _, tc := range []struct {
+ method, origin, path string
+ dup bool
+ want int
+ }{{"POST", h.cfg.Origin, handoff.CallbackPath, false, 204}, {"POST", h.cfg.Origin, handoff.CallbackPath + "/", false, 403}, {"POST", h.cfg.Origin, "/account/home", false, 403}, {"GET", "https://wrong.example.com", handoff.CallbackPath, false, 404}, {"GET", h.cfg.Origin, "/", true, 400}, {"GET", h.cfg.Origin, "/", false, 204}} {
+ r := httptest.NewRequest(tc.method, tc.origin+tc.path, nil)
+ if tc.dup {
+ r.AddCookie(&http.Cookie{Name: h.cfg.SessionCookie, Value: "a"})
+ r.AddCookie(&http.Cookie{Name: h.cfg.SessionCookie, Value: "b"})
+ }
+ w := httptest.NewRecorder()
+ handler.ServeHTTP(w, r)
+ if w.Code != tc.want {
+ t.Fatalf("%s %s: %d", tc.method, tc.path, w.Code)
+ }
+ if w.Header().Get("Content-Security-Policy") != h.cfg.FormPolicy {
+ t.Fatal("app policy replaced")
+ }
+ }
+ if called != 2 {
+ t.Fatal("guard bypass", called)
+ }
+ localCalls := 0
+ local := h.LocalGuard(func(w http.ResponseWriter, r *http.Request) { localCalls++ })
+ r := httptest.NewRequest("GET", h.cfg.Origin+"/auth/verify", nil)
+ r.AddCookie(&http.Cookie{Name: "__Host-rastrillo_pending", Value: "a"})
+ r.AddCookie(&http.Cookie{Name: "__Host-rastrillo_pending", Value: "b"})
+ w := httptest.NewRecorder()
+ local(w, r)
+ if w.Code != 400 || localCalls != 0 {
+ t.Fatal("ambiguous local pending reached plugin")
+ }
+}
+func TestSignoutEndsLocallyAndClearsHandoffPointers(t *testing.T) {
+ revoked := false
+ h := &Routes{cfg: RoutesConfig{SignOut: func(http.ResponseWriter, *http.Request) { revoked = true }}}
+ w := httptest.NewRecorder()
+ h.Signout(w, httptest.NewRequest("POST", "https://slides.example.com/signout", nil))
+ if !revoked || w.Code != 303 || w.Header().Get("Location") != "/signed-out" {
+ t.Fatal("signout restarted Home exchange")
+ }
+ if len(w.Result().Cookies()) != 6 {
+ t.Fatal("handoff pointers left behind")
+ }
+ for _, c := range w.Result().Cookies() {
+ if c.MaxAge != -1 || !c.Secure || !c.HttpOnly {
+ t.Fatal("cookie not cleared safely")
+ }
+ }
+}