Files
summercms/bouncer/cookie_guard_test.go
Jakub Zych ef448da1cc test(10-05): cover every Phase 10 Go change with branch-level tests
- bouncer TestPhase10CookieGuard: cookie read without Bearer, Bearer wins,
  empty cookie, frontend audience and blacklisted jti rejected
- boardwalk TestPhase10BoardwalkServing: HEAD, query strings, encoded
  traversal, index by name, nested prefix, MIME fallback, constructor errors
- cabana TestPhase10Coverage: mounted unsafe routes vs the CSRF walk, option
  and filter edges, read-only labels, relation message defaults, bundle
  fallback locale, cookie refresh of an expired token in the refresh window
- phrasebook override precedence, new locale, Bundle merge order, Forms shapes
- surf prefix collision for deeper paths and the default /backend prefix
- swagger2openapi TestUnionRewrite and converter branch tests
- framework tests no longer name the application (acme fixtures instead)
2026-09-27 18:05:28 +02:00

133 lines
4.7 KiB
Go

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)
}
})
}