Skip to content

fix: signing baskets follow NextGenPSD2 1.3.16, belong to their TPP and book what they authorise (minimal) - #2936

Open
hongwei1 wants to merge 1 commit into
OpenBankProject:developfrom
hongwei1:feature/signing-basket-poc
Open

hongwei1 wants to merge 1 commit into
OpenBankProject:developfrom
hongwei1:feature/signing-basket-poc

Conversation

@hongwei1

@hongwei1 hongwei1 commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

fix: signing baskets follow NextGenPSD2 1.3.16, belong to their TPP and book what they authorise (minimal)

Base: origin/develop (78406d7f7). 1 commit, about 400 changed lines of main code.

A deliberately small version of the Berlin Group signing basket for the mapped connector, enough to run the main flow (create a basket, start its authorisation, answer with the OTP, book the payments one by one, ACTC) without the defects found in the current implementation. A fuller version (execution ledger, resumption scheduler, consents, PSU binding, results endpoint) is in #2934; this PR is the subset worth reviewing first.

What changes

Response shape and validation

  • transactionStatus is upper case (RCVD, not rcvd).
  • _links.scaStatus is a { "href": ... } object, and the PUT answer links to /signing-baskets/{basketId}/authorisations/{id} instead of a payment URL.
  • An empty id list, or the same id twice, is 400 FORMAT_ERROR.
  • Authorisation bodies that are not implemented (updatePsuAuthentication, selectPsuAuthenticationMethod, authorisationConfirmation) are refused with 400 SERVICE_INVALID instead of being discarded.
  • A wrong or expired OTP is 401 PSU_CREDENTIALS_INVALID; an authorisation id the basket does not have is 404; unknown basket or one the caller may not address is 403 RESOURCE_UNKNOWN; status conflicts are 409 STATUS_INVALID.

Ownership. A basket records the consumer that created it (nullable consumerid column on signingbasket, added by Schemifier). Every operation is limited to that consumer. An unknown basket, another TPP's basket and a basket created before ownership was recorded all answer the same way. A payment may only be put in a basket by the TPP that lodged it, and only while it awaits SCA.

State. Delete (RCVD -> CANC) and the claim of an answered authorisation (RCVD -> AUTHORISING) are conditional updates, so an answer and a delete racing each other have one winner. A basket that is no longer RCVD cannot be deleted or given a new authorisation.

Answering the authorisation (PUT .../authorisations/{id}), in this order: instance setting, ownership, the authorisation belongs to the basket, body variant, basket and challenge state, every payment still waiting for SCA; then the answer; then the claim; then the payments are booked one after another, each awaited, so the money has moved when the response is sent. Only a challenge the connector records as finalised authorises anything. The basket becomes ACTC only if every payment was booked. Before this change a successful PUT stored the payments as COMPLETED and the basket as ACTC while no account moved.

Switched off by default. signing_basket_authorisation_enabled (default false): the PUT answers 403 SERVICE_BLOCKED until it is set. Creating, reading, starting an authorisation on and deleting baskets are not affected.

Consents are refused (400 SERVICE_INVALID) until their authorisation exists. Before this change a consent named in a basket was recorded and never authorised.

Behaviour changes visible to TPPs

Call Before After
any basket response transactionStatus: "rcvd" "RCVD"
unknown basket / another TPP's basket 400 OBP-50200, or success for any holder of the id 403 RESOURCE_UNKNOWN
DELETE after the authorisation was answered 204, basket became CANC 409 STATUS_INVALID
POST authorisation on a cancelled or authorised basket 201 409 STATUS_INVALID
POST basket with [], duplicates, invented or already booked ids 201 400 / 409
POST basket naming a consent 201, never authorised 400 SERVICE_INVALID
POST authorisation with PSU credentials in the body 201, body discarded 400 SERVICE_INVALID
PUT authorisation authorised the basket, did not reliably book 403 unless the property is true; then books the payments before answering
PUT with a payment that is no longer waiting for SCA accepted 409, nothing booked

Known limits (it is a proof of concept)

  • Several payments are not one transaction. If one cannot be booked the earlier ones stay booked, the basket stays AUTHORISING (reported RCVD) and the response says not every payment was booked; each payment's own status shows which. Nothing resumes it.
  • Nothing prevents the same payment from being put in two baskets, or from being authorised on its own while a basket holds it; the check that a payment still awaits SCA is the only guard.
  • The challenge is minted for the authenticated user, so the flow assumes the PSU's own token. A client-credentials TPP relaying an OTP is not handled.
  • Only the mapped connector is considered; other connectors are untested.
  • Not in this change: Location and ASPSP-SCA-Approach headers, per-member results, consents, PSU binding.

Verification

  • SigningBasketServiceSBSApiTest (45 scenarios, including payments booked and balances moved, ownership across two TPPs and a legacy basket, answer replay against another basket, a wrong answer, the instance setting, PUT racing DELETE, and a stand-in connector that returns a failed or unfinished challenge) and MappedSigningBasketProviderTest (4) pass.
  • BG PIS, AIS, consent access and JSON factory suites pass (130).
  • scripts/check_lift_http4s_resource_doc_parity.py and the test-isolation lint are clean; no ResourceDoc was changed.

…nd book what they authorise

A minimal signing basket for the mapped connector.

Responses and validation: transactionStatus is upper case, _links.scaStatus
is a href object, the PUT answer links to the basket's authorisation, an
empty or duplicated id list is a format error, and authorisation bodies the
server does not implement are refused by name instead of discarded. Codes
outside the standard's list (wrong OTP, unknown basket or authorisation,
status conflicts) are mapped to ones inside it.

Ownership: a basket records the consumer that created it and every operation
is limited to that consumer; an unknown basket, another TPP's basket and a
basket created before ownership was recorded answer 403 RESOURCE_UNKNOWN.

State: delete and the move to AUTHORISING are conditional updates, so an
answer and a delete racing each other have one winner.

Answering the authorisation checks everything first (instance setting,
ownership, basket and challenge state, every payment still waiting for SCA),
then the answer, and only a challenge recorded as finalised authorises
anything. The payments are then booked one after another, each awaited, and
the basket is ACTC only if every one was. Authorisation is off by default
(signing_basket_authorisation_enabled).

Payments are admitted only if the caller lodged them and they await SCA.
Consents in a basket are refused until their authorisation exists.
@sonarqubecloud

sonarqubecloud Bot commented Oct 7, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant