Participants · participants/cba-issuer
ISS CommBank issuer
The issuer side decides whether a customer gets a credential offer or a trip through CDR consent. PingAuthorize sits in front of the offers API and returns a 302 into consent when the pod has no income yet, a 409 while it loads, and lets the call through once income is ready. The offers API creates the MATTR offer and doubles as MATTR's claim source.
What it does
- On Railway the gateway is
offers-gatewayand reaches pod-runtime and offers-api over private networking. offers-api answers/offersonly to the gateway (403 publicly);/claims/mattrstays public for MATTR. - Gateway policy is built by
build-policy.py(deny-with-302 plusLocationheader), tested with PAP decision tests and an e2e harness. - MATTR VII tenant
hackathon1: managed IACA, claim sourcecba-issuer-claims, auth provider = CBA PingFederate, mDoc configsau.com.cba.commbank-idandau.com.cba.commbank-id-income. - No
issuer_stateon MATTR auth-code offers - the customer is bound withlogin_hint = sub, and PF'ssubflows into the claim source. - QR landing serves the listings, the QR, the
/rent/verifyfallback and the AASA for universal links.
Ports and endpoints
| offers-gateway (PingAuthorize 11.1, the app calls this) | 7401 local · 1080 on Railway |
|---|---|
| offers-api | 7400 (/offers gateway-only, /claims/mattr public) |
| qr-landing | 7700 |
Railway
offers-api | offers-api-production.up.railway.app success |
|---|---|
offers-gateway | offers-gateway-production.up.railway.app success |
qr-landing | qr-landing-production.up.railway.app success |
Components and versions
Read from participants/cba-issuer/docker-compose.yml at build time.
| Service | Image | Host ports |
|---|---|---|
offers-api | (built from repo) | 7400→7400 |
offers-gateway | (built from repo) | 7401→1080 |
participants/cba-issuer/mattr/README.md
MATTR VII - hackathon tenant
Tenant: https://hackathon1.vii.au01.mattr.global (au01). Credentials live in the gitignored root mattr.env
(JSON: tenant_url, auth_url, audience, client_id, client_secret).
Setup
MATTR_ENV_FILE=../../../mattr.env python3 setup-tenant.py --dry-run
MATTR_ENV_FILE=../../../mattr.env python3 setup-tenant.pyIdempotent. Needs PyYAML. It creates/updates:
| Step | Resource | Status |
|---|---|---|
| 1 | Managed IACA (MATTR provisions the document signer) | 055d4474-ffb7-43f2-8ecb-e854e68a8cc2, active |
| 2 | Claim source cba-issuer-claims -> https://offers-api-production.up.railway.app/claims/mattr (x-api-key) |
c7cc4a5e-7b78-44e7-9a47-d7b735ddb550 |
| 3 | Auth provider -> CBA PingFederate https://cba-pf-production.up.railway.app (client mattr-vii) |
ca408707-430c-4811-a1bf-5f54404e0fd4; redirect https://hackathon1.vii.au01.mattr.global/core/v1/oauth/authentication/callback |
| 2b | Claim source cba-issuer-identity-claims -> same URL with income=0 (identity only, no pod needed) |
b55a428d-bd8b-4182-8da2-1fe9684e59b9 |
| 4 | mDoc configs au.com.cba.commbank-id / au.com.cba.commbank-id-income |
434dffc5-c6e8-4c9b-85b2-9a858926b190 -> cba-issuer-identity-claims / fe332445-784c-4b5d-b412-73a4bd463b7e -> cba-issuer-claims |
| 5 | Wallet OAuth client check (read-only probe of /v1/oauth/authorize) |
ios-sample-mobile-credential-holder-app usable |
Env for a full run (values in the root .env): OFFERS_API_URL=https://offers-api-production.up.railway.app,
ADR_CBA_PF_URL=https://cba-pf-production.up.railway.app, INTERNAL_API_KEY, MATTR_PF_CLIENT_SECRET. A run
missing the claim-source vars keeps the configs' existing claim source rather than unlinking it.
The identity config has its own claim source because /claims/mattr also asks the pod for income unless
income=0, and fails (404 pod_not_found, 409 not ready) for a customer with no pod - MATTR then answers the
wallet's credential request with 503 service_unavailable.
Re-run after steps 2-3 have their env vars: the configs are then updated with the claim source id. The
auth-provider response prints the redirectUrl that CBA PF's mattr-vii client must allow
(participants/adr-cba/pingfederate/terraform var.mattr_redirect_uris).
Wallet OAuth client (authorization-code issuance)
MATTR's /v1/oauth/authorize only takes a client it knows: anything else gets a 400 HTML page
"client is invalid", and an unregistered redirect gets "redirect_uri did not match any of the client's
registered redirect_uris". There is no API to register one. MATTR's docs say trusted wallet apps are set
up "by contacting MATTR": either a client ID with white-listed redirect URLs (lower assurance), or Wallet
Attestation - client ID, the wallet backend's root certificates and whether DPoP is required, configured
"through out-of-band processes":
- https://learn.mattr.global/docs/issuance/trusted-holder-apps/overview
- https://learn.mattr.global/docs/issuance/trusted-holder-apps/wallet-attestation
- https://learn.mattr.global/docs/issuance/credential-issuance/overview ("only registered wallets can call this endpoint")
The Platform API spec (v12.2.0) has an "OpenID OAuth Client" tag and a bare /v1/openid/clients/{id} path with
no operations; on this tenant GET/POST /v1/openid/clients and GET/PUT/DELETE /v1/openid/clients/{id} all
answer 404 Resource Not Found - not enabled. Holder applications (POST /v1/holder/applications, clientId
field) are something else: they register a MATTR Holder SDK app with the tenant's SDK backend (licence,
App Attest, wallet-attestation tokens). Creating one for cba-hackathon-wallet did not make the client valid at
/authorize (tried and deleted).
What works today: every tenant carries the sample clients MATTR's Holder SDK tutorials use (https://learn.mattr.global/docs/holding/credential-claiming-tutorial):
| client_id | redirect_uri |
|---|---|
ios-sample-mobile-credential-holder-app |
io.mattrlabs.sample.mobilecredentialholderapp://credentials/callback |
android-sample-mobile-credential-holder-app |
same |
Public client (token_endpoint_auth_methods_supported: ["none"]), PKCE S256, no client attestation, DPoP
optional (ES256; bearer also works). The iOS app uses the iOS one (apps/ios/CBAApp/Config/Base.xcconfig). For
anything beyond the hackathon, ask MATTR support to register cba-hackathon-wallet with redirect
cbaapp://callback and switch the xcconfig back.
MATTR's authorize also needs the configuration's scope (mso_mdoc:au.com.cba.commbank-id) in scope: openid
alone gets through PingFederate and then comes back error=access_denied.
Proving it: test-issuance.py
MATTR_ENV_FILE=../../../mattr.env python3 test-issuance.py [--profile identity|income] [--customer cba-cust-002]
[--scope ...] [--prompt login] [--no-dpop]Plain HTTP, as the SDK does it: offer (POST /v1/openid/offers, login_hint) -> metadata -> authorize as the
sample client -> CBA PF log-on (customer picker, OTP 000789) -> MATTR -> code -> token (PKCE + DPoP) -> key
proof (iss = client_id; MATTR has no nonce endpoint) -> credential -> decoded mDoc namespaces. The income
profile needs the customer's pod to be ready, or MATTR returns 503.
templates/ holds the resource bodies. platform-config/ is the old cba-demo1 mirror used by plan.sh
(mattrctl) - not used for hackathon1.
Railway (project cba-hackathon)
cba-pf— CBA PingFederate,https://cba-pf-production.up.railway.app(deploy:railway up ./participants/adr-cba/pingfederate --path-as-root --no-gitignore --service cba-pf)offers-api—https://offers-api-production.up.railway.app(deploy from the repo root:railway up --service offers-api;.railwayignoretrims the upload)
Shared secrets (INTERNAL_API_KEY, OFFERS_SESSION_SECRET, MATTR_PF_CLIENT_SECRET, CBA_PF_ADMIN_PASSWORD) are in the gitignored root .env.
participants/cba-issuer/gateway/deploy/README.md
offers-gateway (PingAuthorize)
PingAuthorize 11.1 as an API gateway in front of offers-api, in embedded PDP mode with policy/cba-offers.deploymentpackage baked into the image. There's no Policy Editor at runtime.
| Image | deploy/gateway.Dockerfile, build context participants/cba-issuer/gateway |
| Port | container 1080 (plain HTTP, Gateway servlet only), local 7401 |
| Railway | service offers-gateway, https://offers-gateway-production.up.railway.app (target port 1080). offers-api stays public for /claims/mattr, but answers /offers only over the private network (OFFERS_GATEWAY_ONLY). |
Env
| Var | What |
|---|---|
OFFERS_API_URL |
where permitted requests go, e.g. http://offers-api.railway.internal:7400 |
POD_RUNTIME_URL |
the policy's PodStatus information point |
INTERNAL_API_KEY |
sent as Authorization: Bearer … to pod-runtime and to offers-api /internal/consent-location |
CBA_JWKS_BASE_URL + CBA_JWKS_PATH |
CBA_JWKS_URL split in two, e.g. https://adr-cba-pf… + /ext/cba/jwks, or {ADR_CDR_KIT_URL} + /dev/jwks.json |
PING_IDENTITY_DEVOPS_USER / _KEY |
evaluation licence, fetched on every start. Set them yourself; never bake them. |
The three service values reach the policy as Gateway.podRuntimeUrl, Gateway.offersApiUrl and Gateway.internalAuthorization, through the endpoint's policy-request-attribute. That's how one package serves every environment.
Caveat: this means the internal key travels inside the policy request, so it can show up in PAZ decision or trace logs. Keep debug tracing off on Railway.
What it answers
| Request | Answer |
|---|---|
| no or invalid token | 401 {"status":401,"error":"invalid_token",…} |
commbank-id-income, pod created, loading or loaded |
409 {"status":409,"error":"pod_loading","state":"loading"} |
commbank-id-income, pod none, destroyed or failed (or pod-runtime down) |
302 Location: {ADR_CDR_KIT_URL}/consent/start?return=cbaapp%3A%2F%2Fconsent-complete&listingId=…&session=… + {"error":"consent_required","state":…} |
commbank-id, or income with pod income_ready |
forwarded to offers-api, which returns 200 {credentialOffer} |
offers-api signs the 302 session handle (/internal/consent-location) because PAZ can't sign HS256 itself. The policy only fetches it when the 302 rule fires.
Changing the policy
# a Policy Editor in demo mode (e.g. the local pbd-pap on :47443), and the stub where it can reach it
node tests/stub-upstreams.mjs &
./build-policy.py --export policy # rebuilds; runs 9 decision tests; exports only if all pass
docker build -f deploy/gateway.Dockerfile -t cba-offers-gateway .The branch is seeded from policy/cba-offers.snapshot. The first build used --seed idp-pingauthorize/conformance/policy/authzen-certification.snapshot.
Run and test locally
node tests/stub-upstreams.mjs & # :7491 fake pod-runtime + JWKS + MATTR
(cd ../offers && PORT=7400 OFFERS_LOCAL_PDP=false CBA_JWKS_URL=http://localhost:7491/jwks.json \
POD_RUNTIME_URL=http://localhost:7491 INTERNAL_API_KEY=stub-internal-key \
OFFERS_SESSION_SECRET=«redacted»
MATTR_TENANT_URL=http://localhost:7491 MATTR_AUTH_URL=http://localhost:7491 MATTR_CLIENT_ID=x MATTR_CLIENT_SECRET=y \
npx tsx src/server.ts &)
set -a; . ~/.pingidentity/config; set +a
docker run -d --name cba-offers-gw -e PING_IDENTITY_DEVOPS_USER -e PING_IDENTITY_DEVOPS_KEY \
-e OFFERS_API_URL=http://host.docker.internal:7400 -e POD_RUNTIME_URL=http://host.docker.internal:7491 \
-e INTERNAL_API_KEY=stub-internal-key -e CBA_JWKS_BASE_URL=http://host.docker.internal:7491 \
-e CBA_JWKS_PATH=/jwks.json -p 127.0.0.1:7401:1080 cba-offers-gateway
python3 -m unittest discover -s tests -p 'test_gateway.py' -v # 7 end-to-end testsRailway
Deployed to project cba-hackathon, environment production, service offers-gateway:
- URL: https://offers-gateway-production.up.railway.app (Railway domain, target port 1080). The iOS app's
RAILWAY_OFFERS_GATEWAY_URLpoints here. - Build: the service variable
RAILWAY_DOCKERFILE_PATH=deploy/gateway.Dockerfile. Railway ignoredrailway.jsonon a--path-as-rootupload and fell back to Railpack until it was set. - Variables:
OFFERS_API_URL=http://offers-api.railway.internal:7400POD_RUNTIME_URL=http://pod-runtime.railway.internal:7500CBA_JWKS_BASE_URL=https://cba-pf-production.up.railway.appCBA_JWKS_PATH=/ext/cba/jwksINTERNAL_API_KEY, from the root.envPING_IDENTITY_DEVOPS_USER/_KEY, from~/.pingidentity/configPING_IDENTITY_ACCEPT_EULA=YES,PORT=1080
- Private networking works from PAZ to both upstreams. The trace log shows
PodStatus→pod-runtime.railway.internalandConsentLocation→offers-api.railway.internal, both 200. - Logs: the image tails PAZ's
traceandpolicy-decisionlogs to stdout. Decision logging is at its default level, and the internal key doesn't appear in them (checked after the smoke run). Don't turn on policy tracing.
# redeploy, from the repo root
railway up ./participants/cba-issuer/gateway --path-as-root --service offers-gateway --detach
# prove it with a real CBA PF log-on (cba-cust-001 / 000789): 401, 200, 302, and direct offers-api 403
participants/cba-issuer/gateway/scripts/smoke-railway.shoffers-api behind the gateway
offers-api runs with OFFERS_LOCAL_PDP=false (PAZ decides) and OFFERS_GATEWAY_ONLY=true. It's still publicly reachable, because MATTR has to call /claims/mattr. So /offers checks where the connection came from as well as verifying the CBA token itself. Railway's public edge connects from 100.64.0.0/10, and the gateway connects over the private network (10/8, fd00::/8). Anything not from a private address gets 403 {"error":"gateway_only"}. /claims/mattr (x-api-key), /internal/consent-location (internal key) and /health aren't affected.
I chose this over a shared header from PAZ. A header would need a modify-headers statement on the permit path and a new deployment package from a Policy Editor, and the socket address can't be forged through the edge. Even without the lock, the claim source refuses income without a ready pod, so going round the gateway could never issue an income credential.
To go back to running without PAZ, set OFFERS_LOCAL_PDP=true and OFFERS_GATEWAY_ONLY=false on offers-api.
Config browser
Read-only, from files tracked in git. Keys, keystores, .sec/, real env files and anything gitignored are left out; secret-looking values are shown as «redacted». 63 files.