commit edc84e7c1e3958e58e1e760beedc3c2a90575ecb
parent a242f682257c7bfbf88c62dfbe4a64cfc54c6dde
Author: Florian Dold <dold@taler.net>
Date: Mon, 24 Aug 2026 01:13:32 +0200
core bank API: document v12 behavior and security semantics
Diffstat:
11 files changed, 142 insertions(+), 21 deletions(-)
diff --git a/core/api-corebank.rst b/core/api-corebank.rst
@@ -39,7 +39,10 @@ The current protocol version is **v12**.
* ``v10``: Update two factor authentication API to match Merchant Backend API
* ``v11``: Add observability API
-* ``v12``: Add wire transfer API
+* ``v12``: Add wire transfer API. The v12 contract also clarifies account
+ contact removal, token refresh rotation, TAN challenge terminal states,
+ cashout idempotency and anonymous histories for public accounts. These are
+ corrections to v12 behavior and do not introduce a new protocol version.
**Upcoming versions:**
@@ -65,6 +68,11 @@ Authentication
Some endpoints require the client to authenticate using a bearer token. Tokens can be obtained or refreshed using the :http:post:`/accounts/$USERNAME/token </accounts/$USERNAME/token>` endpoint.
This endpoint supports authentication via HTTP Basic auth (RFC 7617). When using Basic authentication, the user-id must be the bank's username, and the password the password of the corresponding user.
+Refreshing a token rotates credentials instead of extending the presented
+credential indefinitely. See the token endpoint for the fixed overlap period
+and retry behavior. Clients must store the replacement before discarding the
+source token.
+
The user ``admin`` is a special, hard-coded username. Some requests require the client to authenticate as administrator.
.. warning::
diff --git a/core/corebank/get-accounts-USERNAME-tokens.rst b/core/corebank/get-accounts-USERNAME-tokens.rst
@@ -52,6 +52,7 @@
// validity?
// Refreshable tokens effectively provide indefinite
// access if they are refreshed in time.
+ // The canonical JSON member name is exactly "refreshable".
refreshable: boolean;
// Optional token description
@@ -66,3 +67,7 @@
// deprecated since **v9**. Use *token_id* instead.
row_id: Integer;
}
+
+ The wire response uses ``refreshable``. Clients must not expect, and
+ servers must not emit, the implementation-style spelling
+ ``isRefreshable``.
diff --git a/core/corebank/get-accounts-USERNAME-transactions.rst b/core/corebank/get-accounts-USERNAME-transactions.rst
@@ -2,6 +2,25 @@
Retrieve a subset of transactions related to $USERNAME.
+ An authenticated request is available to the account owner and the
+ administrator. The same endpoint also accepts an unauthenticated request
+ when the account is public. Anonymous responses contain the complete
+ `BankAccountTransactionInfo` records, including the subject and both the
+ debtor and creditor payto URIs; no fields are redacted.
+
+ For an unauthenticated request, a private account and a missing account both
+ return ``404 Not Found`` so the endpoint does not disclose which case
+ applies. Anonymous responses use ``Cache-Control: no-store``.
+
+ .. warning::
+
+ Setting ``is_public`` to true discloses the account's existing and future
+ transaction history, not only transactions created after the setting was
+ enabled. Accounts that were already public when this v12 correction was
+ deployed are grandfathered into this full-history disclosure without a
+ migration or a renewed opt-in. Operators must review existing public
+ accounts before deployment.
+
**Request:**
:query limit: *Optional.*
@@ -24,11 +43,13 @@
:http:statuscode:`204 No content`:
No transaction found.
:http:statuscode:`401 Unauthorized`:
- Invalid or missing credentials.
+ Invalid credentials were supplied. Missing credentials are permitted
+ only for public accounts.
:http:statuscode:`403 Forbidden`:
Missing rights.
:http:statuscode:`404 Not found`:
- The account pointed by ``$USERNAME`` was not found.
+ The account pointed by ``$USERNAME`` was not found. For anonymous
+ requests this status is also returned for private accounts.
**Details:**
diff --git a/core/corebank/get-accounts-USERNAME.rst b/core/corebank/get-accounts-USERNAME.rst
@@ -56,7 +56,8 @@
// be done via the access API.
cashout_payto_uri?: string;
- // Is this account visible to anyone?
+ // Is this account visible to anyone? Public account balances, payto
+ // URIs and complete transaction histories are anonymously accessible.
is_public: boolean;
// Is this a taler exchange account?
diff --git a/core/corebank/get-public-accounts.rst b/core/corebank/get-public-accounts.rst
@@ -3,6 +3,11 @@
Show those accounts whose histories are publicly visible. For example,
accounts from donation receivers. As such, this request is unauthenticated.
+ A public account exposes its balance and payto URI here, and its complete
+ existing and future transaction records through
+ ``GET /accounts/$USERNAME/transactions``. Complete records include subjects
+ and both debtor and creditor payto URIs. No transaction fields are redacted.
+
**Request:**
:query limit: *Optional.*
diff --git a/core/corebank/patch-accounts-USERNAME.rst b/core/corebank/patch-accounts-USERNAME.rst
@@ -7,8 +7,10 @@
.. ts:def:: AccountReconfiguration
interface AccountReconfiguration {
- // Addresses where to send the TAN for protected operations.
- contact_data?: ChallengeContactData;
+ // Sparse update of the addresses where TANs are sent.
+ // The object is merged with the stored contact data; it does not
+ // replace the complete object.
+ contact_data?: ChallengeContactDataPatch;
// Payto URI of a fiat bank account.
// Payments will be sent to this bank account
@@ -22,7 +24,10 @@
// Only admin can change this property if not allowed in config
name?: string;
- // Make this account visible to anyone?
+ // Make this account visible to anyone? Changing this to true
+ // immediately exposes the balance, payto URI and complete existing and
+ // future transaction history, including subjects and both parties'
+ // payto URIs, without authentication.
is_public?: boolean;
// If present, change the max debit allowed for this user
@@ -40,10 +45,11 @@
// Sending null will disable 2FA
tan_channel?: TanChannel | null;
- // If present and not empty, enables 2FA and set the TAN channels used for challenges
- // Sending null or an empty array will disable 2FA
+ // If present and not empty, enables 2FA and sets the TAN channels used
+ // for challenges. An empty array is the only representation for
+ // disabling all TAN channels.
// @since **v10**
- tan_channels?: TanChannel[] | null;
+ tan_channels?: TanChannel[];
// @deprecated in **v9**, user conversion rate classes instead
// FIXME-REMOVED-ALREADY: LibEuFin no longer implements this field
@@ -51,6 +57,37 @@
min_cashout?: Amount;
}
+ .. ts:def:: ChallengeContactDataPatch
+
+ interface ChallengeContactDataPatch {
+ // New e-mail address. Null removes the stored e-mail address.
+ // An omitted member leaves it unchanged.
+ email?: EmailAddress | null;
+
+ // New phone number. Null removes the stored phone number.
+ // An omitted member leaves it unchanged.
+ phone?: PhoneNumber | null;
+ }
+
+ ``AccountReconfiguration`` is a sparse merge. Omitting ``contact_data``
+ leaves all contact data unchanged. Within ``contact_data``, an omitted
+ member is unchanged, a string replaces that member and ``null`` removes
+ that member. For example, the following removes only the phone number and
+ preserves the e-mail address:
+
+ .. code-block:: json
+
+ {
+ "contact_data": {
+ "phone": null
+ }
+ }
+
+ Removing contact data that is required by an enabled TAN channel is invalid
+ unless the same PATCH also removes that channel from ``tan_channels``.
+ Send ``"tan_channels": []`` to disable multi-factor authentication. JSON
+ ``null`` is not accepted for ``tan_channels``.
+
**Response:**
:http:statuscode:`202 Accepted`:
diff --git a/core/corebank/post-accounts-USERNAME-cashouts.rst b/core/corebank/post-accounts-USERNAME-cashouts.rst
@@ -18,8 +18,8 @@
interface CashoutRequest {
// Nonce to make the request idempotent. Requests with the same
- // ``request_uid`` that differ in any of the other fields
- // are rejected.
+ // ``request_uid`` that differ in any of the other fields, including
+ // ``amount_credit``, are rejected.
request_uid: ShortHashCode;
// Optional subject to associate to the
@@ -45,6 +45,12 @@
amount_credit: Amount;
}
+ Idempotency compares the account, ``amount_debit``, ``amount_credit`` and
+ ``subject``. Repeating an identical request returns the original result;
+ changing any of those values while reusing ``request_uid`` returns
+ ``TALER_EC_BANK_TRANSFER_REQUEST_UID_REUSED``. In particular, a corrected
+ conversion quote must use a new ``request_uid``.
+
**Response:**
:http:statuscode:`200 OK`:
diff --git a/core/corebank/post-accounts-USERNAME-challenge-CHALLENGE_ID.rst b/core/corebank/post-accounts-USERNAME-challenge-CHALLENGE_ID.rst
@@ -2,7 +2,15 @@
Send TAN code for the ``CHALLENGE_ID`` challenge.
- This request can be posted several times to trigger TAN retransmission when the current code has expired or too many confirmation attempts have been made.
+ This request can be posted again to trigger TAN retransmission only while
+ the challenge is unsolved, unexpired and still has confirmation attempts
+ remaining. The client must wait until ``earliest_retransmission`` before
+ requesting another delivery.
+
+ Expiration and exhaustion of confirmation attempts are terminal for the
+ challenge ID. They do not create a new TAN and cannot be recovered by
+ retransmission. The client must repeat the original protected operation to
+ obtain a fresh challenge set.
This endpoint is not authenticated. @since **v10**
@@ -15,15 +23,17 @@
:http:statuscode:`200 Ok`:
The TAN code has been sent. The body will be a `ChallengeRequestResponse`.
:http:statuscode:`404 Not Found`:
- * ``TALER_EC_BANK_TAN_CHANNEL_SCRIPT_FAILED``: TAN transmition failed.
- * ``TALER_EC_BANK_TAN_CHALLENGE_EXPIRED``: TAN transmition failed.
- The challenge was not found.
+ * ``TALER_EC_BANK_TRANSACTION_NOT_FOUND``: the challenge ID was not found.
+ * ``TALER_EC_BANK_TAN_CHALLENGE_EXPIRED``: the challenge has expired or
+ exhausted its confirmation attempts and is terminal.
:http:statuscode:`410 Gone`:
The challenge was already solved.
:http:statuscode:`429 Too many requests`:
Too many challenges are active right now, you must wait or confirm current challenges.
:http:statuscode:`502 Bad Gateway`:
- * ``TALER_EC_BANK_TAN_CHANNEL_SCRIPT_FAILED``: TAN transmition failed.
+ * ``TALER_EC_BANK_TAN_CHANNEL_SCRIPT_FAILED``: the configured delivery
+ script failed. Delivery was not confirmed; the client may retry while
+ the challenge remains live and the retransmission policy permits it.
**Details:**
@@ -34,7 +44,7 @@
// challenge.
solve_expiration: Timestamp;
- // What is the earlist time at which the client
+ // What is the earliest time at which the client
// may request a new challenge to be transmitted?
earliest_retransmission: Timestamp;
}
diff --git a/core/corebank/post-accounts-USERNAME-token.rst b/core/corebank/post-accounts-USERNAME-token.rst
@@ -1,6 +1,11 @@
.. http:post:: /accounts/$USERNAME/token
- Create an authentification token.
+ Create or refresh an authentication token.
+
+ With HTTP Basic authentication, this endpoint creates a token from the
+ account password. With bearer authentication, the presented token must be
+ refreshable and the endpoint rotates it into a newly generated token. A
+ refreshed token cannot have a broader scope than its source token.
**Request:**
@@ -34,6 +39,7 @@
Invalid or missing credentials.
:http:statuscode:`403 Forbidden`:
* ``TALER_EC_GENERIC_FORBIDDEN``: missing rights.
+ * ``TALER_EC_GENERIC_TOKEN_PERMISSION_INSUFFICIENT``: the source token is not refreshable or the requested scope is broader than its scope.
* ``TALER_EC_BANK_ACCOUNT_LOCKED``: account is locked and cannot create new token using its password.
**Details:**
@@ -50,3 +56,17 @@
// Opque access token.
access_token: string;
}
+
+ **Refresh rotation:**
+
+ A successful bearer-authenticated refresh atomically creates the replacement
+ token and limits the source token's expiration to the earlier of its existing
+ expiration and five minutes after the first successful refresh. This is a
+ fixed, non-sliding overlap period: later refresh retries never extend the
+ source token's new deadline. The source token remains usable until that
+ deadline so that a client can retry after losing a successful response.
+
+ A retry during the overlap period may create another valid replacement.
+ Clients must therefore treat each successful response as a fresh credential,
+ persist it atomically and tolerate multiple replacement tokens. After the
+ source token expires, it can no longer authenticate or be refreshed.
diff --git a/core/corebank/post-accounts.rst b/core/corebank/post-accounts.rst
@@ -22,7 +22,9 @@
// Legal name of the account owner
name: string;
- // Make this account visible to anyone?
+ // Make this account visible to anyone? If true, the balance, payto
+ // URI and complete existing and future transaction history, including
+ // subjects and both parties' payto URIs, are anonymously accessible.
// Defaults to false.
is_public?: boolean;
diff --git a/libeufin/bank-manual.rst b/libeufin/bank-manual.rst
@@ -264,7 +264,13 @@ XXX Cashout channel
Used in a regional currency setup to specify the external account number of a bank account in fiat currency that belongs to the merchant. Allows the merchant to exchange its regional currency money for wire transfers in fiat currency into this account. Optional. Not available unless the bank is configured for regional currencies.
Is this account public?
- Public accounts can be viewed without access control, their balance and transaction history becomes public.
+ Public accounts can be viewed without access control. Their balance, payto
+ URI and complete existing and future transaction history become public.
+ Transaction disclosure includes wire subjects and both parties' payto URIs;
+ it is not redacted. Changing an account from private to public immediately
+ exposes its old history as well. Accounts that were already public before
+ this Core Bank API v12 correction are automatically subject to the same
+ disclosure, so operators should review them before upgrading.
After submitting the form, a randomly created password for the new account
will be shown in a notification. The administrator can also change passwords