Block guest-role tokens from online payment routes (RequireNonGuest middleware)

Guests (account_role='guest') have no login flow and never receive a JWT
normally, so this is defense-in-depth: any token whose role claim is 'guest'
(forged/minted guest tokens or future changes) is refused 403 before the money
handlers run. The check reads role from context ONLY — real guests are seeded
with account_type='email', so account_type is never the discriminator. A
missing role passes through (RequireAuth guarantees presence; same trust model
as isVerifiedRole).

Wired onto all user-facing money routes: booking payment, apply-redemption,
payment-lock POST/DELETE, tip, gift-card redeem and gift-card buy. Admin and
till routes (RequireAdmin) are untouched — admin can never be guest. The
payment-methods routes gain RequireVerified (verified_email, admin) alongside,
so only verified accounts can manage saved cards.

Tests: 6 middleware tests (reject guest, allow verified/unverified/admin/
affiliate/missing-role) + 4 integration tests (guest 403 on booking payment
with zero side effects, tip, gift-card buy; verified user still pays 200).
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent 858ae87e9c
commit 2b4c50b4b0
4 changed files with 260 additions and 10 deletions
+16 -10
View File
@@ -385,22 +385,28 @@ func main() {
r.Delete("/bookings/reserve", bookings.CancelReservationHandler)
// User payment routes
r.Post("/bookings/{id}/payment", payments.CreateBookingPayment)
r.Post("/bookings/{id}/apply-redemption", payments.ApplyLoyaltyRedemption)
r.Post("/bookings/{id}/payment-lock", payments.AcquirePaymentLock)
r.Delete("/bookings/{id}/payment-lock", payments.ReleasePaymentLock)
r.Get("/user/payment-methods", payments.GetUserPaymentMethods)
r.Post("/user/payment-methods", payments.CreatePaymentMethod)
r.Delete("/user/payment-methods/{id}", payments.DeletePaymentMethod)
r.Post("/bookings/{id}/tip", payments.CreateTipPayment)
// Product rule (security): guests (account_role='guest') must not
// pay online — RequireNonGuest 403s any token with a guest role
// claim before the money handlers run.
r.With(mw.RequireNonGuest).Post("/bookings/{id}/payment", payments.CreateBookingPayment)
r.With(mw.RequireNonGuest).Post("/bookings/{id}/apply-redemption", payments.ApplyLoyaltyRedemption)
r.With(mw.RequireNonGuest).Post("/bookings/{id}/payment-lock", payments.AcquirePaymentLock)
r.With(mw.RequireNonGuest).Delete("/bookings/{id}/payment-lock", payments.ReleasePaymentLock)
// Product rule (security): cards may only be saved by verified
// accounts — RequireAuth (group mw above) runs first and injects
// userID/role into ctx, then RequireVerified 403s the rest.
r.With(mw.RequireVerified).Get("/user/payment-methods", payments.GetUserPaymentMethods)
r.With(mw.RequireVerified).Post("/user/payment-methods", payments.CreatePaymentMethod)
r.With(mw.RequireVerified).Delete("/user/payment-methods/{id}", payments.DeletePaymentMethod)
r.With(mw.RequireNonGuest).Post("/bookings/{id}/tip", payments.CreateTipPayment)
r.Get("/bookings/{id}/payment-summary", payments.GetBookingPaymentSummary)
r.With(mw.RateLimit(10, time.Minute), limitBody(defaultBodyLimit)).
Get("/bookings/{id}/discount-preview", payments.GetDiscountPreviewHandler)
// User gift card routes
r.Post("/user/giftcards/redeem", payments.RedeemGiftCard)
r.With(mw.RequireNonGuest).Post("/user/giftcards/redeem", payments.RedeemGiftCard)
r.Get("/user/giftcards/balance", payments.GetGiftCardBalance)
r.Post("/user/giftcards/buy", payments.BuyGiftCard)
r.With(mw.RequireNonGuest).Post("/user/giftcards/buy", payments.BuyGiftCard)
})
r.With(mw.RequireAuth, mw.RequireVerified, limitBody(uploadBodyLimit)).Post("/user/profile-picture", user.UploadProfilePictureHandler)