Skip to content

Commit b6f970e

Browse files
committed
feat: implement secure payment confirmation data structures and denormalizers
1 parent ffedd7d commit b6f970e

35 files changed

+4845
-5
lines changed

.ci-tools/phpstan-baseline.neon

Lines changed: 504 additions & 0 deletions
Large diffs are not rendered by default.

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/.ci-tools export-ignore
44
/.github export-ignore
55
/bin export-ignore
6+
/docs export-ignore
67
/tests export-ignore
78
/.editorconfig export-ignore
89
/.gitattributes export-ignore

docs/examples/QUICKSTART.md

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
# Secure Payment Confirmation - Quick Start Guide
2+
3+
## 🚀 Choose Your Implementation
4+
5+
You have **two options** to implement Secure Payment Confirmation:
6+
7+
### Option A: Standalone Controller (Full Control)
8+
**Time:** ~30 minutes | **Complexity:** Medium | **Flexibility:** High
9+
10+
### Option B: Bundle Configuration (Quick Setup)
11+
**Time:** ~15 minutes | **Complexity:** Low | **Flexibility:** Medium
12+
13+
---
14+
15+
## Option A: Standalone Controller
16+
17+
### Step 1: Copy the controller
18+
```bash
19+
cp docs/examples/payment-controller-standalone.php src/Controller/PaymentController.php
20+
cp docs/examples/payment-service-example.php src/Service/PaymentService.php
21+
```
22+
23+
### Step 2: Create the entity
24+
```bash
25+
php bin/console make:entity PaymentTransaction
26+
```
27+
28+
Add the properties from `PaymentTransactionEntity` in `payment-service-example.php`.
29+
30+
### Step 3: Create migration
31+
```bash
32+
php bin/console make:migration
33+
php bin/console doctrine:migrations:migrate
34+
```
35+
36+
### Step 4: Register routes
37+
```yaml
38+
# config/routes.yaml
39+
payment_options:
40+
path: /payment/options
41+
controller: App\Controller\PaymentController::options
42+
methods: [POST]
43+
44+
payment_verify:
45+
path: /payment/verify
46+
controller: App\Controller\PaymentController::verify
47+
methods: [POST]
48+
```
49+
50+
### Step 5: Test it!
51+
```html
52+
<!-- In your Twig template -->
53+
<form data-controller="webauthn--authentication"
54+
data-action="submit->webauthn--authentication#authenticate"
55+
data-webauthn--authentication-options-url-value="/payment/options"
56+
data-webauthn--authentication-result-url-value="/payment/verify">
57+
58+
<input type="hidden" name="transactionId" value="{{ transaction.id }}">
59+
<button type="submit">Confirm Payment of {{ transaction.amount }} {{ transaction.currency }}</button>
60+
</form>
61+
```
62+
63+
**✅ Done!** Your payment system is ready.
64+
65+
---
66+
67+
## Option B: Bundle Configuration
68+
69+
### Step 1: Configure the bundle
70+
```bash
71+
# Add to config/packages/webauthn.yaml
72+
cat >> config/packages/webauthn.yaml << 'EOF'
73+
74+
# Payment profile
75+
request_profiles:
76+
payment:
77+
rp_id: '%env(WEBAUTHN_RP_ID)%'
78+
challenge_length: 32
79+
timeout: 60000
80+
user_verification: 'required'
81+
82+
controllers:
83+
enabled: true
84+
request:
85+
payment:
86+
profile: 'payment'
87+
options_path: '/payment/options'
88+
result_path: '/payment/verify'
89+
options_handler: App\Webauthn\Handler\PaymentOptionsHandler
90+
success_handler: App\Webauthn\Handler\PaymentSuccessHandler
91+
failure_handler: App\Webauthn\Handler\PaymentFailureHandler
92+
EOF
93+
```
94+
95+
### Step 2: Create handlers
96+
```bash
97+
mkdir -p src/Webauthn/Handler
98+
cp docs/examples/payment-handlers.php src/Webauthn/Handler/
99+
```
100+
101+
Edit the file to split into three separate handler classes.
102+
103+
### Step 3: Create the service
104+
```bash
105+
cp docs/examples/payment-service-example.php src/Service/PaymentService.php
106+
```
107+
108+
### Step 4: Create the entity (same as Option A)
109+
```bash
110+
php bin/console make:entity PaymentTransaction
111+
php bin/console make:migration
112+
php bin/console doctrine:migrations:migrate
113+
```
114+
115+
### Step 5: Test it!
116+
Same HTML as Option A - routes are automatically created by the bundle!
117+
118+
**✅ Done!** Your payment system is ready with less code.
119+
120+
---
121+
122+
## 🧪 Testing Your Implementation
123+
124+
### 1. Check browser support
125+
```javascript
126+
const isSupported = 'PaymentRequest' in window &&
127+
'PublicKeyCredential' in window;
128+
console.log('SPC supported:', isSupported);
129+
```
130+
131+
### 2. Test the flow
132+
133+
1. **Create a test transaction:**
134+
```php
135+
$transaction = $paymentService->createTransaction(
136+
userId: 'user123',
137+
amount: '99.99',
138+
currency: 'EUR',
139+
payeeName: 'Test Merchant',
140+
payeeOrigin: 'https://merchant.example.com'
141+
);
142+
```
143+
144+
2. **Open the payment page** in Chrome 105+
145+
146+
3. **Click "Confirm Payment"** - Browser should show payment UI
147+
148+
4. **Authenticate** with biometrics or security key
149+
150+
5. **Check the result** - Transaction should be marked as "confirmed"
151+
152+
### 3. Debug issues
153+
154+
```bash
155+
# Check Symfony logs
156+
tail -f var/log/dev.log
157+
158+
# Check WebAuthn events in browser console
159+
# Open DevTools > Console
160+
# Click payment button
161+
# Look for webauthn:* events
162+
```
163+
164+
---
165+
166+
## 🔒 Security Checklist
167+
168+
Before going to production:
169+
170+
- [ ] ✅ Payment amounts fetched from server-side database (NEVER from client)
171+
- [ ] ✅ Transaction IDs are cryptographically random
172+
- [ ] ✅ User verification is set to "required" for payments
173+
- [ ] ✅ HTTPS enabled (required for WebAuthn)
174+
- [ ] ✅ Transaction expiry implemented (e.g., 15 minutes)
175+
- [ ] ✅ Proper error handling and logging
176+
- [ ] ✅ Rate limiting on payment endpoints
177+
- [ ] ✅ CSRF protection enabled
178+
- [ ] ✅ CSP headers configured
179+
180+
---
181+
182+
## 📱 Frontend Examples
183+
184+
### Using Stimulus (Recommended)
185+
```html
186+
<form data-controller="webauthn--authentication"
187+
data-action="submit->webauthn--authentication#authenticate"
188+
data-webauthn--authentication-options-url-value="/payment/options"
189+
data-webauthn--authentication-result-url-value="/payment/verify"
190+
data-webauthn--authentication-success-redirect-uri-value="/payment/success">
191+
192+
<input type="hidden" name="transactionId" value="{{ txn_id }}">
193+
<button type="submit">Pay {{ amount }}</button>
194+
</form>
195+
```
196+
197+
### Using Vanilla JavaScript
198+
```html
199+
<button onclick="confirmPayment('txn_abc123')">Pay Now</button>
200+
201+
<script type="module">
202+
import { startAuthentication } from '@simplewebauthn/browser';
203+
204+
window.confirmPayment = async (txnId) => {
205+
const options = await fetch('/payment/options', {
206+
method: 'POST',
207+
headers: { 'Content-Type': 'application/json' },
208+
body: JSON.stringify({ transactionId: txnId })
209+
}).then(r => r.json());
210+
211+
const credential = await startAuthentication({ optionsJSON: options });
212+
213+
const result = await fetch('/payment/verify', {
214+
method: 'POST',
215+
headers: { 'Content-Type': 'application/json' },
216+
body: JSON.stringify(credential)
217+
}).then(r => r.json());
218+
219+
if (result.success) {
220+
window.location.href = '/payment/success';
221+
}
222+
};
223+
</script>
224+
```
225+
226+
---
227+
228+
## 🆘 Common Issues
229+
230+
### "WebAuthn not supported"
231+
- ✅ Use HTTPS (required, except localhost)
232+
- ✅ Test in Chrome 105+ or Edge 105+
233+
- ✅ Firefox/Safari don't support SPC yet
234+
235+
### "Transaction not found"
236+
- ✅ Check transaction ID is correct
237+
- ✅ Verify transaction hasn't expired
238+
- ✅ Check database connection
239+
240+
### "Payment extension not present"
241+
- ✅ Verify payment extension is added in options handler
242+
- ✅ Check `isPayment: true` is set
243+
- ✅ Ensure all required fields are present
244+
245+
### "Signature verification failed"
246+
- ✅ RP ID must match credential registration
247+
- ✅ Origin must match
248+
- ✅ Challenge must not be expired
249+
- ✅ Credential must exist in database
250+
251+
---
252+
253+
## 📚 Next Steps
254+
255+
1. **Read the full documentation:** `docs/examples/README.md`
256+
2. **Review security considerations:** Especially server-side validation
257+
3. **Implement error handling:** For better user experience
258+
4. **Add monitoring:** Log all payment attempts
259+
5. **Test with real devices:** Try different authenticators
260+
261+
---
262+
263+
## 💡 Pro Tips
264+
265+
1. **Always validate payment data server-side** - Never trust the client!
266+
2. **Use short expiry times** - 15 minutes is recommended
267+
3. **Log everything** - Payment attempts, failures, successes
268+
4. **Test thoroughly** - Different browsers, devices, authenticators
269+
5. **Have a fallback** - Not all users have compatible devices
270+
271+
---
272+
273+
## 🎯 Complete Flow Diagram
274+
275+
```
276+
User clicks "Pay"
277+
278+
Frontend sends transactionId to /payment/options
279+
280+
Server fetches REAL payment data from database
281+
282+
Server creates WebAuthn options with payment extension
283+
284+
Browser shows payment UI with amount/merchant
285+
286+
User confirms with biometrics
287+
288+
Frontend sends credential to /payment/verify
289+
290+
Server validates signature
291+
292+
Server processes payment
293+
294+
Success! Redirect to confirmation page
295+
```
296+
297+
---
298+
299+
## Need Help?
300+
301+
- 📖 Full documentation: `docs/examples/README.md`
302+
- 🐛 Report issues: GitHub Issues
303+
- 💬 Discussions: GitHub Discussions
304+
- 📧 Security issues: security@example.com (use your actual security contact)
305+
306+
Happy coding! 🚀

0 commit comments

Comments
 (0)