098-token-fountains.rst (11902B)
1 .. _dd-98: 2 3 DD 98: Token Fountains for Promotions 4 ##################################### 5 6 :Design status: Draft 7 :Implementation status: Partial 8 :DD shepherd: TBD 9 :Historical contributors: Bohdan Potuzhnyi 10 :First published: 2026-07-23 11 :Last substantive change: 2026-09-06 12 :Normative references: ``core/api-merchant.rst`` (v43 token-fountain API) 13 14 Summary 15 ======= 16 17 This document proposes token fountains as a simple way for institutions to 18 distribute promotional ``discount`` and ``subscription`` tokens. A wallet 19 imports a bearer credential, periodically retrieves the available grants and 20 withdraws blind-signed tokens within limits configured by the institution. 21 22 .. note:: 23 24 The merchant backend implements the fountain management and wallet APIs 25 under protocol **v43** (commit ``97439d8f``), replacing the provisional 26 ``vTokenFountains`` name. 27 28 Motivation 29 ========== 30 31 Institutions sometimes want to distribute promotional benefits to a group 32 without creating and paying a separate order for every recipient. For 33 example, a university may give students discount tokens accepted by a campus 34 merchant, or periodically provide subscription tokens for a continuing 35 promotion. 36 37 GNU Taler already defines blind-signed merchant tokens. What is missing is a 38 simple distribution channel through which a wallet can obtain an authorized 39 set of those tokens over time. A **token fountain** is a bearer credential 40 that authorizes such withdrawals. 41 42 The institution can distribute a fountain as a QR code or deep link. The 43 wallet imports the link, learns which token families are available and 44 withdraws blind-signed tokens subject to the configured limits. Blind 45 signatures keep a later use of a token unlinkable from its withdrawal. 46 47 User Flow 48 ========= 49 50 The recipient scans the promotion once. The wallet obtains promotional 51 tokens and offers an applicable token later at checkout. 52 53 .. uml:: images/098/promotions-user-flow.puml 54 :align: center 55 :width: 90% 56 :caption: Joining and using a token-fountain promotion 57 :alt: Scan the promotion, receive promotional tokens in the wallet, use a 58 token at checkout and receive the discount or benefit. 59 60 Requirements 61 ============ 62 63 * Institutions must be able to distribute promotional tokens without 64 creating a separate order for every withdrawal. 65 66 * A fountain must support grants for existing ``discount`` and 67 ``subscription`` token families. 68 69 * Token withdrawals must use blind signatures so that later token use is not 70 linkable to withdrawal. 71 72 * The merchant backend must enforce per-period withdrawal limits atomically. 73 74 * Wallets must be able to import a fountain using a QR code or Taler URI. 75 76 * The bearer secret must not appear in HTTP request URLs after onboarding. 77 78 * Deployments must be able to update or revoke future withdrawals without 79 invalidating tokens that were already issued. 80 81 Proposed Solution 82 ================= 83 84 Terminology 85 ----------- 86 87 In protocol descriptions, this document uses the existing token-family 88 ``kind`` values: 89 90 * A ``discount`` token is useful for a one-time or otherwise limited 91 promotion. It may be consumed without the merchant issuing a replacement. 92 93 * A ``subscription`` token represents a continuing promotion. When an order 94 consumes it, the existing merchant token mechanism can return a replacement 95 token envelope as an output of the selected choice. 96 97 User-facing applications may call either benefit a **pass**. The protocol 98 continues to distinguish them by token-family kind. 99 100 Fountain Credential 101 ------------------- 102 103 A fountain has: 104 105 * a public ``fountain_id`` identifying the fountain within a merchant 106 instance; 107 108 * a random ``fountain_secret`` used as its bearer credential; 109 110 * a polling frequency; and 111 112 * zero or more grants describing which token families may be withdrawn and 113 at what rate. 114 115 The onboarding URI is: 116 117 :: 118 119 taler://fountain/$MERCHANT_HOST[/$INSTANCE_PATH]/$FOUNTAIN_ID/$FOUNTAIN_SECRET 120 121 The URI may be displayed as a QR code or opened as a wallet deep link. 122 ``fountain_id`` tells the wallet which fountain it is importing, while 123 ``fountain_secret`` authenticates the import. The wallet must treat the full 124 URI as sensitive. 125 126 After import, the secret must not be placed in subsequent HTTP URLs. It is 127 sent in the ``Authorization`` header when reading fountain information and in 128 the request body when withdrawing tokens, as specified by the merchant API. 129 130 Deployment Models 131 ----------------- 132 133 An institution may use one fountain for an entire campaign. This is simple 134 to distribute, but every recipient shares one bearer credential and the same 135 withdrawal limits. A leaked link cannot be revoked for only one recipient. 136 137 Alternatively, the institution may create one fountain per recipient and 138 distribute each link through its existing student communication system. This 139 allows independent limits and revocation without requiring the merchant 140 backend to store the student's real identity. The fountain description can 141 contain an opaque reference meaningful only to the institution. 142 143 The choice is operational rather than cryptographic. Blind token issuance 144 prevents the merchant from linking a withdrawn token to its later use in 145 either model. 146 147 Grant Model 148 ----------- 149 150 Each :ts:type:`FountainGrant` names a token family and defines: 151 152 * ``tokens_per_period_limit``, the maximum number of tokens signed for one 153 issue-key validity period; 154 155 * ``tokens_per_period_stash``, the number of tokens the wallet should try to 156 keep available for that period; and 157 158 * ``key_window_size``, the number of issue-key slots into the future for 159 which the wallet may prepare tokens. 160 161 The merchant may update grants without replacing the bearer credential. 162 Wallets periodically retrieve the current grants according to ``poll_freq``. 163 Removing a grant prevents future withdrawals from that family, but tokens 164 already withdrawn remain valid according to the token family's issue keys. 165 166 An empty grants array means no grants are currently available, not that the 167 credential is revoked. Wallets continue polling for updates. 168 169 Protocol Flow 170 ------------- 171 172 1. The institution configures suitable ``discount`` or ``subscription`` token 173 families on the merchant backend. 174 175 2. It creates a fountain with grants for those token families. 176 177 3. The backend returns ``fountain_id`` and ``fountain_secret`` once. The 178 institution forms the Taler fountain URI and gives it to the intended 179 recipients. 180 181 4. A wallet imports the URI and calls ``GET /fountain/info`` using the bearer 182 credential. 183 184 5. The wallet prepares blinded token envelopes for any grants it needs to 185 replenish and submits them to ``POST /fountain/withdraw``. 186 187 6. The backend checks the grant, issue-key window and withdrawal limit, then 188 blind-signs the accepted envelopes. 189 190 7. The wallet unblinds and stores the tokens. Their later use follows the 191 existing merchant token protocol. 192 193 Merchant API 194 ------------ 195 196 Private Management Operations 197 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 198 199 The merchant API adds private operations to create, inspect, update, list and 200 delete fountains: 201 202 * ``POST /private/fountains`` 203 204 * ``GET /private/fountains`` (since protocol **v44**, paginated listing ordered 205 by ``fountain_serial`` with ``limit``/``offset`` parameters; defaults to 20 206 entries, with a hard cap of 100 per request) 207 208 * ``GET /private/fountains/$FOUNTAIN_ID`` 209 210 * ``PATCH /private/fountains/$FOUNTAIN_ID`` 211 212 * ``DELETE /private/fountains/$FOUNTAIN_ID`` 213 214 Creation returns: 215 216 .. ts:def:: FountainCreateResponse 217 218 interface FountainCreateResponse { 219 // Public identifier included in the wallet onboarding URI. 220 fountain_id: Slug; 221 222 // 256-bit bearer credential, Crockford Base32 encoded. 223 // Returned only at creation; the backend stores only its hash. 224 fountain_secret: string; 225 } 226 227 Wallet Operations 228 ~~~~~~~~~~~~~~~~~ 229 230 ``GET /fountain/info`` returns the current grants, token-family metadata and 231 issue public keys needed to prepare blinded envelopes. 232 233 ``POST /fountain/withdraw`` accepts the bearer credential and blinded 234 envelopes. The request is grouped by token family and issue-key slot so that 235 the backend can enforce each grant's limits. 236 237 Each grant result returns ``token_sigs`` using the same 238 :ts:type:`SignedTokenEnvelope` format as the order payment response. 239 240 The backend records completed withdrawals by fountain and canonical request 241 hash. Retrying with the same ``grants`` array returns the original result 242 without consuming quota or signing again, even if the grants or key window 243 have changed. Quota consumption, issued-token records and replay data are 244 committed in one transaction so a failed operation does not consume quota. 245 Replay data is retained as a complete response until the latest issue-key 246 expiry in the withdrawal, even if some entries expire earlier. Deleting a 247 fountain drops its replay data at once. 248 249 Deletion invalidates future requests using the credential. It does not 250 invalidate tokens that the wallet already withdrew. 251 252 Security Considerations 253 ======================= 254 255 Anyone possessing a fountain URI can withdraw under its grants. Distribution 256 systems must therefore protect the URI like any bearer credential. Wallets 257 must avoid exposing it in history, telemetry, screenshots or backups that are 258 not intended to contain secrets. 259 260 The backend stores only a hash of ``fountain_secret``. Rate limiting of failed 261 authentication may be handled by the deployment's reverse proxy. The backend 262 must enforce withdrawal limits atomically to prevent concurrent requests from 263 exceeding a grant. 264 265 Fountains are not proof of student identity. Eligibility is decided by the 266 institution when it distributes the link. A shared campaign link can be 267 forwarded, while a per-recipient link can only be individually disabled after 268 the institution learns that it was compromised. 269 270 Privacy Considerations 271 ====================== 272 273 Fountain descriptions should contain opaque administrative references rather 274 than names or student identifiers. The merchant backend does not need a 275 recipient's identity to issue blind-signed tokens. 276 277 Network metadata can still correlate a wallet's requests. Deployments that 278 require stronger network-level privacy need an appropriate transport or proxy; 279 blind signatures alone do not hide IP addresses or request timing. 280 281 Drawbacks 282 ========= 283 284 The fountain API adds persistent bearer credentials, polling and quota state 285 to the merchant backend. 286 287 Shared campaign fountains trade administrative simplicity for coarse 288 revocation and shared quotas. Per-recipient fountains provide better control 289 but require the institution to create and distribute many distinct links. 290 291 Test Plan 292 ========= 293 294 * Test creation, inspection, update, listing and deletion of fountains. 295 296 * Test importing a URI containing both ``fountain_id`` and 297 ``fountain_secret``. 298 299 * Test withdrawals for both ``discount`` and ``subscription`` token families. 300 301 * Test atomic enforcement of per-period limits under concurrent requests. 302 303 * Test rejection of unknown grants, unsupported issue-key slots and invalid 304 credentials. 305 306 * Test that deleting a fountain prevents new withdrawals without invalidating 307 tokens already held by a wallet. 308 309 * Test that token use cannot be linked to its fountain withdrawal from the 310 blind-signature transcript. 311 312 Definition of Done 313 ================== 314 315 * The merchant backend implements fountain management, information and 316 withdrawal operations. 317 318 * The onboarding URI includes the merchant location, ``fountain_id`` and 319 ``fountain_secret``. 320 321 * Wallet Core can import a fountain, poll its grants, replenish tokens within 322 the configured limits and stop when the credential is revoked. 323 324 * Merchant applications can create and distribute promotional fountains 325 without supplying recipient identities to the merchant backend. 326 327 * Discount and subscription token families retain their existing consumption 328 and replacement semantics.