taler-docs

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

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.