CommBank ID + income

Design · docs/issuer-notes.md

CBA issuer notes - offers, gateway, MATTR, QR landing

Covers participants/cba-issuer/ (offers-api, offers-gateway, mattr config) and apps/qr-landing/. Contracts: docs/CONTRACTS.md up to v1.3.

The flow

  1. The app calls POST {OFFERS_GATEWAY_URL}/offers {profile, listingId?} with its CBA access token.
  2. PingAuthorize validates the JWT (CBA_JWKS_URL), reads GET {POD_RUNTIME_URL}/pods/{sub}/status?purpose=offer-eligibility and decides:
    • commbank-id-income with no usable pod (none, destroyed, failed) → 302 to {ADR_CDR_KIT_URL}/consent/start?return=cbaapp%3A%2F%2Fconsent-complete&listingId=…&session=…
    • pod created, loading or loaded → 409 {"error":"pod_loading","state":…}
    • income_ready, or commbank-id → forwarded to offers-api, which returns 200 {credentialOffer}
  3. offers-api creates a MATTR authorization-code offer and returns MATTR's uri verbatim.
  4. The wallet logs in at CBA PingFederate (MATTR's auth provider). At issuance MATTR calls GET {OFFERS_API_URL}/claims/mattr?sub=…, and offers-api merges identity (data/customers.json) with the pod's income claims.

With OFFERS_LOCAL_PDP=true (the default), offers-api makes the same 302/409/200 decision itself, so the demo still runs without PingAuthorize. Set it to false behind the gateway. On Railway it's false, with OFFERS_GATEWAY_ONLY=true, so /offers only answers the gateway over the private network (see participants/cba-issuer/gateway/deploy/README.md).

Income from CommBank accounts and other banks (v1.9)

The income credential draws on the customer's own CommBank accounts and, optionally, other banks through the CDR. Pods are for CDR only.

The session handle

An HS256 JWT over OFFERS_SESSION_SECRET (shared with adr-cba), with claims {sub, listingId, return, exp} plus iat. It lasts 10 minutes by default and is capped at 15. return must start with cbaapp://, and offers-api refuses to start otherwise. The gateway gets it from GET /internal/consent-location, because PingAuthorize can't sign HS256 itself.

MATTR endpoints used

Confirmed against the Platform API OpenAPI spec v12.2.0 (learn.mattr.global/api/api-reference/platform-latest.yml), the mattr-vii skills, and read-only calls to cba-demo1:

Call Use
POST {MATTR_AUTH_URL}/oauth/token client credentials, audience = tenant URL
GET /v2/credentials/mobile/configurations finds the config id by type (cached)
POST /v1/openid/offers auth-code offer: {credentials:[id], request_parameters:{login_hint: sub}} → 200 {uri}

There is no issuer_state on MATTR's auth-code offer API. The offer is reusable, and the holder is whoever logs in at the auth provider. offers-api binds the customer by sending login_hint = sub. The auth-provider template forwards login_hint to PF. The binding that actually holds is PF's sub flowing into the claim source.

The URI is openid-credential-offer://?credential_offer=… (inline), not credential_offer_uri=. The app should treat it as opaque.

MATTR config (participants/cba-issuer/mattr/)

Not applied. Apply in two phases, after reading the plan:

cd participants/cba-issuer/mattr
set -a; . /path/to/mattr.env; set +a           # MATTR_CLIENT_ID / MATTR_CLIENT_SECRET
OFFERS_API_URL=https://<offers-api public> INTERNAL_API_KEY=… node render.mjs && ./plan.sh
MATTR_CONFIG_ROOT=$PWD/platform-config python3 <mattrctl.py> sync --env cba-demo1     # creates the claim source
CLAIM_SOURCE_ID_CBA_ISSUER=<id from sync / list> … node render.mjs && ./plan.sh && <same sync>  # links it

Auth provider swap. MATTR allows one provider per tenant. Today that's idp-mattr-pf, which other cba-demo1 demos use. Swapping to adr-cba PF breaks them. The cleanest route is a direct PUT /v1/users/authentication-providers/{id} with the new url, client and secret, so the id stays the same. After that, copy MATTR's redirectUrl into adr-cba's var.mattr_redirect_uris. render.mjs --swap-auth-provider plus sync --prune also works, but prune deletes every live resource that isn't in git.

Listing threshold rule

incomeThreshold = ceil(rentPerWeek × 52 / RENT_TO_INCOME_RATIO). The ratio defaults to 0.30 and is applied to net income, because the pod's income.annualised is net. For $750/wk that's $130,000 net a year.

The demo customer Alex Chan earns about $75,849 net, so he fails that listing at 0.30. For the proof to pass on stage, set RENT_TO_INCOME_RATIO=0.52 (threshold $75,000) or lower the rent.

QR landing

/listings/surry-hills-123 shows the listing and a server-rendered QR for https://{QR_LANDING_HOST}/rent/verify?listing=surry-hills-123. /rent/verify is the fallback page, with a cbaapp://income-verify?listing=… button. /.well-known/apple-app-site-association lists {APPLE_TEAM_ID}.au.com.cba.hackathon.wallet for /rent/verify*. /api/listings/{id} returns {id, address, rentPerWeek, incomeThreshold, agency}.

Run

cd participants/cba-issuer/offers && pnpm install && pnpm test        # 67 tests
cd participants/cba-issuer/core-banking && pnpm test                    # 20 tests
cd apps/qr-landing && pnpm install && pnpm test                         # 5 tests
# gateway: see participants/cba-issuer/gateway/deploy/README.md (15 PAP decision tests + 10 e2e)