Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions ent/client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 0 additions & 7 deletions ent/paymentorder/paymentorder.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 2 additions & 38 deletions ent/paymentorder_create.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 4 additions & 16 deletions ent/paymentorder_update.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions ent/runtime/runtime.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions routers/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ func senderRoutes(route *gin.Engine) {
v1 := route.Group("/v1/sender/")
v1.Use(middleware.OrdersReadinessMiddleware())
v1.Use(middleware.DynamicAuthMiddleware)
v1.Use(middleware.DomainWhitelistMiddleware)
v1.Use(middleware.OnlySenderMiddleware)

v1.POST("orders", senderCtrl.InitiatePaymentOrder)
Expand All @@ -144,6 +145,7 @@ func senderRoutes(route *gin.Engine) {
v2 := route.Group("/v2/sender/")
v2.Use(middleware.OrdersReadinessMiddleware())
v2.Use(middleware.DynamicAuthMiddleware)
v2.Use(middleware.DomainWhitelistMiddleware)
v2.Use(middleware.OnlySenderMiddleware)

v2.POST("orders", senderCtrl.InitiatePaymentOrderV2)
Expand Down
47 changes: 47 additions & 0 deletions routers/middleware/domain_whitelist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package middleware

import (
"net/http"

"github.com/gin-gonic/gin"
"github.com/paycrest/aggregator/ent"
u "github.com/paycrest/aggregator/utils"
)

// DomainWhitelistMiddleware enforces the sender profile's domain_whitelist.
// Must run after auth middleware so "sender" is in context.
// - No sender in context: pass through (e.g. provider routes).
// - Empty whitelist: allow (backward compatibility).
// - Non-empty whitelist: allow only if Origin/Referer domain is in whitelist; otherwise 403.
func DomainWhitelistMiddleware(c *gin.Context) {
val, exists := c.Get("sender")
if !exists || val == nil {
c.Next()
return
}
profile, ok := val.(*ent.SenderProfile)
if !ok || profile == nil {
c.Next()
return
}
whitelist := profile.DomainWhitelist
if len(whitelist) == 0 {
c.Next()
return
}
origin := c.GetHeader("Origin")
referer := c.GetHeader("Referer")
domain := u.ExtractRequestDomain(origin, referer)
if u.IsDomainAllowed(domain, whitelist) {
c.Next()
return
}
// When whitelist is set but no domain could be extracted, block (e.g. missing Origin/Referer).
if domain == "" {
u.APIResponse(c, http.StatusForbidden, "error", "Origin or Referer required when domain whitelist is configured", nil)
c.Abort()
return
}
u.APIResponse(c, http.StatusForbidden, "error", "Domain not allowed", nil)
c.Abort()
}
109 changes: 109 additions & 0 deletions routers/middleware/domain_whitelist_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Domain whitelist middleware tests.
// Run from the aggregator module root: go test ./routers/middleware/... -run TestDomainWhitelist -v
package middleware

import (
"net/http"
"testing"

"github.com/gin-gonic/gin"
"github.com/paycrest/aggregator/ent"
"github.com/paycrest/aggregator/utils/test"
"github.com/stretchr/testify/assert"
)

func TestDomainWhitelistMiddleware(t *testing.T) {
gin.SetMode(gin.TestMode)

t.Run("no sender in context allows request", func(t *testing.T) {
router := gin.New()
router.GET("/test", DomainWhitelistMiddleware, func(c *gin.Context) {
c.Status(http.StatusOK)
})
w, _ := test.PerformRequest(t, "GET", "/test", nil, nil, router)
assert.Equal(t, http.StatusOK, w.Code)
})

t.Run("sender with empty whitelist allows any origin", func(t *testing.T) {
router := gin.New()
router.GET("/test", setSenderProfile(&ent.SenderProfile{DomainWhitelist: nil}), DomainWhitelistMiddleware, func(c *gin.Context) {
c.Status(http.StatusOK)
})
headers := map[string]string{"Origin": "https://any-domain.com"}
w, _ := test.PerformRequest(t, "GET", "/test", nil, headers, router)
assert.Equal(t, http.StatusOK, w.Code)
})

t.Run("sender with empty whitelist slice allows any origin", func(t *testing.T) {
router := gin.New()
router.GET("/test", setSenderProfile(&ent.SenderProfile{DomainWhitelist: []string{}}), DomainWhitelistMiddleware, func(c *gin.Context) {
c.Status(http.StatusOK)
})
headers := map[string]string{"Origin": "https://any-domain.com"}
w, _ := test.PerformRequest(t, "GET", "/test", nil, headers, router)
assert.Equal(t, http.StatusOK, w.Code)
})

t.Run("sender with whitelist allows whitelisted origin", func(t *testing.T) {
router := gin.New()
router.GET("/test", setSenderProfile(&ent.SenderProfile{DomainWhitelist: []string{"example.com"}}), DomainWhitelistMiddleware, func(c *gin.Context) {
c.Status(http.StatusOK)
})
headers := map[string]string{"Origin": "https://example.com"}
w, _ := test.PerformRequest(t, "GET", "/test", nil, headers, router)
assert.Equal(t, http.StatusOK, w.Code)
})

t.Run("sender with whitelist allows whitelisted subdomain", func(t *testing.T) {
router := gin.New()
router.GET("/test", setSenderProfile(&ent.SenderProfile{DomainWhitelist: []string{"example.com"}}), DomainWhitelistMiddleware, func(c *gin.Context) {
c.Status(http.StatusOK)
})
headers := map[string]string{"Origin": "https://app.example.com"}
w, _ := test.PerformRequest(t, "GET", "/test", nil, headers, router)
assert.Equal(t, http.StatusOK, w.Code)
})

t.Run("sender with whitelist blocks non-whitelisted origin", func(t *testing.T) {
router := gin.New()
router.GET("/test", setSenderProfile(&ent.SenderProfile{DomainWhitelist: []string{"example.com"}}), DomainWhitelistMiddleware, func(c *gin.Context) {
c.Status(http.StatusOK)
})
headers := map[string]string{"Origin": "https://evil.com"}
w, _ := test.PerformRequest(t, "GET", "/test", nil, headers, router)
assert.Equal(t, http.StatusForbidden, w.Code)
body := decodeResponseBody(t, w)
assert.Equal(t, "error", body["status"])
assert.Equal(t, "Domain not allowed", body["message"])
})

t.Run("sender with whitelist but no origin or referer returns 403", func(t *testing.T) {
router := gin.New()
router.GET("/test", setSenderProfile(&ent.SenderProfile{DomainWhitelist: []string{"example.com"}}), DomainWhitelistMiddleware, func(c *gin.Context) {
c.Status(http.StatusOK)
})
w, _ := test.PerformRequest(t, "GET", "/test", nil, nil, router)
assert.Equal(t, http.StatusForbidden, w.Code)
body := decodeResponseBody(t, w)
assert.Equal(t, "error", body["status"])
assert.Equal(t, "Origin or Referer required when domain whitelist is configured", body["message"])
})

t.Run("sender with whitelist allows referer when origin missing", func(t *testing.T) {
router := gin.New()
router.GET("/test", setSenderProfile(&ent.SenderProfile{DomainWhitelist: []string{"example.com"}}), DomainWhitelistMiddleware, func(c *gin.Context) {
c.Status(http.StatusOK)
})
headers := map[string]string{"Referer": "https://example.com/page"}
w, _ := test.PerformRequest(t, "GET", "/test", nil, headers, router)
assert.Equal(t, http.StatusOK, w.Code)
})
}

// setSenderProfile returns a handler that sets the given sender profile in context.
func setSenderProfile(profile *ent.SenderProfile) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("sender", profile)
c.Next()
}
}
Loading
Loading