mirror of
https://github.com/hanzoai/status.git
synced 2026-10-30 18:21:48 +00:00
zip's registration is a verb method on the group, so a route that is not a typed operation says which method it answers with. The security package's own two-method router goes with it: it existed so both the app and a sub-router could satisfy one name, and restating a routing surface is what let the two disagree — the methods it named no longer exist. The app spells itself as a group, which registers at exactly the paths it did. The toolchain and the pin move together, because below zip v1.36.50 the JSON encoder was selected on a build tag: a repo whose go directive rises first writes a wire that is neither the old release's nor the new one's until the pin catches up. metrics_test.go still does not compile, before and after: it compares a metric registry against a gatherer from a different metrics library, so the two Gather signatures disagree. That is its own change. Co-authored-by: Hanzo Dev <[email protected]>
139 lines
4.3 KiB
Go
139 lines
4.3 KiB
Go
package security
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
// ApplySecurityMiddleware is what stands between the internet and the routes
|
|
// registered after it. Three things about it are load-bearing, and only one of
|
|
// them was pinned before: the refusal (401 and its body), the ADMISSION of a
|
|
// live session — a middleware that refused everything would have satisfied the
|
|
// old test — and the barrier property, that routes registered BEFORE it stay
|
|
// public. All three are asserted here on exact bytes.
|
|
|
|
func protectedApp(t *testing.T, c *Config) *zip.App {
|
|
t.Helper()
|
|
app := zip.New(zip.Config{})
|
|
// Registered before the middleware: the unprotected half of the surface.
|
|
app.Raw(http.MethodGet, "/public", func(c *zip.Ctx) error { return c.String(http.StatusOK, "public") })
|
|
if err := c.ApplySecurityMiddleware(app.Group("").Group("")); err != nil {
|
|
t.Fatalf("ApplySecurityMiddleware: %v", err)
|
|
}
|
|
app.Raw(http.MethodGet, "/private", func(c *zip.Ctx) error { return c.String(http.StatusOK, "private") })
|
|
return app
|
|
}
|
|
|
|
func oidcSessionConfig() *Config {
|
|
return &Config{OIDC: &OIDCConfig{
|
|
IssuerURL: "https://idp.test",
|
|
RedirectURL: "http://localhost:80/authorization-code/callback",
|
|
ClientID: "client-id",
|
|
ClientSecret: "client-secret",
|
|
Scopes: []string{"openid"},
|
|
SessionTTL: DefaultOIDCSessionTTL,
|
|
}}
|
|
}
|
|
|
|
func TestProtectWire(t *testing.T) {
|
|
c := oidcSessionConfig()
|
|
app := protectedApp(t, c)
|
|
|
|
// A session the server actually knows, established the way the callback
|
|
// establishes one.
|
|
sessions.SetWithTTL("live-session", "[email protected]", DefaultOIDCSessionTTL)
|
|
t.Cleanup(func() { sessions.Delete("live-session") })
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
path string
|
|
cookies []*http.Cookie
|
|
status int
|
|
body string
|
|
}{
|
|
{
|
|
name: "no cookie at all",
|
|
path: "/private",
|
|
status: http.StatusUnauthorized,
|
|
body: "token is missing or invalid",
|
|
},
|
|
{
|
|
name: "a session id the server does not know",
|
|
path: "/private",
|
|
cookies: []*http.Cookie{{Name: cookieNameSession, Value: "invented"}},
|
|
status: http.StatusUnauthorized,
|
|
body: "token is missing or invalid",
|
|
},
|
|
{
|
|
name: "the wrong cookie name carries no session",
|
|
path: "/private",
|
|
cookies: []*http.Cookie{{Name: "session", Value: "live-session"}},
|
|
status: http.StatusUnauthorized,
|
|
body: "token is missing or invalid",
|
|
},
|
|
{
|
|
// A bearer is not how this surface is entered; only the cookie is.
|
|
name: "a live session is admitted",
|
|
path: "/private",
|
|
cookies: []*http.Cookie{{Name: cookieNameSession, Value: "live-session"}},
|
|
status: http.StatusOK,
|
|
body: "private",
|
|
},
|
|
{
|
|
name: "a route registered before the middleware stays public",
|
|
path: "/public",
|
|
status: http.StatusOK,
|
|
body: "public",
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", tc.path, http.NoBody)
|
|
for _, ck := range tc.cookies {
|
|
req.AddCookie(ck)
|
|
}
|
|
resp, err := app.Fiber().Test(req)
|
|
if err != nil {
|
|
t.Fatalf("Test: %v", err)
|
|
}
|
|
b, _ := io.ReadAll(resp.Body)
|
|
_ = resp.Body.Close()
|
|
if resp.StatusCode != tc.status {
|
|
t.Errorf("status = %d, want %d", resp.StatusCode, tc.status)
|
|
}
|
|
if string(b) != tc.body {
|
|
t.Errorf("body = %q, want %q", string(b), tc.body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The Authorization header is the default way g8 reads a token. This surface
|
|
// replaces that with the session cookie, so a bearer must buy nothing — pinned
|
|
// because a conversion that reverted to the default extractor would otherwise
|
|
// look like it worked.
|
|
func TestProtectWire_BearerBuysNothing(t *testing.T) {
|
|
c := oidcSessionConfig()
|
|
app := protectedApp(t, c)
|
|
sessions.SetWithTTL("bearer-session", "[email protected]", DefaultOIDCSessionTTL)
|
|
t.Cleanup(func() { sessions.Delete("bearer-session") })
|
|
|
|
req := httptest.NewRequest("GET", "/private", http.NoBody)
|
|
req.Header.Set("Authorization", "Bearer bearer-session")
|
|
resp, err := app.Fiber().Test(req)
|
|
if err != nil {
|
|
t.Fatalf("Test: %v", err)
|
|
}
|
|
b, _ := io.ReadAll(resp.Body)
|
|
_ = resp.Body.Close()
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Errorf("status = %d, want 401 — a bearer is not a session here", resp.StatusCode)
|
|
}
|
|
if string(b) != "token is missing or invalid" {
|
|
t.Errorf("body = %q, want %q", string(b), "token is missing or invalid")
|
|
}
|
|
}
|