Skip to content

fix product route access#1134

Merged
BilalG1 merged 8 commits intodevfrom
fix-product-access
Jan 27, 2026
Merged

fix product route access#1134
BilalG1 merged 8 commits intodevfrom
fix-product-access

Conversation

@BilalG1
Copy link
Contributor

@BilalG1 BilalG1 commented Jan 26, 2026

Summary by CodeRabbit

  • Security

    • Added client-side access checks on payments endpoints and expanded customer-type handling (including a new "custom" type).
  • SDK / Client

    • Client interface methods now accept explicit request types (client/server/admin) to route requests appropriately.
  • Server

    • New server-side product listing to support server requests and caching.
  • Tests

    • E2E tests updated to use a fast sign-up flow and pass authentication tokens for authorized requests.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Jan 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
stack-backend Ready Ready Preview, Comment Jan 27, 2026 6:03pm
stack-dashboard Ready Ready Preview, Comment Jan 27, 2026 6:03pm
stack-demo Ready Ready Preview, Comment Jan 27, 2026 6:03pm
stack-docs Ready Ready Preview, Comment Jan 27, 2026 6:03pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 26, 2026

📝 Walkthrough

Walkthrough

Adds client-scoped access checks to multiple payments endpoints by passing full request context into route handlers and invoking ensureClientCanAccessCustomer (now recognizing a "custom" type that is denied). Also updates client/server request routing, server error-wrapping, and many e2e tests to use Auth.fastSignUp and tokenized requests.

Changes

Cohort / File(s) Summary
Payment Route Handlers
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts, apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/route.ts, apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts
Route handlers now accept fullReq as a second param and run ensureClientCanAccessCustomer when req.auth.type === "client", using fullReq.auth?.user, tenancy, and a custom forbidden message before continuing existing logic.
Authorization Guard Utility
apps/backend/src/lib/payments.tsx
ensureClientCanAccessCustomer signature extended to accept `customerType: "user"
Client / Server Interfaces
packages/stack-shared/src/interface/client-interface.ts, packages/stack-shared/src/interface/server-interface.ts
getItem and listProducts gain `requestType: "client"
Server App Implementations / Template
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts, packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Server app: new listProducts method and internal calls now pass "server" requestType. Client app: listProducts prefers current user's internal session when present.
E2E Tests & Helpers
apps/e2e/tests/backend/.../*.test.ts, apps/e2e/tests/backend/backend-helpers.ts, apps/e2e/tests/js/payments.test.ts
Many tests replaced User.create() with Auth.fastSignUp() (returns userId, accessToken, refreshToken); tests pass userAuth or change accessType between client/server; create-purchase-url now called with server access where applicable.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Route as Payments Route Handler
  participant Guard as ensureClientCanAccessCustomer
  participant Tenancy as Tenancy/DB

  Client->>Route: HTTP GET/POST (auth.type = "client")
  Note over Route,Guard: Route receives fullReq (includes fullReq.auth?.user)
  Route->>Guard: ensureClientCanAccessCustomer({ customerType, customerId, user, tenancy, forbiddenMessage })
  Guard->>Tenancy: verify ownership or short-circuit for "custom"
  Tenancy-->>Guard: validation result or error
  Guard-->>Route: OK or throw Forbidden
  alt Guard OK
    Route->>Tenancy: continue processing (fetch/create)
    Tenancy-->>Route: data
    Route-->>Client: 200 response
  else Guard throws
    Route-->>Client: 403 Forbidden
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through routes with tiny feet,
Guarding clients so access is neat.
"Custom" gets a firm, polite "nope",
Tests now carry tokens and hope.
🥕🔐

🚥 Pre-merge checks | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description contains only a boilerplate comment reminding contributors to read CONTRIBUTING.md, with no actual implementation details, rationale, or explanation of the changes made. Add a proper description explaining what was fixed, why the changes were necessary, which endpoints were modified, and how the new access validation works.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'fix product route access' is vague and generic, using non-descriptive terms that don't convey the specific nature of the changes beyond referencing a general product route access issue. Make the title more specific by describing the exact access control being implemented, e.g., 'Add client access validation to product and payment endpoints' or 'Implement customer access guards for payment routes'.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 26, 2026

Greptile Overview

Greptile Summary

This PR fixes a security issue by adding access control checks to payment route handlers. The changes ensure that client requests can only access their own user or team products/items.

Key Changes:

  • Added ensureClientCanAccessCustomer checks to GET handlers for items and products routes
  • Extended ensureClientCanAccessCustomer to handle "custom" customer types by denying client access
  • Routes now properly validate client access before returning payment data

Security Impact:
The PR addresses an authorization vulnerability where clients could potentially access products and items belonging to other users or teams by manipulating the customer_id parameter in the URL.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The changes are straightforward security improvements that add proper authorization checks. The implementation follows existing patterns in the codebase (e.g., billing route at line 39-46), uses an existing helper function, and correctly handles all customer types including the edge case for "custom" customers.
  • No files require special attention

Important Files Changed

Filename Overview
apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts Added client access control check to prevent unauthorized access to customer items
apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/route.ts Added client access control check to prevent unauthorized access to customer products
apps/backend/src/lib/payments.tsx Extended ensureClientCanAccessCustomer to handle custom customer types by denying client access

Sequence Diagram

sequenceDiagram
    participant Client
    participant RouteHandler
    participant ensureClientCanAccessCustomer
    participant DB
    
    Client->>RouteHandler: GET /payments/items/[type]/[id]/[item_id]
    RouteHandler->>RouteHandler: Check auth.type === "client"
    RouteHandler->>ensureClientCanAccessCustomer: Validate access
    
    alt customerType === "custom"
        ensureClientCanAccessCustomer->>Client: 403 Forbidden
    else customerType === "user"
        ensureClientCanAccessCustomer->>ensureClientCanAccessCustomer: Check customerId === currentUser.id
        alt IDs don't match
            ensureClientCanAccessCustomer->>Client: 403 Forbidden
        else IDs match
            ensureClientCanAccessCustomer->>RouteHandler: Access granted
        end
    else customerType === "team"
        ensureClientCanAccessCustomer->>DB: Check team_admin permission
        alt No permission
            ensureClientCanAccessCustomer->>Client: 403 Forbidden
        else Has permission
            ensureClientCanAccessCustomer->>RouteHandler: Access granted
        end
    end
    
    RouteHandler->>DB: Fetch item/product data
    DB->>RouteHandler: Return data
    RouteHandler->>Client: 200 OK with data
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

No files reviewed, no comments

Edit Code Review Agent Settings | Greptile

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.

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (15)
apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts (5)

146-202: Missing userAuth on client requests may cause test failures.

This test uses Auth.fastSignUp() but does not pass userAuth on the client DELETE (line 176) and GET (line 188) requests. Given the PR's goal is to add client-access guards that validate whether the client can access the customer, these requests may now require authentication tokens to succeed.

🐛 Proposed fix
+  const { userId, accessToken, refreshToken } = await Auth.fastSignUp();
-  const { userId } = await Auth.fastSignUp();
   await niceBackendFetch(`/api/v1/payments/products/user/${userId}`, {
     method: "POST",
     accessType: "server",
     body: {
       product_id: "pro-plan",
     },
   });

   const cancelResponse = await niceBackendFetch(`/api/v1/payments/products/user/${userId}/pro-plan`, {
     method: "DELETE",
     accessType: "client",
+    userAuth: { accessToken, refreshToken },
   });
   expect(cancelResponse).toMatchInlineSnapshot(`
     NiceResponse {
       "status": 200,
       "body": { "success": true },
       "headers": Headers { <some fields may have been hidden> },
     }
   `);

   const listResponse = await niceBackendFetch(`/api/v1/payments/products/user/${userId}`, {
     accessType: "client",
+    userAuth: { accessToken, refreshToken },
   });

225-248: Test for unauthorized access may need authentication tokens.

This test verifies that a client cannot cancel someone else's subscription. However, the second user's accessToken and refreshToken are not passed in the cancel request (line 237). If the backend now requires authentication to determine "who is making the request", the test may not correctly simulate an authenticated but unauthorized user.

🐛 Proposed fix
-  const { userId: userId2 } = await Auth.fastSignUp();
+  const { userId: userId2, accessToken, refreshToken } = await Auth.fastSignUp();
   expect(userId2).not.toEqual(userId1);

   const cancelResponse = await niceBackendFetch(`/api/v1/payments/products/user/${userId1}/pro-plan`, {
     method: "DELETE",
     accessType: "client",
+    userAuth: { accessToken, refreshToken },
   });

314-356: Missing userAuth on client requests for cancellation and listing.

Similar to other tests, the client DELETE (line 330) and GET (line 342) requests don't include userAuth. The test expects these to succeed, but if client-access guards now require authentication, these requests will fail.


378-398: Unused destructured variables and missing userAuth.

The accessToken and refreshToken are not used. Additionally, the client DELETE request (line 387) lacks userAuth.


421-443: Unused destructured variables and missing userAuth.

Line 421 destructures variables that aren't used. The client DELETE (line 432) likely needs userAuth for the new access guards.

apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts (7)

52-67: Missing userAuth on client request.

The test uses Auth.fastSignUp() but doesn't pass userAuth to the client request. If the backend now enforces client access guards, this request may fail without authentication.

🐛 Proposed fix
-  const { userId } = await Auth.fastSignUp();
+  const { userId, accessToken, refreshToken } = await Auth.fastSignUp();
   const response = await niceBackendFetch(`/api/latest/payments/items/user/${userId}/test-item`, {
     accessType: "client",
+    userAuth: { accessToken, refreshToken },
   });

82-100: Missing userAuth on client request.

Same issue - client request without authentication tokens.


115-138: Missing userAuth on client request.

Client request lacks authentication, which may be required by the new access guards.


181-195: Missing userAuth on client GET request.

Line 190 makes a client request without userAuth.

🐛 Proposed fix
-  const { userId } = await Auth.fastSignUp();
+  const { userId, accessToken, refreshToken } = await Auth.fastSignUp();

   const post1 = await niceBackendFetch(`/api/latest/payments/items/user/${userId}/test-item/update-quantity?allow_negative=false`, {
     method: "POST",
     accessType: "admin",
     body: { delta: 2 },
   });
   expect(post1.status).toBe(200);

   const get1 = await niceBackendFetch(`/api/latest/payments/items/user/${userId}/test-item`, {
     accessType: "client",
+    userAuth: { accessToken, refreshToken },
   });

210-224: Missing userAuth on client GET request.

Line 219's client request lacks authentication.


239-255: Missing userAuth on client GET request.

Line 250's client request lacks authentication.


407-438: Missing userAuth on client GET request.

Line 424's client request lacks authentication.

apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts (3)

142-149: Missing userAuth on client request.

The client request at line 145 lacks authentication tokens. If the backend now requires client authentication for the items endpoint, this test will fail.

🐛 Proposed fix
-  const { userId } = await Auth.fastSignUp();
+  const { userId, accessToken, refreshToken } = await Auth.fastSignUp();

   // Before webhook: quantity should be 0
   const getBefore = await niceBackendFetch(`/api/latest/payments/items/user/${userId}/${itemId}`, {
     accessType: "client",
+    userAuth: { accessToken, refreshToken },
   });

And similarly for line 208:

   const getAfter = await niceBackendFetch(`/api/latest/payments/items/user/${userId}/${itemId}`, {
     accessType: "client",
+    userAuth: { accessToken, refreshToken },
   });

522-528: Missing userAuth on client requests.

Multiple client requests in this test (lines 524, 591, 601) lack userAuth.


635-641: Missing userAuth on client requests.

Client requests at lines 637, 704, 744 lack authentication tokens.

🧹 Nitpick comments (7)
packages/stack-shared/src/interface/client-interface.ts (1)

1801-1833: The (this as any).sendServerRequest as never pattern is a type-safety concern.

This pattern accesses a method (sendServerRequest) that only exists on the child class (StackServerInterface) from the parent class (StackClientInterface). While this matches existing patterns in the file (e.g., listProjectApiKeys), it has risks:

  1. Runtime error if getItem is called with requestType !== "client" on a StackClientInterface instance (not a StackServerInterface)
  2. The as never cast suppresses all type checking on the return value

Consider adding a runtime guard or documenting this constraint clearly:

+    // Note: requestType !== "client" requires this to be a StackServerInterface instance
     const sendRequest = (requestType === "client" ? this.sendClientRequest : (this as any).sendServerRequest as never).bind(this);

Alternatively, consider refactoring to make sendServerRequest available on the base class (returning an error for non-server instances) or using a discriminated check.

apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts (6)

31-38: Unused destructured variables.

The accessToken and refreshToken are destructured but not used in this test. The test expects a 401 response for client requests trying to grant a product, so authentication isn't needed to trigger the access type rejection.

♻️ Suggested fix
-  const { userId, accessToken, refreshToken } = await Auth.fastSignUp();
+  const { userId } = await Auth.fastSignUp();

271-291: Unused destructured variables.

The accessToken and refreshToken from the result of Auth.fastSignUp() on line 271 are not used in subsequent requests.


568-576: Unused destructured variables.

accessToken and refreshToken are destructured but only userId is used in subsequent requests.


765-780: Unused destructured variables.

accessToken and refreshToken are destructured but not used since the subsequent request uses accessType: "server".


804-821: Unused destructured variables.

accessToken and refreshToken are destructured but the server request doesn't need them.


844-872: Unused destructured variables.

Same pattern - accessToken and refreshToken are destructured but only server-access requests follow.

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.

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)

1762-1774: Apply consistent session derivation pattern to listInvoices method.

The listProducts method correctly derives the session from the current user's _internalSession when available, but listInvoices (line 1776) still uses await this._getSession() directly. Since both are payment-related endpoints requiring proper access guards, they should follow the same pattern.

Update listInvoices to use:

const currentUser = await this.getUser();
const session = currentUser?._internalSession ?? await this._getSession();
🧹 Nitpick comments (7)
apps/e2e/tests/backend/endpoints/api/v1/payments/before-offer-to-product-rename/outdated--validate-code.test.ts (1)

2-2: Remove unused User import.

The User import is no longer used in this file after migrating to Auth.fastSignUp().

Suggested fix
-import { Auth, Payments, Project, User, niceBackendFetch } from "../../../../../backend-helpers";
+import { Auth, Payments, Project, niceBackendFetch } from "../../../../../backend-helpers";
apps/e2e/tests/backend/endpoints/api/v1/payments/before-catalog-to-product-line-rename/outdated--validate-code.test.ts (1)

6-6: Unused User import.

User is imported but no longer used in this file after migrating to Auth.fastSignUp().

Suggested fix
-import { Auth, Payments, Project, User, niceBackendFetch } from "../../../../../backend-helpers";
+import { Auth, Payments, Project, niceBackendFetch } from "../../../../../backend-helpers";
apps/e2e/tests/backend/endpoints/api/v1/payments/purchase-session.test.ts (2)

3-3: Unused User import.

User is imported but no longer used in this file after migrating to Auth.fastSignUp().

Suggested fix
-import { Auth, Payments, Project, User, niceBackendFetch } from "../../../../backend-helpers";
+import { Auth, Payments, Project, niceBackendFetch } from "../../../../backend-helpers";

156-167: Consider passing userAuth explicitly for consistency.

Other tests in this file explicitly pass userAuth to niceBackendFetch for client requests, but this test relies on the implicit context from Auth.fastSignUp(). While both approaches work (since niceBackendFetch falls back to backendContext.value.userAuth), explicit token propagation is clearer and more consistent with the authenticated flow pattern used elsewhere in this file.

apps/e2e/tests/backend/endpoints/api/v1/payments/validate-code.test.ts (1)

2-2: Unused User import.

User is imported but no longer used in this file after migrating to Auth.fastSignUp().

Suggested fix
-import { Auth, Payments, Project, User, niceBackendFetch } from "../../../../backend-helpers";
+import { Auth, Payments, Project, niceBackendFetch } from "../../../../backend-helpers";
apps/e2e/tests/backend/endpoints/api/v1/payments/before-offer-to-product-rename/outdated--purchase-session.test.ts (2)

3-3: Unused User import.

User is imported but no longer used in this file after migrating to Auth.fastSignUp().

Suggested fix
-import { Auth, Payments, Project, User, niceBackendFetch } from "../../../../../backend-helpers";
+import { Auth, Payments, Project, niceBackendFetch } from "../../../../../backend-helpers";

158-169: Consider passing userAuth explicitly for consistency.

Similar to the main purchase-session.test.ts, this test relies on implicit context rather than explicit token propagation. Consider aligning with the pattern used in tests at lines 456-497 and 580-622 for consistency.

@BilalG1 BilalG1 requested a review from N2D4 January 26, 2026 20:39
@BilalG1 BilalG1 assigned N2D4 and unassigned BilalG1 Jan 26, 2026
@github-actions github-actions bot assigned BilalG1 and unassigned N2D4 Jan 27, 2026
@BilalG1 BilalG1 enabled auto-merge (squash) January 27, 2026 17:08
@BilalG1 BilalG1 merged commit 4e45aed into dev Jan 27, 2026
27 checks passed
@BilalG1 BilalG1 deleted the fix-product-access branch January 27, 2026 18:30
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.

2 participants