CommBank ID + income

Design · docs/dh-notes.md

Data Holders and Register - build notes

The mock CDR Register and the three Data Holders (Westpac, NAB, ANZ) are authentic Ping CDR Kit participants, built from the vendored kit at vendor/pingidentity-cdr-sandbox (a pinned copy of github.com/pingidentity/pingidentity-cdr-sandbox). Nothing from idp-openbanking-demo is used, and no kit function is hand-rolled.

The earlier Node build (a Node CDR kit inside PF, a Node consent app, dh-banking-api and a Node Register) is superseded. It now lives in archive/non-kit/:

Topology

                         participants/register  (host :8084, Docker network `cdr`, alias cdrregister)
                         kit test harness: SSAs, Register JWKS, DH brands, ADR status, CDR PKI (CA, /public/sign)
                                   ▲  (DHs reach it as cdrregister:8084 -> host-gateway)
                                   │
 browser / ADR ──mTLS──► PingAccess 8.3.2 ── https://{sso,api,consent}.<brand>.localhost:944x
                          │  kit rules: MTLS-RequireClientAuth, HoK thumbprint, CDR Validate Status,
                          │  CDR Refresh Token Processor, DCR rewrite /register -> /as/clients.oauth2
                          ├──► PingFederate 13.0.0 (:9032 mutual TLS)  pf-cdr-au-modules: CDRAUPlugin DCR policy,
                          │        CDRAdapterSelector, cdrPolicyFragment, consent grant storage, arrangement revocation
                          │        └─ identifier-first (mobile) -> OTP form (LDAP PCV) -> agentless consent adapter
                          ├──► agentless-consentapp (kit image) ── account list from mock-dh-apis, drop-off to PF
                          └──► PingAuthorize 10.3.0.4 (kit "PingDataGovernance" gateway, embedded policy)
                                   └─ X-USER / X-ACCOUNTS from the token + consent -> mock-dh-apis (kit image)
 PingDirectory 10.3.0.4: customers (ou=people), OAuth clients, persistent grants, Consent API, Register cache
 PingDataSync 10.3.0.4 (pds-cdr-au-modules): Register status -> PD cache (10 s); consent revoked -> notify ADR
 datain-configure-pf (kit side-car, one-shot): Register CA into PF/PA, Register-signed PA certs, site mTLS

One stack per bank, each with its own private Docker network, where every container keeps its kit hostname (pingfederate, pingaccess, pingdirectory, pingdatagovernance, agentless-consentapp, mock-dh-api). Only PingAccess publishes a port.

What each component does in a DH

Component Image Kit artefact Role
PingFederate pingidentity/pingfederate:13.0.0-edge pf-cdr-au-modules-v1.4.11-SNAPSHOT.jar (+ data-in jar, Clickatell jar, unused) AS: DCR with SSA (CDRAUPlugin), PAR, request objects, CDR scopes/claims, cdr_arrangement_id, grants in PD, arrangement revocation, CDR discovery template
PingAccess pingidentity/pingaccess:2601-8.3.2 pa-cdr-au-modules-v1.2.11-SNAPSHOT.jar Only public entry; mTLS; HoK; status checks against the Register cache; proxies PF, the consent app and the APIs
PingDirectory pingidentity/pingdirectory:10.3.0.4-latest pd-cdr-au-data-in-modules, schema, kit LDIF Users, OAuth clients, grants, Consent API, metadata cache
PingDataSync pingidentity/pingdatasync:10.3.0.4-latest pds-cdr-au-modules-1.0.3.jar Register status → PD; DH-initiated revocation → ADR
PingAuthorize pingidentity/pingauthorize:10.3.0.4-latest kit DeploymentPackage API gateway policy: validates the PF token, injects X-USER/X-ACCOUNTS
Consent app tamatping/agentless-consentapp:20231123 - CDR account selection; drop-off to PF agentless adapter
Banking API tamatping/mock-dh-apis:20231123 - CDS banking endpoints from its cache
Side-car tamatping/datain-configure-pf:20231123 - Kit post-config (certs, listeners)
Register tamatping/cdr-register-testharness:20231123_1 - Mock CDR Register + PKI

Versions: the kit pins pingfederate:13.0.0-edge and pingaccess:2601-8.3.2; both still pull and run on arm64. Its pingdirectory/pingdatasync/pingauthorize:edge tags now point at 11.1, which the kit's PD extensions predate, and Docker Hub no longer serves 10.2, so PD, PDS and PAZ are pinned to 10.3.0.4-latest. All the tamatping/* images are multi-arch.

How a DH participant is built

node deploy/kit/build-dh.mjs <brand|all> copies the kit profiles into participants/dh-<brand>/server-profiles/{pingfederate,pingaccess,pingdirectory,pingdatasync,pingauthorize} and applies a short, listed set of changes (see each server-profiles/KIT-CHANGES.md):

  1. Ports: the kit's local-dev listener :6443 becomes ${DH_PUBLIC_PORT} (9441/9442/9443) in PF and PA, and so does the hard-coded X-Forwarded-Port: 443 in two PA Groovy rules.
  2. OTP login. The kit fragment cdrPolicyFragmentNoMFA is identifier-first → Clickatell SMS → agentless consent. Clickatell's demo mode only accepts 123456 for the number 12345678910. So that step is now a PF HTML Form adapter (cdrOtpFormAdapter, cloned from the kit's htmlFormAdapter) with an LDAP PCV (cdrOtpPCV, cloned from the kit's adrUserPCV) that finds the customer by uid or mobile in PingDirectory. The demo code 000789 is the customer's LDAP password. The identifier-first lookup also matches mobile. All other parts of the fragment are unchanged, and nothing outside PF is involved.
  3. Customers: the kit's CRN0-5 LDIF is replaced with this brand's customers from data/customers.json. Each has uid = DH customer id, mobile, userPassword = the OTP, and entryUUID = nameUUIDFromBytes(customerId).
  4. Branding: the PF identifier-first page is re-skinned, the OTP page is new, and assets/brand/{brand.css,logo.svg} are added. The consent app uses consent-app/templates/index.html through SPRING_THYMELEAF_PREFIX. This template has the same Thymeleaf model as the kit's, but a brand skin and the CX layout: ADR name and accreditation tick, account selection with ineligible accounts greyed out and the reason given, data-language clusters, sharing period, Confirm/Cancel. The kit image is unchanged.
  5. Banking data: mock-dh-api/cache/ pre-seeds the kit image's own cache (/tmp/cache/<model class>/<X-USER>/*.txt), keyed by the customer's entryUUID, from data/seed.json.

It also writes docker-compose.yml and env/dh.env (from deploy/kit/templates/), copies the kit's pf.env/pa.env, and adds scripts/smoke.{sh,mjs}.

Data flow for seeds: data/customers.json → node data/generate-seeds.mjs → participants/dh-<brand>/data/seed.json. This includes 12 months of salary credits plus noise, and the joint accounts are ineligible. Then deploy/kit/build-dh.mjs → PD LDIF + mock-dh-apis cache. Don't run the generator while a stack is booting. It rewrites the mounted profiles.

Run

docker network create cdr                                          # once (Register + ADR use it)
docker compose -f participants/register/docker-compose.yml up -d    # ~10 s
docker compose -f participants/dh-westpac/docker-compose.yml up -d  # ~8-10 min to first ready
participants/dh-westpac/scripts/smoke.sh                            # Alex, 0412345678 / 000789

NAB is the same with dh-nab, port 9442 and Alex's NAB mobile 0498765432. ANZ uses dh-anz, port 9443 and Priya on 0400000002.

You're ready when https://sso.<brand>.localhost:<port>/.well-known/openid-configuration returns 200 and dh-<brand>-kit-configure has exited. Browser: open the ADR flow, or authorize URLs, directly. *.localhost resolves to 127.0.0.1 and the TLS cert comes from the Register CA, so accept the warning once.

Smoke test (scripts/smoke.sh)

The smoke test runs the kit's own Postman sequence (cdr-au.consent.postman_collection.json) and uses the Register test tools to build the ADR-side JWTs. Software product cba-dh-smoke uses the harness helper JWKS.

  1. Get the SSA from the Register. Get a Register-signed mTLS client cert.
  2. Discovery (issuer, PAR, cdr_arrangement_revocation_endpoint).
  3. DCR:
    • a forged SSA → 400;
    • no client cert → refused;
    • a valid SSA → 201 dcr-sso-….
  4. PAR with a signed request object → request_uri.
  5. Authorize:
    • branded mobile page (with the "never ask for your password" notice);
    • a wrong OTP is rejected;
    • 000789 → kit consent app;
    • account selection (ineligible accounts greyed out) → Confirm → code at redirect_uri.
  6. Token (private_key_jwt + PKCE + mTLS) → access, refresh and cdr_arrangement_id.
  7. GET /banking/accounts (only the consented accounts come back) and GET /accounts/{id}/transactions (salary credits present). Without the bound cert → refused.
  8. POST /data-holder/arrangements/revoke → 204. After that, the API refuses the token and refresh is refused.

Verified on 7 Oct 2026. Each stack ran on its own from a clean up, on arm64, and was ready in about 220 s:

DH Customer Result
Westpac Alex, 0412345678 36/36. 3 accounts selectable, joint account greyed out, 43 salary credits
NAB Alex, 0498765432 35/35. 1 account (Reward Saver), no salary at NAB, as in customers.json
NAB Jordan, 0400000003 35/35. 76 weekly salary credits
ANZ Priya, 0400000002 36/36

A re-run against a live DH hits the kit's "one registration per software_id" rule. The smoke test then reuses the kit's deterministic client id (dcr-sso- + nameUUIDFromBytes(software_id)).

Known gaps