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
This commit is contained in:
171
modules/bouncer/backend_guard_test.go
Normal file
171
modules/bouncer/backend_guard_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package bouncer
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestPhase09GuardIsolation is the phase gate for token crossover. Each
|
||||
// subtest fails closed when a frontend and backend credential can be used
|
||||
// on the other guard, including same-secret audience checks, distinct
|
||||
// secrets, refresh, blacklist, and password-reset cutoff.
|
||||
func TestPhase09GuardIsolation(t *testing.T) {
|
||||
const (
|
||||
userSecret = "frontend-secret"
|
||||
backendSecret = "backend-secret"
|
||||
sharedSecret = "same-secret-for-both-guards"
|
||||
issuer = "https://app.test"
|
||||
)
|
||||
frontendUsers := memUsers{byID: map[uint]*Principal{1: {ID: 1}}}
|
||||
backendUsers := memUsers{byID: map[uint]*Principal{2: {ID: 2, Backend: true}}}
|
||||
|
||||
userTok, _, err := MintAudience(sharedSecret, "1", issuer, time.Hour, AudienceUser)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backendTok, _, err := MintAudience(sharedSecret, "2", issuer, time.Hour, AudienceBackend)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
frontend := NewJWTGuard(sharedSecret, frontendUsers, nil)
|
||||
backend := NewBackendJWTGuard(sharedSecret, backendUsers, nil, nil)
|
||||
|
||||
t.Run("own audience", func(t *testing.T) {
|
||||
principal, err := frontend.Authenticate(withBearer(userTok))
|
||||
if err != nil || principal == nil || principal.ID != 1 {
|
||||
t.Fatalf("frontend guard rejected its own token: %v %+v", err, principal)
|
||||
}
|
||||
principal, err = backend.Authenticate(withBearer(backendTok))
|
||||
if err != nil || principal == nil || principal.ID != 2 || !principal.Backend {
|
||||
t.Fatalf("backend guard rejected its own token: %v %+v", err, principal)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("audience crossover same secret", func(t *testing.T) {
|
||||
if principal, err := backend.Authenticate(withBearer(userTok)); err == nil || principal != nil {
|
||||
t.Fatal("backend guard accepted a frontend-audience token signed with the same secret")
|
||||
}
|
||||
if principal, err := frontend.Authenticate(withBearer(backendTok)); err == nil || principal != nil {
|
||||
t.Fatal("frontend guard accepted a backend-audience token signed with the same secret")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("distinct secrets and registries", func(t *testing.T) {
|
||||
userOnly, _, err := MintAudience(userSecret, "1", issuer, time.Hour, AudienceUser)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
adminOnly, _, err := MintAudience(backendSecret, "2", issuer, time.Hour, AudienceBackend)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
front := NewJWTGuard(userSecret, frontendUsers, nil)
|
||||
back := NewBackendJWTGuard(backendSecret, backendUsers, nil, nil)
|
||||
if principal, err := back.Authenticate(withBearer(userOnly)); err == nil || principal != nil {
|
||||
t.Fatal("backend guard accepted a frontend token signed with the frontend secret")
|
||||
}
|
||||
if principal, err := front.Authenticate(withBearer(adminOnly)); err == nil || principal != nil {
|
||||
t.Fatal("frontend guard accepted a backend token signed with the backend secret")
|
||||
}
|
||||
if principal, err := back.Authenticate(withBearer(adminOnly)); err != nil || principal == nil || principal.ID != 2 {
|
||||
t.Fatalf("backend registry principal = %+v err=%v", principal, err)
|
||||
}
|
||||
if principal, err := back.Authenticate(withBearer(mustMint(t, backendSecret, "1", AudienceBackend))); err == nil || principal != nil {
|
||||
t.Fatal("backend guard resolved a subject from the frontend user registry")
|
||||
}
|
||||
reg := NewRegistry()
|
||||
if err := reg.Register("golem15.user", "jwt", front); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := reg.Register("summercms.cabana", "backend", back); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mw, err := reg.Middleware("backend")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
mw(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("backend middleware ran the handler for a frontend token")
|
||||
})).ServeHTTP(rec, withBearer(userOnly))
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("crossover status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("malformed and wrong audience", func(t *testing.T) {
|
||||
if _, err := backend.Authenticate(withBearer("not-a-jwt")); err == nil {
|
||||
t.Fatal("malformed token was accepted")
|
||||
}
|
||||
other, _, err := MintAudience(sharedSecret, "2", issuer, time.Hour, "frontend")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if principal, err := backend.Authenticate(withBearer(other)); err == nil || principal != nil {
|
||||
t.Fatal("backend guard accepted an unexpected audience")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("blacklist and reset cutoff", func(t *testing.T) {
|
||||
bl := NewMemoryBlacklist()
|
||||
revoked, jti, err := MintAudience(backendSecret, "2", issuer, time.Hour, AudienceBackend)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := bl.Add(t.Context(), jti, time.Now().Add(time.Hour), time.Now().Add(-time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
guard := NewBackendJWTGuard(backendSecret, backendUsers, bl, nil)
|
||||
if principal, err := guard.Authenticate(withBearer(revoked)); err == nil || principal != nil {
|
||||
t.Fatal("blacklisted backend token was accepted")
|
||||
}
|
||||
if _, err := RefreshAudience(backendSecret, revoked, AudienceBackend, 2*time.Hour, bl, time.Minute, issuer); err == nil {
|
||||
t.Fatal("blacklisted backend token was refreshed")
|
||||
}
|
||||
cutoff := memUsers{byID: map[uint]*Principal{2: {
|
||||
ID: 2, Backend: true, TokensValidAfter: time.Now().Add(time.Minute),
|
||||
}}}
|
||||
stale, _, err := MintAudience(backendSecret, "2", issuer, time.Hour, AudienceBackend)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if principal, err := NewBackendJWTGuard(backendSecret, cutoff, nil, nil).Authenticate(withBearer(stale)); err == nil || principal != nil {
|
||||
t.Fatal("token issued before the password-reset cutoff was accepted")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("refresh does not cross audiences", func(t *testing.T) {
|
||||
access, _, err := MintAudience(userSecret, "1", issuer, time.Hour, AudienceUser)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
admin, _, err := MintAudience(backendSecret, "2", issuer, time.Hour, AudienceBackend)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := RefreshAudience(userSecret, access, AudienceBackend, 2*time.Hour, nil, time.Minute, issuer); err == nil {
|
||||
t.Fatal("frontend token refreshed into the backend audience")
|
||||
}
|
||||
if _, err := RefreshAudience(backendSecret, admin, AudienceUser, 2*time.Hour, nil, time.Minute, issuer); err == nil {
|
||||
t.Fatal("backend token refreshed into the frontend audience")
|
||||
}
|
||||
if _, err := RefreshAudience(userSecret, admin, AudienceBackend, 2*time.Hour, nil, time.Minute, issuer); err == nil {
|
||||
t.Fatal("backend token refreshed with the frontend secret")
|
||||
}
|
||||
next, err := RefreshAudience(backendSecret, admin, AudienceBackend, 2*time.Hour, nil, time.Minute, issuer)
|
||||
if err != nil || next == "" {
|
||||
t.Fatalf("backend refresh failed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func mustMint(t *testing.T, secret, sub, audience string) string {
|
||||
t.Helper()
|
||||
token, _, err := MintAudience(secret, sub, "https://app.test", time.Hour, audience)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return token
|
||||
}
|
||||
Reference in New Issue
Block a user