007-payment.rst (13245B)
1 DD 07: Specification of the Payment Flow 2 ######################################## 3 4 :Design status: Accepted 5 :Implementation status: Implemented 6 :DD shepherd: TBD 7 :Historical contributors: Florian Dold, Christian Grothoff, Thien-Thi Nguyen 8 :First published: 2020-08-09 9 :Last substantive change: 2020-10-31 10 :Implementation evidence: merchant (2020-07-21, 2020-08-16) 11 :Normative references: :doc:`../core/api-merchant`, :doc:`../wallet/wallet-core` 12 13 .. note:: 14 15 This document records the original browser payment-flow design. The 16 current merchant and wallet APIs are authoritative for implemented endpoint 17 behavior; unresolved questions below are historical. 18 19 Summary 20 ======= 21 22 This design document describes how the payment flow works in the browser, and how 23 features like session IDs, re-purchase detection and refunds interact. 24 25 Requirements 26 ============ 27 28 * The payment flow must both support wallets that are integrated in the browser, 29 as well as external wallets (mobile phone, command line) 30 * The initiator of the payment can be a Website or another channel, 31 such as an e-mail or a messaging service. 32 * For paid digital works, there should be a reasonable technical barrier to 33 sharing the information with unauthorized users 34 * A simple API should be offered to shops 35 * Sharing of links or re-visiting of bookmarks should result in well-defined 36 behavior instead of random, unclear error messages. 37 * The payment flow must degrade gracefully when JavaScript is disabled. 38 39 Proposed Solution 40 ================= 41 42 43 44 Session-bound payment flow for Web resources 45 -------------------------------------------- 46 47 In this payment flow, the user initiates the payment by navigating to a 48 paywalled Web resource. Let *resource-URL* be the URL of the paywalled resource. 49 50 Storefront 51 ^^^^^^^^^^ 52 53 When *resource-URL* is requested, the storefront runs the following steps: 54 55 1. Extract the *resource name* from the *resource-URL*. 56 2. Extract the *session-ID* (or null) from the request's validated cookie (for example, by using signed cookies). 57 3. Extract the *order-ID* (or null) from the request's ``order_id`` cookie. This cookie may optionally be validated. 58 59 .. 60 is "invalid" equivalent to "null"? 61 62 4. If *session-ID* or *order-ID* is null, assign a fresh session ID and 63 create a new order for *resource name* by doing a ``POST /private/orders`` 64 to the merchant backend. Set both in the cookie to be sent with the response. 65 5. Check the status of the payment for *order-ID* under *session-ID* by doing a ``GET /private/orders/{order-ID}?session_id={session-ID}``. 66 This results in the *order-status*, *refund-amount* and the *client-order-status-URL*. 67 6. If the *order-status* is claimed, set *order-ID* to null and go back to step 4. 68 7. If the *order-status* is paid and *refund-amount* is non-zero, 69 return to the client a page with an explanation that the payment has been refunded. **Terminate.** 70 8. If the client has not (fully) obtained the granted refunds yet, show a link to the public order page 71 of the backend to allow the client to obtain the refund. **Terminate.** 72 9. If the *order-status* is paid, return to the client the resource associated with *resource name*. **Terminate.** 73 10. Otherwise, either the *order-status* is unpaid or the customer tried to access a paid resource after having deleted their cookies. Redirect the client to *client-order-status-URL*. **Terminate.** 74 11. If the wallet detects that the resource was paid before, it will resend the same payment again, and will get the item; if not, the wallet will create a new payment and send to the merchant. 75 76 .. note:: 77 78 Instead of making a request to the merchant backend on every request to *resource-URL*, the storefront 79 may use a *session-page-cache* that stores (*session-ID*, *order-ID*, *resource-name*) tuples. 80 When a refund is given, the corresponding tuple must be removed from the *session-page-cache*. 81 82 Backend Private Order Status 83 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 84 85 The merchant backend runs the following steps to generate the 86 *client-order-status-URL* when processing a request for ``GET 87 /private/orders/{order-ID}?session_id={session-ID}&timeout_ms={timeout}``: 88 89 1. Let *session-ID* be the session ID of the request or null if not given (note: **not** the last paid session ID) 90 2. If *order-ID* does not identify an existing order, return a 404 Not Found response. **Terminate**. 91 3. If *order-ID* identifies an order that is *unclaimed* and has claim token *claim-token*, return the URL 92 93 .. code-block:: none 94 95 {backendBaseUrl}/orders/{order-ID}?token={claim-token}&session_id={session-ID} 96 97 (if no claim-token was generated, omit that parameter from the above URI). **Terminate.** 98 99 4. Here *order-ID* identifies an order that is *claimed*. If the order is *unpaid*, wait until timeout or payment. 100 101 5. If the order remains unpaid or was paid for a different *session-ID*, obtain the contract terms hash *contract-hash* and return the URL 102 103 .. code-block:: none 104 105 {backendBaseUrl}/orders/{order-ID}?h_contract={contract-hash}&session_id={session-ID} 106 107 together with the status *unpaid*. (If *session-ID* is null, it does not 108 matter for which session the contract was paid.) **Terminate.** 109 110 6. Here *order-ID* must now identify an order that is *paid* or *refunded*. Obtain the contract terms hash *contract-hash* and return the URL 111 112 .. code-block:: none 113 114 {backendBaseUrl}/orders/{order-ID}?h_contract={contract-hash}&session_id={session-ID} 115 116 together with the status *paid* or *refunded* (and if applicable, with 117 details about the applied refunds). **Terminate.** 118 119 120 121 Backend Client Order Status Page 122 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 123 124 The merchant backend runs the following steps to generate the HTML page for 125 ``GET /orders/{order-ID}?session_id={session-ID}&token={claim-token}&h_contract={contract-hash}``: 126 127 1. If *order-ID* does not identify an existing order, render a 404 Not Found response. **Terminate.** 128 2. If *order-ID* identifies a paid order (where the *session-ID* matches the one from the payment), run these steps: 129 130 1. If the *contract-hash* request parameter does not match the contract terms hash of the order, 131 return a 403 Forbidden response. **Terminate.** 132 133 2. If the order has granted refunds that have not been obtained by the wallet yet, prompt the URI 134 135 .. code-block:: none 136 137 taler{proto_suffix}://refund/{/merchant_prefix*}/{order-id}/{session-id} 138 139 The generated Web site should long-poll until all refunds have been obtained, 140 then redirect to the *fulfillment-URL* of the order once the refunds have been 141 obtained. **Terminate.** 142 ----- FIXME: IIRC our long-polling API does only allow waiting for the granted refund amount, not for the *obtained* refund amount. => API change? 143 144 3. Here the order has been paid and possibly refunded. 145 Redirect to the *fulfillment-URL* of the order. 146 **Terminate.** 147 148 149 3. If *order-ID* identifies an *unclaimed* order, run these steps: 150 151 1. If the order is *unclaimed* and the *claim-token* request parameter does not 152 match the claim token of the order, return a 403 Forbidden response. **Terminate**. 153 154 2. Prompt the URI 155 156 .. code-block:: none 157 158 taler{proto_suffix}://pay/{/merchant_prefix*}/{order-id}/{session-ID}?c={claim-token} 159 160 The generated Web site should long-poll to check for the payment happening. 161 It should then redirect to the *fulfillment-URL* of the order once 162 payment has been proven under *session-ID*, or possibly redirect to the 163 *already-paid-order-ID*. Which of these happens depends on the (long-polled) JSON replies. 164 **Terminate.** 165 166 4. If *order-ID* identifies an *claimed* and *unpaid* order, run these steps: 167 168 1. If the *claim-token* request parameter is given and the *contract-hash* request parameter is 169 not given, redirect to the fulfillment URL of the order. (**Note**: We do not check 170 the claim token, as the merchant might have already deleted it when the order is paid, 171 and the fulfillment URL is not considered to be secret/private.) 172 173 2. If the *contract-hash* request parameter does not 174 match the contract hash of the order, return a 403 Forbidden response. **Terminate**. 175 176 3. If there is a non-null *already-paid-order-ID* for *session-ID* stored under the current order, 177 redirect to the *fulfillment-URL* of *already-paid-order-ID*. **Terminate**. 178 179 4. Prompt the URI 180 181 .. code-block:: none 182 183 taler{proto_suffix}://pay/{/merchant_prefix*}/{order-id}/{session-ID} 184 185 The generated Web site should long-poll to check for the payment happening. 186 It should then redirect to the *fulfillment-URL* of the order once 187 payment has been proven under *session-ID*, or possibly redirect to the 188 *already-paid-order-ID*. Which of these happens depends on the (long-polled) JSON replies. 189 **Terminate.** 190 191 Examples 192 ======== 193 194 The examples use the prefix ``S:`` for the storefront, ``B:`` for the customer's browser 195 and ``W:`` for the wallet. 196 197 The following example uses a detached wallet: 198 199 .. code:: none 200 201 B: [user nagivates to the book "Moby Dick" in the demo storefront] 202 B: -> GET https://shop.demo.taler.net/books/moby-dick 203 (content-type: application/html) 204 205 S: [Assigns session ID ``sess01`` to browser] 206 S: -> POST https://merchant-backend.demo.taler.net/orders 207 S: -> GET https://merchant-backend.demo.taler.net/orders/ord01?session_id=sess01 208 209 B: <- HTTP 307, redirect to https://merchant-backend.demo.taler.net/orders/ord01?token=ct01&session_id=sess01 210 211 B: -> GET https://merchant-backend.demo.taler.net/orders/ord01?token=ct01 212 (content-type: application/html) 213 B: <- HTTP status 402 Payment Required, QR code / link to 214 taler://pay/shop.demo.taler.net/ord01/sess01?c=ct01 215 216 B: [via JavaScript on page] 217 B: -> GET https://merchant-backend.demo.taler.net/orders/ord01?token=ct01&session_id=sess01 218 (content-type: application/json) 219 B: <- HTTP status 402 Payment Required 220 221 W: [user scans QR taler://pay code] 222 W: POST https://shop.demo.taler.net/orders/ord01/claim 223 224 B: [via JavaScript on page] 225 B: -> GET https://merchant-backend.demo.taler.net/orders/ord01?token=ct01&session_id=sess01 226 (content-type: application/json) 227 B: <- HTTP status 402 Payment Required 228 229 W: POST https://shop.demo.taler.net/orders/ord01/pay 230 231 B: [via JavaScript on page] 232 B: -> GET https://merchant-backend.demo.taler.net/orders/ord01?token=ct01&session_id=sess01 233 (content-type: application/json) 234 B: <- HTTP status 202 Accepted 235 B: [redirects to fulfillment URL of ord01 baked into the JavaScript code] 236 237 B: -> GET https://shop.demo.taler.net/books/moby-dick 238 (content-type: application/html) 239 S: -> GET https://merchant-backend.demo.taler.net/orders/ord01?session_id=sess01 240 S: <- HTTP 200, order status "paid" 241 B: <- HTTP 200, content of "moby-dick" is rendered 242 243 244 Discussion / Q&A 245 ================ 246 247 Notes 248 ----- 249 250 * The *timeout_ms* argument is expected to be ignored when generating HTML. 251 Long-polling simply makes no sense if a browser accesses the site directly. 252 253 254 Covered Scenarios 255 ----------------- 256 257 * **Re-purchase detection**. Let's say a detached wallet has already successfully paid for a resource URL. 258 A browser navigates to the resource URL. The storefront will generate a new order and assign a session ID. 259 Upon scanning the QR code, the wallet will detect that it already has purchased the resource (checked via the fulfillment URL). 260 It will then prove the payment of the **old** order ID under the **new** session ID. 261 262 263 * **Bookmarks of Lost Purchases / Social Sharing of Fulfillment URLs** 264 265 FIXME: explain how we covered this by moving order ID into session cookie! 266 Let's say I bought some article a few months ago and I lost my wallet. I still have the augmented fulfillment URL 267 for the article bookmarked. When I re-visit the URL, I will be prompted via QR code, but I can *never* prove 268 that I already paid, because I lost my wallet! 269 270 In this case, it might make sense to include some "make new purchase" link on the client order status page. 271 It's not clear if this is a common/important scenario though. 272 273 But we might want to make clear on the client order status page that it's showing a QR code for something 274 that was already paid. 275 276 The same concern applies when sending the fulfillment URL of a paid paywalled Web resource to somebody else. 277 278 279 280 Problematic Scenarios 281 --------------------- 282 283 The Back Button 284 ^^^^^^^^^^^^^^^ 285 286 The following steps lead to unintuitive navigation: 287 1. Purchase a paywalled URL for the first time via a detached wallet 288 2. Marvel at the fulfillment page 289 3. Press the back button (or go back to bookmarked page 1), possibly press reload if page was still cached). 290 291 This will display an error message, as the authentication via the claim token on the 292 ``/orders/{order-ID}`` page is not valid anymore. 293 294 We could consider still allowing authentication with the claim token in this case. 295 296 Proposal: generate 410 Gone in case token is provided for claimed order. For now 297 in JSON, eventually possibly with a nice HTML page if respective content type is 298 provided.