Skip to content

feat(aggregator): enforce domain whitelist for sender API routes#723

Open
sundayonah wants to merge 1 commit intomainfrom
feat/sender-domain-whitelist-enforcement
Open

feat(aggregator): enforce domain whitelist for sender API routes#723
sundayonah wants to merge 1 commit intomainfrom
feat/sender-domain-whitelist-enforcement

Conversation

@sundayonah
Copy link
Collaborator

@sundayonah sundayonah commented Mar 10, 2026

Description

This PR implements enforcement of the existing domain whitelist on sender API routes so that only requests from allowed domains (or any domain when the whitelist is empty) can access sensitive payment APIs.

Background: The SenderProfile table already had a domain_whitelist field, but it was not enforced. CORS used Access-Control-Allow-Origin: * and there was no validation of Origin/Referer. Any domain could call the sender API with a valid API key.

Changes:

  • Domain validation middleware (routers/middleware/domain_whitelist.go): After auth, reads the sender profile from context. If domain_whitelist is non-empty, it derives the request domain from Origin or Referer, checks it against the whitelist (exact and subdomain), and returns 403 Forbidden when not allowed or when Origin/Referer is missing. Empty whitelist continues to allow all domains (backward compatible).
  • Domain helpers (utils/domain.go): ExtractRequestDomain(origin, referer) and IsDomainAllowed(host, whitelist) with subdomain support and case normalization.
  • Route wiring (routers/index.go): DomainWhitelistMiddleware is applied only to /v1/sender/ and /v2/sender/ (after DynamicAuthMiddleware, before OnlySenderMiddleware). Provider and other routes are unchanged.
  • CORS (routers/middleware/cors.go): For sender paths when API-Key is present, Access-Control-Allow-Origin is set only when the request Origin is in the sender’s whitelist; otherwise the header is omitted so the browser blocks the response. Non-sender paths and requests without API-Key keep previous behavior (e.g. *).
  • Tests: utils/domain_test.go for ExtractRequestDomain and IsDomainAllowed; routers/middleware/domain_whitelist_test.go for the middleware (all three acceptance criteria covered). Duplicate decodeResponseBody in middleware tests removed in favor of the helper in request_test.go.
  • Ent runtime: Guard in ent/runtime/runtime.go so init does not panic when PaymentOrder has no hooks (enables tests that load ent).

Behaviour summary:

Scenario Result
Sender with configured whitelist + request from whitelisted domain (or subdomain) Allowed
Sender with configured whitelist + request from non-whitelisted domain 403, "Domain not allowed"
Sender with empty whitelist + any domain Allowed (backward compatibility)
Sender with whitelist but no Origin/Referer 403, "Origin or Referer required when domain whitelist is configured"
No sender in context (e.g. provider routes) No domain check; request proceeds

Breaking changes: None for callers that use a non-empty whitelist correctly. Senders that previously had a whitelist configured but relied on it not being enforced will now get 403 for non-whitelisted origins; they must add those origins to the whitelist or clear the whitelist to allow all.

Alternatives considered: Keeping CORS as * and only enforcing in middleware (403) was considered sufficient for security; CORS was updated as well for defense-in-depth and correct behaviour with credentials.


References

Closes #522


Testing

Unit / package tests:

  • From repo root (e.g. paycrest/aggregator):

    • go test ./utils/... -run "TestExtractRequestDomain|TestIsDomainAllowed" -v
    • go test ./routers/middleware/... -run TestDomainWhitelist -v
  • Or run all tests: go test ./...

  • This change adds test coverage for new/changed/fixed functionality

  • with valid domain

image
  • with invalid domain
Screenshot 2026-03-11 091150
  • with empty domain
image

Checklist

  • I have added documentation and tests for new/changed/fixed functionality in this PR
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not main

By submitting a PR, I agree to Paycrest's Contributor Code of Conduct and Contribution Guide.

Summary by CodeRabbit

  • New Features

    • Added domain whitelist validation for sender endpoints. Senders can now restrict API access to a specific list of allowed domains by validating Origin and Referer headers. Requests from non-whitelisted domains are rejected with HTTP 403.
  • Tests

    • Added comprehensive test coverage for domain whitelist middleware and domain extraction/validation utilities to verify allowlisting behavior across various scenarios.

- Add domain validation middleware (Origin/Referer vs sender whitelist)
- Add utils/domain.go (ExtractRequestDomain, IsDomainAllowed, subdomain support)
- Apply DomainWhitelistMiddleware to /v1/sender/ and /v2/sender/
- Update CORS to allow only whitelisted origins when API-Key present on sender paths
- Empty whitelist allows all domains (backward compatible)
- Add tests for domain utils and domain whitelist middleware
- Guard ent runtime init when PaymentOrder has no hooks (fix test panic)
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

📝 Walkthrough

Walkthrough

This PR implements domain whitelist validation middleware to enforce sender profile domain restrictions, while refactoring PaymentOrder hook handling by removing exported Hooks and simplifying error handling in default initialization methods.

Changes

Cohort / File(s) Summary
PaymentOrder Hook Refactoring
ent/client.go, ent/paymentorder/paymentorder.go, ent/runtime/runtime.go
Removed exported Hooks variable and hook augmentation in client; simplified PaymentOrderClient.Hooks() to return existing hooks without merging defaults; removed corresponding hook initialization from runtime.
Default Initialization Simplification
ent/paymentorder_create.go, ent/paymentorder_update.go
Removed error return types from defaults() methods; eliminated nil-checks and direct error handling in Save flow; simplified default value assignments.
Domain Whitelist Middleware
routers/middleware/domain_whitelist.go, routers/middleware/domain_whitelist_test.go
New middleware that validates request origin/referer against sender profile domain whitelist; extracts domain from headers and enforces 403 responses for unauthorized domains; includes comprehensive test coverage.
Domain Utilities
utils/domain.go, utils/domain_test.go
New utility functions for domain extraction (ExtractRequestDomain) and validation (IsDomainAllowed); supports subdomain matching and case normalization; includes full unit test suite.
Router Integration
routers/index.go
Added DomainWhitelistMiddleware to v1 and v2 sender routes after auth but before OnlySenderMiddleware.

Sequence Diagram(s)

sequenceDiagram
    participant Client as HTTP Client
    participant Router as Router/Handler
    participant AuthMW as DynamicAuthMiddleware
    participant WhitelistMW as DomainWhitelistMiddleware
    participant Utils as Domain Utils
    participant DB as Database

    Client->>Router: Request with Origin/Referer
    Router->>AuthMW: Process
    AuthMW->>DB: Validate credentials
    DB-->>AuthMW: Sender profile
    AuthMW->>Router: Set sender in context
    Router->>WhitelistMW: Process
    alt Sender exists & whitelist non-empty
        WhitelistMW->>Utils: ExtractRequestDomain(origin, referer)
        Utils-->>WhitelistMW: Extracted domain
        WhitelistMW->>Utils: IsDomainAllowed(domain, whitelist)
        Utils-->>WhitelistMW: Validation result
        alt Domain allowed
            WhitelistMW->>Router: Continue
            Router-->>Client: Process request normally
        else Domain denied
            WhitelistMW-->>Client: 403 Forbidden (Domain not allowed)
        end
    else No sender or empty whitelist
        WhitelistMW->>Router: Continue (backward compatible)
        Router-->>Client: Process request normally
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • chibie
  • 5ran6
  • onahprosper

Poem

🐰 A whitelist emerges, domains now secure,
Blocking unwanted guests at the door so pure,
With defaults simplified and hooks trimmed clean,
The safest API I ever have seen! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Most changes are directly scoped to #522. However, the ent runtime modifications (removing PaymentOrder hooks initialization) appear tangential to domain whitelist enforcement, though justified as avoiding test panics. Clarify whether ent/runtime/runtime.go changes are essential for this PR's domain whitelist work or if they address a separate concern that should be in a different PR.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: enforcing domain whitelist for sender API routes, which is the primary objective of the PR.
Linked Issues check ✅ Passed The PR fully implements all three acceptance criteria from #522: whitelisted domains are allowed via middleware and CORS logic, non-whitelisted domains receive 403 responses, and empty whitelists allow any domain for backward compatibility.
Description check ✅ Passed The PR description comprehensively covers purpose, implementation details, testing approach, and acceptance criteria alignment with structured tables and testing evidence.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/sender-domain-whitelist-enforcement

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ent/paymentorder_create.go (1)

603-697: ⚠️ Potential issue | 🔴 Critical

Restore nil-checks for runtime-wired PaymentOrder defaults to prevent panics on uninitialized package vars.

The defaults() method calls package-level function variables (DefaultCreatedAt, DefaultUpdatedAt, DefaultAmountPaid, etc.) without checking if they are initialized. These variables are wired at runtime via ent/runtime/runtime.go using type-asserted assignments from schema descriptors. If initialization is delayed or fails, calling a nil function will panic instead of returning a recoverable error.

This affects both:

  • PaymentOrderCreate.Save() (line 605) which calls _c.defaults() fire-and-forget
  • PaymentOrderCreateBulk.Save() (line 2586) which calls builder.defaults() on each builder without error handling

Reintroduce nil-checks by making defaults() return an error and propagate it in both Save() methods:

Suggested pattern
 func (_c *PaymentOrderCreate) Save(ctx context.Context) (*PaymentOrder, error) {
-	_c.defaults()
+	if err := _c.defaults(); err != nil {
+		return nil, err
+	}
 	return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks)
 }

 func (_c *PaymentOrderCreate) defaults() error {
 	if _, ok := _c.mutation.CreatedAt(); !ok {
+		if paymentorder.DefaultCreatedAt == nil {
+			return fmt.Errorf("ent: uninitialized default for field %q", paymentorder.FieldCreatedAt)
+		}
 		v := paymentorder.DefaultCreatedAt()
 		_c.mutation.SetCreatedAt(v)
 	}
+	// Apply same guard pattern to all function-type defaults
+	return nil
 }
🧹 Nitpick comments (1)
routers/middleware/domain_whitelist.go (1)

35-46: Consider reordering checks for clearer control flow.

The current logic is correct, but the flow is slightly confusing: IsDomainAllowed is called before checking if the domain is empty, even though an empty domain will always fail the allowlist check. Consider reordering to check for empty domain first:

♻️ Optional: Clearer control flow
-	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
 	}
+	if u.IsDomainAllowed(domain, whitelist) {
+		c.Next()
+		return
+	}
 	u.APIResponse(c, http.StatusForbidden, "error", "Domain not allowed", nil)
 	c.Abort()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@routers/middleware/domain_whitelist.go` around lines 35 - 46, Reorder the
checks in the domain whitelist middleware so the empty-domain case is handled
before calling IsDomainAllowed: first test if domain == "" and call
u.APIResponse(c, http.StatusForbidden, "error", "Origin or Referer required when
domain whitelist is configured", nil) followed by c.Abort() and return; only
after that call u.IsDomainAllowed(domain, whitelist) and allow the request
(c.Next()/return) or respond with the "Domain not allowed" message and abort.
Ensure you update the control flow surrounding the domain variable,
IsDomainAllowed, APIResponse, c.Abort, and c.Next to reflect this new order.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@routers/middleware/domain_whitelist.go`:
- Around line 35-46: Reorder the checks in the domain whitelist middleware so
the empty-domain case is handled before calling IsDomainAllowed: first test if
domain == "" and call u.APIResponse(c, http.StatusForbidden, "error", "Origin or
Referer required when domain whitelist is configured", nil) followed by
c.Abort() and return; only after that call u.IsDomainAllowed(domain, whitelist)
and allow the request (c.Next()/return) or respond with the "Domain not allowed"
message and abort. Ensure you update the control flow surrounding the domain
variable, IsDomainAllowed, APIResponse, c.Abort, and c.Next to reflect this new
order.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d06ed2c1-1433-4b3d-930e-dbb8a303bddf

📥 Commits

Reviewing files that changed from the base of the PR and between 5f3ecff and 1c9799f.

📒 Files selected for processing (10)
  • ent/client.go
  • ent/paymentorder/paymentorder.go
  • ent/paymentorder_create.go
  • ent/paymentorder_update.go
  • ent/runtime/runtime.go
  • routers/index.go
  • routers/middleware/domain_whitelist.go
  • routers/middleware/domain_whitelist_test.go
  • utils/domain.go
  • utils/domain_test.go
💤 Files with no reviewable changes (2)
  • ent/runtime/runtime.go
  • ent/paymentorder/paymentorder.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement and enforce domain whitelist validation

1 participant