Skip to content

[PM-32480] Add endpoint for Stripe billing portal session#7227

Open
cyprain-okeke wants to merge 10 commits intomainfrom
billing/pm-32480/Endpoint-for-returning-stripe-portal-url
Open

[PM-32480] Add endpoint for Stripe billing portal session#7227
cyprain-okeke wants to merge 10 commits intomainfrom
billing/pm-32480/Endpoint-for-returning-stripe-portal-url

Conversation

@cyprain-okeke
Copy link
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-32480

📔 Objective

  • Adds new POST /account/billing/vnext/portal-session endpoint that returns a Stripe Customer Portal URL
  • Enables Premium users with active or past_due subscriptions to manage or cancel their subscriptions via Stripe's hosted portal
  • Implements security validations to ensure only eligible users can access the portal

Changes

API Layer

  • Added CreatePortalSessionAsync endpoint to AccountBillingVNextController
  • Created PortalSessionRequest model with URL validation and XSS prevention
  • Created PortalSessionResponse model for returning the portal URL

Core Layer

  • Implemented ICreateBillingPortalSessionCommand with comprehensive business logic validation:
  • Verifies user has Stripe customer ID
  • Verifies user has active subscription
  • Validates subscription status (only active or past_due allowed)
  • Creates Stripe billing portal session with custom return URL
  • Added CreateBillingPortalSessionAsync to IStripeAdapter and StripeAdapter
  • Registered command in dependency injection container

Test Plan

1. **Success Case**: Call endpoint with authenticated Premium user (active subscription)                                        
   - Verify portal URL is returned                                                                                              
   - Verify URL can be opened and displays subscription management options                                                      
                                                                                                                              
2. **Past Due Case**: Test with Premium user in past_due status                                                                 
    - Verify portal URL is returned                                                                                                                                                                                                                                │
3. **Error Cases**: Test with users who:                                                                                        
   - Have no Stripe customer ID → Returns 400 BadRequest                                                                        
   - Have no subscription → Returns 400 BadRequest                                                                              
   - Have canceled/incomplete subscription → Returns 400 BadRequest                                                             
   - Subscription not found in Stripe → Returns 400 BadRequest                                                                  
                                                                                                                              
4. **Validation**: Test request validation                                                                                      
   - Missing return URL → Returns 400                                                                                           
   - Invalid URL format → Returns 400                                                                                           
   - Non-HTTP(S) scheme (javascript:, data:, etc.) → Returns 400             

📸 Screenshots

@cyprain-okeke cyprain-okeke requested a review from a team as a code owner March 16, 2026 11:37
@github-actions
Copy link
Contributor

github-actions bot commented Mar 16, 2026

Logo
Checkmarx One – Scan Summary & Details3fc65c01-822d-4ec4-958a-e5af1ab6cc96


New Issues (1) Checkmarx found the following issues in this Pull Request
# Severity Issue Source File / Package Checkmarx Insight
1 MEDIUM CSRF /src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs: 131
detailsMethod at line 131 of /src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs gets a parameter from a user request from request. Thi...
Attack Vector

@codecov
Copy link

codecov bot commented Mar 16, 2026

Codecov Report

❌ Patch coverage is 98.79518% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 57.55%. Comparing base (11af4dc) to head (3ed84b8).

Files with missing lines Patch % Lines
.../Billing/Services/Implementations/StripeAdapter.cs 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7227      +/-   ##
==========================================
+ Coverage   57.50%   57.55%   +0.04%     
==========================================
  Files        2032     2034       +2     
  Lines       89544    89625      +81     
  Branches     7960     7961       +1     
==========================================
+ Hits        51496    51582      +86     
+ Misses      36202    36198       -4     
+ Partials     1846     1845       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

/// </summary>
[Required]
[MaxLength(2000)]
public string? ReturnUrl { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

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

❓ I don't see this mentioned as a request property in the linked JIRA ticket, or in the tech breakdown. When the mobile client calls this endpoint, what value should this be and where does it come from?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The ReturnUrl is required by Stripe's billing portal session API (see
https://docs.stripe.com/api/customer_portal/sessions/create). When users finish managing their subscription in the Stripe
portal, they click a "Return to Bitwarden" link that redirects to this URL.

For mobile clients, this should be a deep link (e.g., bitwarden://billing-return or similar app-specific URL scheme) that opens
the app. The mobile client would:

  1. Open the portal URL in an in-app browser/web view
  2. After the user completes their session, Stripe redirects to the ReturnUrl
  3. The deep link triggers the mobile app to close the web view and return to the billing screen

This is standard practice for Stripe portal integration on mobile - the same pattern is used across other platforms (see
https://docs.stripe.com/customer-management/integrate-customer-portal).

}

// Prevent open redirect vulnerabilities by restricting to HTTP(S) schemes
if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
Copy link
Contributor

Choose a reason for hiding this comment

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

If ReturnUrl is a deeplink like bitwarden://foo, this validation will trigger an error response. Is that intentional?

Copy link
Contributor Author

@cyprain-okeke cyprain-okeke Mar 16, 2026

Choose a reason for hiding this comment

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

remove the returnurl from the request, instead pass it from the server base on client

@sonarqubecloud
Copy link

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