fix: audit phase 1 — payment path, webhooks, order transitions and gateway routing - #261
Open
Moritz1708 wants to merge 6 commits into
Open
Moritz1708 wants to merge 6 commits into
Moritz1708 wants to merge 6 commits into
Conversation
…ernal targets Subscriptions were matched by event type only, so every subscriber of OrderStatusChanged received all customers' events. Events now carry an optional CustomerId (additive contract change) and only that customer's subscriptions receive a delivery; events without it go to nobody. Webhook URLs are restricted to public HTTPS hosts when saved, and every resolved address is checked again at connect time (covers DNS rebinding and service-discovery rewrites). Redirects are not followed, no proxy is used, and a failed delivery stores only the status code, not the response body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed orders Order.Confirm/MarkShipped/MarkDelivered/Cancel return Result and reject invalid transitions; Confirm is only allowed from Created. Rehydration stays unguarded so existing streams still load. PaymentResultProcessor checks the order status before confirming the stock reservation: a captured payment for a cancelled order is refunded instead of turning the order Paid, a failed payment for a cancelled order only closes the saga, and a failure reported for a paid order is logged for review instead of cancelling it. OrderStatusChanged events carry the CustomerId. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every Stripe call carries a deterministic idempotency key (order- or intent-scoped; refunds per refund reason), so a Service Bus redelivery replays the original request instead of creating a second PaymentIntent. Only declines and invalid requests become Result failures; transient faults propagate and are retried by redelivery. The SDK timeout drops to 30 s so one run stays inside the lock-renewal window. A failed capture voids the authorization and keeps the intent id on the record. PaymentRecord transitions return Result and reject invalid moves. The Stripe webhook finds the record by orderId metadata or intent id, answers 503 while the worker has not stored it yet, rejects a missing signature with 400 instead of 500, applies only valid transitions, and logs contradictions as EventId 5001. Adds a non-unique index on payments.TransactionId. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The login endpoint redirected to any returnUrl after sign-in, an open redirect. It now accepts local paths only (same rule as ASP.NET Core's IsLocalUrl) and falls back to "/". The client sends the current page as a relative path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stripe could not reach the webhook: the BFF required a session and CSRF token on /api/**, and the gateway required a JWT on /api/v1/payments/**. The BFF now exposes an anonymous POST /webhooks/stripe (outside /api) that forwards to an anonymous gateway route; the Payment API authenticates it by Stripe signature. Adds the missing /api/v1/admin/coupons gateway route (admin UI got 404). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1 of the 2026-09-23 audit. Six commits, one per area:
OrderStatusChangedreceived all customers' events.OrderStatusChangedIntegrationEventnow carries an optionalCustomerId(additive), and only that customer's subscriptions get a delivery; events without it go to nobody. Target URLs are restricted to public HTTPS hosts on save (WebhookTargetPolicy) and every resolved address is checked again at connect time (WebhookTargetConnector), which also covers DNS rebinding and service-discovery rewrites. Redirects are not followed, no proxy is used, and failed deliveries store only the status code instead of the target's response body.Ordertransitions returnResultand reject invalid moves (Confirmonly fromCreated); rehydration stays unguarded so existing streams load.PaymentResultProcessorchecks the order status before confirming the reservation: a captured payment for a cancelled order is refunded instead of turning the orderPaid; a failed payment for a paid order is logged for review instead of cancelling it.os-pi-create-{orderId},os-pi-capture/cancel-{intent},os-refund-{intent}-{reference}), so a redelivery replays the original request instead of creating a second PaymentIntent. Only declines and invalid requests becomeResultfailures; network errors, 5xx, 401/403/409/429 and idempotency errors propagate and are retried through Service Bus. A failed capture voids the authorization and keeps the intent id on the record.PaymentRecordtransitions returnResult. The webhook finds records byorderIdmetadata or intent id, answers 503 while the worker has not stored the record yet, rejects a missing signature with 400 (was 500), applies only valid transitions and logs contradictions as EventId 5001. Stripe client timeout 30 s (SDK default 80 s). MigrationAddPaymentTransactionIdIndex(index only)./bff/loginaccepts only a localreturnUrl(open redirect); the client sends a relative path.POST /webhooks/stripeon the BFF (outside/api, so neither session policy nor CSRF applies) forwarding to an anonymous gateway route; the Payment API authenticates by Stripe signature. Adds the missing/api/v1/admin/couponsroute (admin UI got 404).docs/operations.md(Stripe webhook endpoint, 24 h DLQ replay rule for payment queues),CHANGELOG.md.Type of change
Checklist
dotnet build OrderSphere.slnx) — 0 errors; only the pre-existing ASPIRE010 warningdotnet test) — 1,267 passed, 130 newAddPaymentTransactionIdIndex, additive index)Breaking changes
No UI contract changes. Behavioural changes operators need to know:
https://<bff-host>/webhooks/stripe.payment-requests,order-confirmation-failedandrefund-requestedonly within 24 h (Stripe idempotency-key retention); afterwards check the intent in the Stripe dashboard first.Test plan
LocalReturnUrl, Stripe route through the BFF, order/payment state machines, Stripe provider idempotency keys and error classification, webhook 400/503/200 paths)./bff/login?returnUrl=https://evil.examplelands on/; admin coupon page loads; webhook subscription tohttps://127.0.0.1/is rejected; two customers withOrderStatusChangedsubscriptions only receive their own events.stripe listen --forward-to https://localhost:<bff>/webhooks/stripe+ a checkout with Stripe configured → webhook 200 and "reconciled" log. (stripe triggerintents carry noorderIdand are acknowledged as ignored.)References
Closes #260
🤖 Generated with Claude Code