taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

007-payment.rst (12662B)


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