Files
summercms/modules/cabana/refresh_revocation_test.go
Jakub Zych 5e50b166ef refactor(10.2-01): nest framework packages under modules
- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
2026-09-28 02:21:02 +02:00

145 lines
6.0 KiB
Go

package cabana_test
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"testing"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/cabana"
)
// TestAdminRefreshRevocation pins CR-01: POST {prefix}/api/v1/auth/refresh
// applies the backend guard's subject checks before minting. A token issued
// before `summer admin:reset-password` (tokens_valid_after), or held by a
// deactivated or soft-deleted admin, cannot be refreshed over either
// transport, and a refused cookie refresh expires summer_admin.
func TestAdminRefreshRevocation(t *testing.T) {
gdb := adminGorm(t)
cookieLogin := func(t *testing.T, h http.Handler, login string) *http.Cookie {
t.Helper()
rec := phase10Send(t, h, http.MethodPost, adminAPI("/auth/login"),
map[string]string{"login": login, "password": adminTestPassword}, nil, true)
if rec.Code != http.StatusOK {
t.Fatalf("cookie login status=%d body=%s", rec.Code, rec.Body.String())
}
return phase10Cookie(t, rec, cabana.DefaultAdminPrefix)
}
bearerLogin := func(t *testing.T, h http.Handler, login string) string {
t.Helper()
rec := phase10Send(t, h, http.MethodPost, adminAPI("/auth/login"),
map[string]string{"login": login, "password": adminTestPassword}, nil, false)
if rec.Code != http.StatusOK {
t.Fatalf("bearer login status=%d body=%s", rec.Code, rec.Body.String())
}
return accessToken(t, rec.Body.Bytes())
}
resetPassword := func(t *testing.T, login string) {
t.Helper()
reset := commandByName(t, cabana.RuntimeCommands(commandApp(t, gdb)), "admin:reset-password")
var buf bytes.Buffer
if err := reset.Run(context.Background(), flagInput{
args: []string{login},
flags: map[string]string{"password": "rrev-replacement-password"},
}, bonfire.NewOutput(nil, &buf, &buf)); err != nil {
t.Fatalf("admin:reset-password: %v output=%s", err, buf.String())
}
}
assertRefusedWithExpiredCookie := func(t *testing.T, rec *httptest.ResponseRecorder) {
t.Helper()
if rec.Code != http.StatusUnauthorized || phase10ErrorCode(t, rec) != "unauthenticated" {
t.Fatalf("refresh status=%d body=%s, want 401 unauthenticated", rec.Code, rec.Body.String())
}
var expired *http.Cookie
for _, c := range rec.Result().Cookies() {
if c.Name == cabana.AdminCookieName {
expired = c
}
}
if expired == nil || expired.Value != "" || expired.MaxAge >= 0 || expired.Path != cabana.DefaultAdminPrefix {
t.Fatalf("refused refresh cookie = %+v, want an expiring %s with Path %s", expired, cabana.AdminCookieName, cabana.DefaultAdminPrefix)
}
}
t.Run("pre-reset cookie is refused and expired", func(t *testing.T) {
h := adminHandler(t, gdb, nil)
insertAdmin(t, gdb, "rrev-cookie", "rrev-cookie@example.test", adminTestPassword, true, false)
old := cookieLogin(t, h, "rrev-cookie")
resetPassword(t, "rrev-cookie")
if me := phase10Send(t, h, http.MethodGet, adminAPI("/auth/me"), nil, old, true); me.Code != http.StatusUnauthorized {
t.Fatalf("pre-reset cookie /auth/me status=%d body=%s", me.Code, me.Body.String())
}
rec := phase10Send(t, h, http.MethodPost, adminAPI("/auth/refresh"), nil, old, true)
assertRefusedWithExpiredCookie(t, rec)
})
t.Run("pre-reset bearer is refused without cookies", func(t *testing.T) {
h := adminHandler(t, gdb, nil)
insertAdmin(t, gdb, "rrev-bearer", "rrev-bearer@example.test", adminTestPassword, true, false)
token := bearerLogin(t, h, "rrev-bearer")
resetPassword(t, "rrev-bearer")
rec := postAuth(t, h, http.MethodPost, adminAPI("/auth/refresh"), token, nil)
if rec.Code != http.StatusUnauthorized || phase10ErrorCode(t, rec) != "unauthenticated" {
t.Fatalf("pre-reset bearer refresh status=%d body=%s, want 401 unauthenticated", rec.Code, rec.Body.String())
}
if got := rec.Header().Values("Set-Cookie"); len(got) != 0 {
t.Fatalf("bearer refresh set cookies: %q", got)
}
})
t.Run("deactivated admin is refused", func(t *testing.T) {
h := adminHandler(t, gdb, nil)
user := insertAdmin(t, gdb, "rrev-deactivated", "rrev-deactivated@example.test", adminTestPassword, true, false)
old := cookieLogin(t, h, "rrev-deactivated")
if err := gdb.Exec(`UPDATE backend_users SET is_activated = false WHERE id = ?`, user.ID).Error; err != nil {
t.Fatal(err)
}
rec := phase10Send(t, h, http.MethodPost, adminAPI("/auth/refresh"), nil, old, true)
assertRefusedWithExpiredCookie(t, rec)
})
t.Run("soft-deleted admin is refused", func(t *testing.T) {
h := adminHandler(t, gdb, nil)
user := insertAdmin(t, gdb, "rrev-deleted", "rrev-deleted@example.test", adminTestPassword, true, false)
old := cookieLogin(t, h, "rrev-deleted")
if err := gdb.Delete(&user).Error; err != nil {
t.Fatal(err)
}
rec := phase10Send(t, h, http.MethodPost, adminAPI("/auth/refresh"), nil, old, true)
assertRefusedWithExpiredCookie(t, rec)
})
t.Run("active admin still refreshes", func(t *testing.T) {
h := adminHandler(t, gdb, nil)
insertAdmin(t, gdb, "rrev-active", "rrev-active@example.test", adminTestPassword, true, false)
first := cookieLogin(t, h, "rrev-active")
rec := phase10Send(t, h, http.MethodPost, adminAPI("/auth/refresh"), nil, first, true)
if rec.Code != http.StatusOK {
t.Fatalf("cookie refresh status=%d body=%s", rec.Code, rec.Body.String())
}
second := phase10Cookie(t, rec, cabana.DefaultAdminPrefix)
if second.Value == first.Value {
t.Fatal("cookie refresh did not rotate the token")
}
phase10AssertCookieBody(t, rec, second.Value)
if me := phase10Send(t, h, http.MethodGet, adminAPI("/auth/me"), nil, second, true); me.Code != http.StatusOK {
t.Fatalf("rotated cookie /auth/me status=%d body=%s", me.Code, me.Body.String())
}
token := bearerLogin(t, h, "rrev-active")
bearer := postAuth(t, h, http.MethodPost, adminAPI("/auth/refresh"), token, nil)
if bearer.Code != http.StatusOK {
t.Fatalf("bearer refresh status=%d body=%s", bearer.Code, bearer.Body.String())
}
phase10AssertBearerBody(t, bearer)
if got := bearer.Header().Values("Set-Cookie"); len(got) != 0 {
t.Fatalf("bearer refresh set cookies: %q", got)
}
})
}