test(03-04): cover framework database, routing and JWT boundaries
- Shared pgx/GORM pool, ICU locale fail, and isolated plugin migrations - Seven-stage middleware order, missing-guard boot failure, typed 404s - Adversarial JWT matrix including alg:none, empty secret, and no leak Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,9 +2,11 @@ package bouncer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -131,4 +133,110 @@ func TestMiddlewareStatusBodies(t *testing.T) {
|
||||
t.Fatalf("status=%d hit=%s", rec.Code, rec.Header().Get("X-Hit"))
|
||||
}
|
||||
})
|
||||
t.Run("malformed", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer not-a-jwt")
|
||||
assert401(t, req, msgMalformed)
|
||||
})
|
||||
t.Run("alg-none", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+noneToken(t, jwt.MapClaims{
|
||||
"sub": "1",
|
||||
"exp": time.Now().Add(time.Hour).Unix(),
|
||||
}))
|
||||
assert401(t, req, msgBadSignature)
|
||||
})
|
||||
t.Run("absent-exp", func(t *testing.T) {
|
||||
tok := sign(t, jwt.SigningMethodHS256, jwt.MapClaims{"sub": "1"}, []byte(secret))
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+tok)
|
||||
assert401(t, req, msgBadSignature)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVerifyRejectsAlgNoneEmptySecretAndAbsentExp(t *testing.T) {
|
||||
valid := sign(t, jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": "1",
|
||||
"exp": time.Now().Add(time.Hour).Unix(),
|
||||
}, []byte(secret))
|
||||
|
||||
none := noneToken(t, jwt.MapClaims{
|
||||
"sub": "1",
|
||||
"exp": time.Now().Add(time.Hour).Unix(),
|
||||
})
|
||||
if _, err := Verify(none, secret); err == nil || err.Error() != msgBadSignature {
|
||||
t.Fatalf("alg none: %v", err)
|
||||
}
|
||||
|
||||
if _, err := Verify(valid, ""); err == nil || !strings.Contains(err.Error(), "jwt secret is empty") {
|
||||
t.Fatalf("empty secret: %v", err)
|
||||
}
|
||||
if _, err := Verify(valid, " "); err == nil || !strings.Contains(err.Error(), "jwt secret is empty") {
|
||||
t.Fatalf("blank secret: %v", err)
|
||||
}
|
||||
|
||||
noExp := sign(t, jwt.SigningMethodHS256, jwt.MapClaims{"sub": "1"}, []byte(secret))
|
||||
if sub, err := Verify(noExp, secret); err == nil || sub != "" {
|
||||
t.Fatalf("absent exp must fail, got %q %v", sub, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyAndMiddlewareOmitTokenAndSecret(t *testing.T) {
|
||||
const leakSecret = "unique-hs256-secret-value-9f3a"
|
||||
tok := sign(t, jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": "1",
|
||||
"exp": time.Now().Add(time.Hour).Unix(),
|
||||
}, []byte(leakSecret))
|
||||
|
||||
assertClean := func(t *testing.T, msg string) {
|
||||
t.Helper()
|
||||
if strings.Contains(msg, leakSecret) || strings.Contains(msg, tok) {
|
||||
t.Fatalf("leaked secret or token: %s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := Verify("not-a-jwt", leakSecret); err == nil {
|
||||
t.Fatal("want malformed")
|
||||
} else {
|
||||
assertClean(t, err.Error())
|
||||
}
|
||||
if _, err := Verify(tok, "other-"+leakSecret); err == nil {
|
||||
t.Fatal("want bad signature")
|
||||
} else {
|
||||
assertClean(t, err.Error())
|
||||
}
|
||||
|
||||
h := Middleware(leakSecret, memUsers{})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("handler must not run")
|
||||
}))
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+tok)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("status = %d", rec.Code)
|
||||
}
|
||||
assertClean(t, rec.Body.String())
|
||||
}
|
||||
|
||||
func TestContextUserRoundTrip(t *testing.T) {
|
||||
if _, ok := User(t.Context()); ok {
|
||||
t.Fatal("empty context must have no user")
|
||||
}
|
||||
p := &Principal{ID: 7, MustChangePassword: true}
|
||||
got, ok := User(WithUser(t.Context(), p))
|
||||
if !ok || got != p || got.ID != 7 || !got.MustChangePassword {
|
||||
t.Fatalf("got %+v ok=%t", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func noneToken(t *testing.T, claims jwt.MapClaims) string {
|
||||
t.Helper()
|
||||
payload, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`))
|
||||
body := base64.RawURLEncoding.EncodeToString(payload)
|
||||
return header + "." + body + "."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user