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