package bouncer import ( "context" "net/http" "net/http/httptest" "testing" "time" ) // TestPhase10CookieGuard covers the backend guard's summer_admin cookie // transport (D-19): the cookie is read only when no Bearer header is sent, // Bearer wins when both are present, an empty cookie is unauthenticated, and // the cookie carries no weaker token than the header (audience, blacklist). func TestPhase10CookieGuard(t *testing.T) { const ( cookie = "summer_admin" issuer = "https://app.test/backend" ) users := memUsers{byID: map[uint]*Principal{ 2: {ID: 2, Backend: true}, 3: {ID: 3, Backend: true}, }} withCookie := func(value string) *http.Request { r := httptest.NewRequest(http.MethodGet, "/backend/api/v1/auth/me", nil) r.AddCookie(&http.Cookie{Name: cookie, Value: value}) return r } mint := func(sub, audience string) (string, string) { t.Helper() tok, jti, err := MintAudience(secret, sub, issuer, time.Hour, audience) if err != nil { t.Fatal(err) } return tok, jti } guard := NewBackendJWTGuard(secret, users, nil, nil, cookie) t.Run("cookie without bearer", func(t *testing.T) { tok, _ := mint("2", AudienceBackend) principal, err := guard.Authenticate(withCookie(tok)) if err != nil || principal == nil || principal.ID != 2 { t.Fatalf("cookie token rejected: %v %+v", err, principal) } }) t.Run("cookie value is trimmed", func(t *testing.T) { tok, _ := mint("2", AudienceBackend) r := httptest.NewRequest(http.MethodGet, "/", nil) r.Header.Set("Cookie", cookie+"= "+tok+" ") if principal, err := guard.Authenticate(r); err != nil || principal == nil { t.Fatalf("padded cookie rejected: %v", err) } }) t.Run("bearer wins over cookie", func(t *testing.T) { bearerTok, _ := mint("3", AudienceBackend) cookieTok, _ := mint("2", AudienceBackend) r := withCookie(cookieTok) r.Header.Set("Authorization", "Bearer "+bearerTok) principal, err := guard.Authenticate(r) if err != nil || principal == nil || principal.ID != 3 { t.Fatalf("bearer did not win: %v %+v", err, principal) } // A bad Bearer is not rescued by a good cookie. bad := withCookie(cookieTok) bad.Header.Set("Authorization", "Bearer not-a-token") if principal, err := guard.Authenticate(bad); err == nil || principal != nil { t.Fatal("an invalid bearer fell back to the cookie") } }) t.Run("empty or missing cookie is unauthenticated", func(t *testing.T) { for name, r := range map[string]*http.Request{ "empty": withCookie(""), "blank": withCookie(" "), "missing": httptest.NewRequest(http.MethodGet, "/", nil), } { principal, err := guard.Authenticate(r) if err == nil || principal != nil || err.Error() != msgTokenNotProvided { t.Fatalf("%s cookie: principal=%+v err=%v, want %q", name, principal, err, msgTokenNotProvided) } } other := httptest.NewRequest(http.MethodGet, "/", nil) tok, _ := mint("2", AudienceBackend) other.AddCookie(&http.Cookie{Name: "summer_other", Value: tok}) if _, err := guard.Authenticate(other); err == nil { t.Fatal("a token under another cookie name was accepted") } }) t.Run("frontend audience in the cookie is rejected", func(t *testing.T) { tok, _ := mint("2", AudienceUser) if principal, err := guard.Authenticate(withCookie(tok)); err == nil || principal != nil { t.Fatal("backend guard accepted a frontend-audience cookie") } }) t.Run("blacklisted jti in the cookie is rejected", func(t *testing.T) { bl := NewMemoryBlacklist() blocking := NewBackendJWTGuard(secret, users, bl, nil, cookie) tok, jti := mint("2", AudienceBackend) if _, err := blocking.Authenticate(withCookie(tok)); err != nil { t.Fatalf("fresh cookie rejected: %v", err) } if err := bl.Add(context.Background(), jti, time.Now().Add(time.Hour), time.Now().Add(-time.Second)); err != nil { t.Fatal(err) } principal, err := blocking.Authenticate(withCookie(tok)) if err == nil || principal != nil || err.Error() != msgBadSignature { t.Fatalf("blacklisted cookie: principal=%+v err=%v", principal, err) } }) t.Run("bearer-only guard ignores the cookie", func(t *testing.T) { tok, _ := mint("2", AudienceBackend) bearerOnly := NewBackendJWTGuard(secret, users, nil, nil) if principal, err := bearerOnly.Authenticate(withCookie(tok)); err == nil || principal != nil { t.Fatal("a guard without cookie names read the cookie") } }) t.Run("second cookie name is tried after an empty first", func(t *testing.T) { tok, _ := mint("2", AudienceBackend) two := NewBackendJWTGuard(secret, users, nil, nil, "legacy_admin", cookie) r := withCookie(tok) r.AddCookie(&http.Cookie{Name: "legacy_admin", Value: ""}) if principal, err := two.Authenticate(r); err != nil || principal == nil { t.Fatalf("second cookie name not tried: %v", err) } }) }