Design · docs/adr-notes.md
ADR (CBA) notes
CBA is the Accredited Data Recipient: participants/adr-cba. It is now an authentic Ping CDR Kit "data-in" participant. The kit's PingFederate, PingAccess, PingDirectory and PingDataSync components, with the kit's data-in modules, do every piece of CDR protocol work. CBA adds:
- a thin web layer for the CX screens;
- one PingFederate Reference ID adapter, cloned from the kit's own;
- one PingDataSync sync destination that publishes events.
History
- Spike (7 Oct, morning). PF 13.1.3 loaded
pf-cdr-au-data-in-modules. The spike concluded the kit needed PD + PA, mTLS and the hybrid flow, and had no event hook, so it was "not viable in hackathon time". - First build: hand-rolled Node
cdr-kit. It did DCR, PAR, token custody, refresh and revocation itself. Archived atarchive/non-kit/adr-cdr-kit/(with its dev DH stubs indev-stubs/). It is not used, and nothing in the workspace imports it. - Rebuild (this document). The user asked for authentic kit participants on their own Ping stacks. Each of the spike's objections is now solved inside Ping config:
| Spike objection | How it's solved |
|---|---|
| Needs PingDirectory + PingAccess | Both run with the kit profiles and data-in jars (PD 11.0.0.4, PA 8.3.2) |
| mTLS to the Register and DHs | The kit's own pattern. The configurator gets a network certificate from the Register CA and installs it as PA's mTLS site authenticator. PF, PD and PDS reach each DH through PA, via the adapter's "Local Base URL" per DH |
| Hybrid flow | The kit does it (code id_token, encrypted id_token, nonce, acr check). The kit DHs support it |
| No event hook | A PingDataSync sync pipe on the kit's token store, with a Server SDK sync destination that publishes to Redis |
Components (participants/adr-cba/)
| Path | What |
|---|---|
server-profiles/pingfederate |
Kit PF profile (bulk config + pf-cdr-au-data-in-modules-1.5.11.jar) on PF 13.0.3 (the kit's 13.0 line; 13.1 is Jakarta and breaks the kit's javax /arrangements/revoke servlet). CBA changes listed below |
server-profiles/pingaccess |
Kit PA profile (pa-cdr-au-data-in-modules-1.2.6.jar). The applications are CBA's: one data-in API app and one mTLS egress app per DH |
server-profiles/pingdirectory |
Kit PD profile: data-in SCIM schemas (adr-token, adr-token-refresh, adr-token-revoke, adr-dataholder(-read), adr-software), pd-cdr-au-data-in-modules-1.2.6.jar (TokenMgt refresh plugin), the kit LDIF |
server-profiles/pingdatasync |
Kit PDS profile: ADHRevokeConsentPipe (kit RevokeDataHolderConsentSyncDest) + the CBA CBAArrangementEventsPipe |
profile-overlay/ |
CBA files laid over the kit profiles (client hook, branded error page, event pipe, event-bridge jar, captured PA config) |
scripts/derive-profiles.py |
Rebuilds server-profiles/ from vendor/pingidentity-cdr-sandbox/server_profiles + the overlay. Every CBA change to the kit is in this script. The profiles are committed output, each usable on its own as a SERVER_PROFILE |
scripts/gen-keys.sh |
Software-product key (PKCS12 → the PF signing / OIDC static keys = /pf/JWKS), PF TLS key, system keys, secrets → keys/ (gitignored) |
scripts/capture-config.py |
Captures the configured PA (after configure) back into the profile, kit style: ${_data_…} placeholders, values in keys/pa.env |
event-bridge/ |
Java source + build.sh for com.cba.cdr.adr.pds.ArrangementEventsSyncDestination (Server SDK; RESP client, no extra jars) |
web/ |
@cba/adr-cba-web (Node 20). CX consent screens, the customer hand-off, OAuth callback, dashboard, /dev/token. No CDR protocol code |
kit.env |
Non-secret config (the sandbox's cdr.env for CBA): hostnames, Register, the DH list, Redis |
docker-compose.yml |
pingdirectory, pingfederate, pingaccess, pingdatasync, web, plus configure (profile configure: the kit's tamatping/datain-configure-pf:20231123) |
Changes from the kit (all in derive-profiles.py)
- PF. Identity and URLs come from env:
- base URL / virtual hosts;
DataHolderSelector: Data Recipient Issuer, Register URLs, brandcommbank, softwarecba-income-verify, SCIMhttps://pingdirectory:1443/scim/v2/, redirect{ADR_PUBLIC_BASE_URL}/ext/cdr/data-in/callback;- the Data Holder List is Westpac / NAB / ANZ, each with issuer + Local Base URL
https://sso-<brand>.internal:3000(PA egress); - request template scopes
bank:accounts.basic:read bank:transactions:read common:customer.basic:read.
- PF metadata. "Metadata Params" =
use_consents,deletion_election,purpose,listing_id,consent_type. Those, plusoverride_sharing_duration, are added to the tracked-parameter allowlist. - PF customer step. In "ADR - Data In - Policy", the kit's local HTML-form login is replaced by
cbaCustomerRef, a Reference ID adapter cloned from the kit's ownconsentAgentlessAdapter. Its subject feeds the kit adapter's user id (userIdAuthenticated). The unused "not in request" branch now continues. - PF contract (additive).
adrContract→adrATM→adrOIDCPolicyalso carrycdr_arrangement_id,data_holderandsharing_expires_atfrom the kit adapter's output. - PF pod token. Mapping
client_credentials|adrATM(sub=cba-pod-runtime,software_id). - PF webdefault. Only the data-in
/arrangements/revokeservlet, with audience{ADR_PUBLIC_BASE_URL}. The kit's DH-side servlets are dropped. - PF hook
87-cba-oauth-clients.sh. Clients live in PD (the kit usesClientManagerLdapImpl), so the hook upsertscba-adr-webandcba-pod-runtimethrough the admin API from env secrets. It also sets the tracked-parameter allowlist: the bulk-configconfigStoreentry is ignored on import. - PF adrATM resource URIs = the PA data-in bases. PA introspects with
aud= the request URL, and PF picks the ATM by resource URI, as the kit did for its SPA. - PF runtime TLS certificate is issued by the Register CA (
gen-keys.sh→/public/sign), as the kit's ADR endpoints are. Without it, DHs refuse to fetch the SSAjwks_uri(PKIX path building failed) and DCR fails with "Signature verification failed". - PF error page. A CBA-branded
http.error.page.template.html(rendered by the kit adapter on DH errors) that sends the customer back to the web layer. - PD. The PF JWKS port and the SPA redirect in the kit's sample-client LDIF.
- PDS. The DH-side pipes (
30–33: recipient-consent revocation and the Register cache for a holder) and their jar are removed. The data-in revocation pipe is kept.40-CBAArrangementEvents.dsconfigis added. - PA.
- The token provider is the kit PF.
- The sandbox's DH applications are replaced with, per DH:
/dh/<brand>/cds-au(the kit'sCDRInjectDataHolderTokenRule, withHTTP Header - Subject/Context=X-CDR-Subject/X-CDR-Contextfor service callers, plus the kit URL-rewrite rule), and an egress app onsso-<brand>.internal:3000→ the DH, usingCDR-SiteAuthenticator(mTLS).
How a consent runs
app ─302 (offers)─► web /consent/start?session (HS256 handle → CBA customer)
CX screens: purpose, clusters, uses, duration, deletion, policy → bank chooser
(DHs = kit PD adr-dataholder-read: the DHs the kit registered with and the Register says are active)
web ─303─► kit PF /as/authorization.oauth2 client cba-adr-web, PKCE, data-holder=<issuer>,
use_consents, deletion_election, purpose, listing_id, consent_type, override_sharing_duration
kit PF policy: cbaCustomerRef ─POST─► web /pf/authn ─dropoff {subject}─► PF ─resume(REF)
→ DataHolderSelector (kit): PAR + PS256 request object (via PA egress, mTLS) → DH authorize
DH (kit): login + OTP + account selection ─hybrid #code,id_token─► kit PF /ext/cdr/data-in/callback
kit: code exchange (private_key_jwt, PKCE), id_token decrypt + nonce/acr, refresh-token introspection,
tokens → PD adr-token (+ metadata) → APC → adrATM code → web /consent/callback
web: code → adrATM claims (cdr_arrangement_id, data_holder) → "Bank connected" → cbaapp://consent-complete?status=success&…
PDS (CBA pipe): new tokenMgtInstance → ArrangementEstablished on cdr.arrangementsCancel on CBA screens → status=cancelled. Refusal at the DH (access_denied) → the kit renders the CBA PF error page → /consent/abort → status=denied. Any kit or DH error → status=error.
Event bridge (requirement 2): PingDataSync
Source.
CBAArrangementSource: the PD changelog underou=adr-software,o=appintegrations, the same subtree the kit's revocation pipe watches.Sync class.
(objectClass=tokenMgtInstance). It auto-maps only non-secret attributes (arrangement id, DH issuer, software id, user ref, status, status message, sharing expiry, approved scopes, metadata JSON), so the destination never sees DH tokens.Destination.
ArrangementEventsSyncDestination(Server SDKSyncDestination):- CREATE →
ArrangementEstablished. Consent choices come from the kit's metadata, anddataHolderBrandIdfrom the kit's DH record. - status →
revoked→ArrangementRevoked(revokedBy=consumer/adrwhen CBA setstatus_msg, otherwisedata-holder). - status →
expired→ArrangementExpired. - Token refreshes are ignored: status is unchanged.
- CREATE →
Retries. A failed XADD fails the sync operation with
RETRY_OPERATION_LIMITED, so PDS retries it from the changelog.Expiry. The kit has no expiry marker. The extension's sweep (every
EXPIRY_SWEEP_SECONDS) setstokenMgtStatus=expiredon the kit records it covers:- ongoing arrangements whose
sharing_duration_expires_athas passed; - once-off arrangements older than
ONCE_OFF_USE_SECONDS.
The change flows through the pipe like any other. Expired records are not revoked at the DH, because the kit's revoke pipe acts only on
revoked.- ongoing arrangements whose
Why not PD notifications or the changelog read from Node? PDS is the kit's own mechanism for reacting to token-store changes (RevokeDataHolderConsentSyncDest is exactly this pattern), and it gives durable, resumable delivery from the changelog.
Pod data access (requirement 3): through the kit's PingAccess
The pod never holds a DH token.
Token.
token-manager.getDataAccess()gets a client-credentials token from the kit PF (cba-pod-runtime, adrATM, cached until expiry).Calls. It calls
{ADR_DATA_PROXY_URL}/dh/{brand}/cds-au/v1/...withX-CDR-Subject: {customerSub}andX-CDR-Context: cba-adr-web.Mediation. In PA,
CDRInjectDataHolderTokenRule:- validates the token (introspection);
- checks the DH is active for CBA's software id;
- reads
adr-token-refreshfor subject + DH + context + software id. The PD TokenMgt plugin refreshes the DH token on read when it is within 120 s of expiry; - swaps
Authorization; - forwards through the mTLS site authenticator.
Code change.
packages/pod-programs:ProgramConfig.adrDataProxy;tokenManager.getDataAccess;CdsClientextraHeaders;- the loader uses
getDataAccess.
packages/pod-runtimereadsADR_DATA_PROXY_URL,ADR_TOKEN_URL,ADR_POD_CLIENT_ID,ADR_POD_CLIENT_SECRETandADR_CDR_CONTEXT. WithoutADR_DATA_PROXY_URLit falls back to the old endpoint (dev fakes only).
Revocation (requirement 4)
- Pod / app "Stop sharing".
POST {web}/internal/arrangements/{id}/revoke(or/api/arrangements/{id}/stop) → SCIM PATCH on the kit'sadr-token-revoke(status=revoked,status_msg=consumer). Then:- the kit's
ADHRevokeConsentPipecalls the DHcdr_arrangement_revocation_endpoint(private_key_jwt, via the PA egress); - the CBA pipe publishes
ArrangementRevoked; - PA stops injecting tokens, because the record is no longer
active.
- the kit's
- DH-initiated. DH →
{ADR_PUBLIC_BASE_URL}/arrangements/revoke, the kit servlet. It verifies the DH-signed bearer JWT against the DH JWKS from the Register and setsstatus=revoked. The CBA pipe publishesArrangementRevoked(data-holder).
CBA customer tokens (requirement 5)
POST {web}/dev/token {sub} → RS256, JWKS {web}/dev/jwks.json, unchanged from the contract. The CBA OIDC OP for the iOS app and MATTR is still the separate participants/adr-cba/pingfederate (Terraform-built, :9034). Not yet folded into the kit PF. To fold it, add its PCV / OTP form adapter / JWT ATM / clients to derive-profiles.py and the client hook.
Networking (CONTRACTS v1.7 §50)
adr-cba shares no Docker network with other participants. The kit containers sit on a private adr-cba-kit network under their kit hostnames (pingfederate, pingaccess, pingdirectory, pingdatasync). Each kit container maps the Register (cdrregister) and every DH name (sso|api.<brand>.localhost) to host-gateway (extra_hosts), so cross-participant calls go through the published host ports, as they would on Railway. The DHs map sso.commbank.localhost → host-gateway:9444.
Test results (7 Oct, against the Westpac kit DH + Register test harness)
scripts/smoke.sh(DH_MOBILE=0400000004 CUSTOMER_SUB=cba-cust-004): 30/30 passed, including against the captured PA profile with no configurator re-run. Covered:- kit health;
- the SSA points at the kit PF JWKS and callback;
- kit DCR at Westpac (
dcr-sso-53f1b838-cba2-3a60-90de-4eba13dbe573); - CBA CX screens → kit PF → Reference ID hand-off → kit DataHolderSelector → PAR (signed request object, mTLS via PA) → Westpac login + OTP + kit consent → hybrid callback → kit code exchange → PD token store →
cbaapp://consent-complete?status=success&cdrArrangementId=…&dataHolderBrandId=westpac&listingId=…; ArrangementEstablishedfrom PingDataSync with the consent choices, no tokens;- pod path:
cba-pod-runtimetoken → kit PA/dh/westpac/cds-au/v1/banking/accounts→ 200 with Westpac data, the DH token injected by the kit rule and certificate-bound over the kit's mTLS egress. A customer with no arrangement gets 403; - withdrawal: the flag in kit PD → the kit's
ADHRevokeConsentPipe→ Westpac/data-holder/arrangements/revoke204 →ArrangementRevoked(consumer)→ PA 403.
pnpm --filter @cba/adr-cba-web test: 12/12.pnpm --filter @cba/pod-programs test: 20/20 (adds the kit data path, plus per-account filtering and de-duplication of transactions for the kit mock API, §49).pnpm --filter @cba/pod-runtime test: 5/5. Typechecks clean.- Not exercised end to end:
- DH-initiated revocation into the kit servlet;
- the PDS expiry sweep;
- NAB/ANZ, which were not running. DCR there is the same configurator step;
- a once-off consent. The kit DH issues no
cdr_arrangement_idwithout a sharing duration, so the kit cannot store a once-off arrangement (SCIM requires the id). Once-off is therefore a kit limitation against the kit DH; the smoke uses ongoing.
- Gotcha. Repeated failed DH logins lock the customer at the DH (kit HTML form adapter lockout). Alex (
0412345678) at Westpac was locked during testing; it clears after the lockout period.
Run
participants/adr-cba/scripts/gen-keys.sh # keys/ (gitignored); Register must be up so it issues the PF TLS cert
participants/adr-cba/event-bridge/build.sh # PDS extension jar -> profile-overlay
python3 participants/adr-cba/scripts/derive-profiles.py # kit profiles + CBA changes -> server-profiles/
# Register must publish CBA's SSA with the kit PF keys:
ADR_JWKS_URI=https://sso.commbank.localhost:9444/pf/JWKS ADR_REVOCATION_JWKS_URI=https://sso.commbank.localhost:9444/pf/JWKS \
docker compose -p register -f participants/register/docker-compose.yml up -d
docker compose -p adr -f participants/adr-cba/docker-compose.yml up -d --build # ~4 min (PD -> PF -> PA, PDS)
docker compose -p adr -f participants/adr-cba/docker-compose.yml --profile configure run --rm configure # kit: certs + DCR (re-run after a DH or PA restart)
python3 participants/adr-cba/scripts/capture-config.py # optional: freeze the configured PA into the profile
participants/adr-cba/scripts/smoke.sh # SMOKE_STAGE=health for kit checks onlyConsoles (local only): PF admin https://localhost:9934/pingfederate/app, PA https://localhost:7311, PD https://localhost:7312/console. All use Administrator / «redacted», the kit default. Memory is about 4.5 GB for PD + PF + PA + PDS + web.
Railway (deployed, 8 Oct)
See deploy/railway/README.md and participants/adr-cba/railway/. The kit PF is behind its Railway HTTPS domain. The kit PA data proxy for the pod is behind another. The PA egress reaches each DH's PingAccess TCP proxy with mTLS (the kit site authenticator). PingDataSync publishes to the project Redis over the private network. PD has a volume. Kit hostnames resolve through /etc/hosts aliases written by railway/entrypoint.sh. Verified in a real browser (Playwright Chromium) for Westpac and ANZ, then event → pod income_ready → withdrawal.
Deviations and gaps
- Register mTLS. The Register test harness serves plain HTTP with no mTLS listener, so PF reaches the Register directly (
CDR Register Base URL (MTLS)=http://cdrregister:8084). The DHs are reached with mTLS through PA. - Context. The kit keys an arrangement by subject + DH + context (the consenting client id
cba-adr-web), so a customer has one active arrangement per DH. A repeat consent amends it. - TLS verification. The web container sets
NODE_TLS_REJECT_UNAUTHORIZED=0locally because the kit PF and PD use self-signed certificates. - Demo credentials. Kit default admin credentials (
«redacted») and the kit's demo PA listener keys stay in place locally. The software-product, PF TLS and system keys are CBA's own (gen-keys.sh). - Fresh kit DH. DCR against a freshly started kit DH needs
configureto run again. That is the kit's "Execute Dynamic Client Registration" action, idempotent per DH.