Overview
Every Open Banking API call is authorised by a consent. Not a token, not a session — a consent. The consent is the legal and technical artefact that says: this customer, on this date, authorised this TPP to access these accounts for these purposes, until this date. Get that right and everything downstream is tractable. Get it wrong and you have an audit trail that satisfies nobody, a revocation mechanism that doesn’t propagate, and a TPP ecosystem that loses confidence in the platform.
The article covers the four hard parts: the consent data model, the CIBA flow that makes mobile-first consent workable, dynamic client registration for TPP onboarding, and the revocation mechanics that need to work under peak load on a SAMA holiday.
The canonical mistake is storing consent as a JWT claim and trusting it for the lifetime of the token. Consent is a living entity — it can be partially revoked (remove one account from scope), suspended by the bank (fraud hold), or expired (reached the agreed end date). Token validation must check a live consent store on every call to the resource API, not just on token issuance. This is the design decision that separates a real consent platform from an OAuth implementation with a consent screen bolted on.
Consent data model
A consent record needs to carry enough information to reconstruct the authorisation decision at any future point — for audit, for dispute, and for real-time enforcement. The minimal production-grade model:
{
"consentId": "csnt-0e4f1a2b-3c5d",
"status": "AUTHORISED",
"statusUpdatedAt": "2026-05-17T09:14:00Z",
"createdAt": "2026-05-17T09:10:00Z",
"expiresAt": "2027-05-17T00:00:00Z",
"tppId": "tpp-acme-finance",
"customerId": "cust-redacted-hash", // hashed; PII stays in core
"permissions": [
"ReadAccountsBasic",
"ReadAccountsDetail",
"ReadBalances",
"ReadTransactionsCredits",
"ReadTransactionsDebits"
],
"accounts": [
{ "accountId": "acc-001", "currency": "SAR" },
{ "accountId": "acc-002", "currency": "USD" }
],
"transactionFromDate": "2026-01-01",
"transactionToDate": "2026-12-31",
"authorisationId": "auth-7f8e9d0a", // links to SCA event
"accessToken": { "expiresAt": "2026-05-17T09:44:00Z" },
"refreshToken": { "expiresAt": "2026-06-17T09:14:00Z" }
}
Consent status is an enum. Transitions are one-way except where the framework allows reauthorisation:
| Status | Meaning | Who can set it | Reversible? |
|---|---|---|---|
AWAITING_AUTHORISATION | Created by TPP, not yet confirmed by customer | Bank (on DCR or consent creation) | Yes → AUTHORISED |
AUTHORISED | Customer confirmed via SCA | Bank (post-SCA) | Yes → REVOKED or EXPIRED |
REVOKED | Cancelled by customer or bank | Customer (portal), Bank (fraud), TPP (DELETE endpoint) | No |
EXPIRED | Past expiresAt or token TTL | System (scheduler) | No |
REJECTED | Customer declined at SCA screen | Bank (post-SCA decline) | No |
SUSPENDED | Temporarily blocked (fraud hold, sanction check) | Bank only | Yes → AUTHORISED |
CIBA flow
CIBA (Client-Initiated Backchannel Authentication, RFC draft) decouples the authentication device from the session initiating the request. In an Open Banking context: the TPP’s web app initiates the consent request, the bank sends a push notification to the customer’s mobile banking app, the customer confirms on their phone, and the TPP’s server polls or receives a callback. No redirect. No browser pop-up. This is the pattern that the SAMA framework strongly favours for Phase 2 payment initiation because it keeps the authentication on a trusted, bank-enrolled device.
The key property: the TPP never handles the authentication. Steps 3–5 happen entirely on the customer’s bank-enrolled device. The auth_req_id is a short-lived (120s) handle that ties the polling at step 6 to the SCA event at step 5.
The auth_req_id must be cryptographically bound to the consent payload submitted at step 1. A naive implementation generates the id independently and looks up the consent at token issuance — leaving a window where a rogue auth_req_id can be substituted. Embed the consent hash in the auth_req_id, verify on token issuance.
Dynamic client registration
TPPs cannot be pre-registered by email. The volumes are wrong and the automation requirement is absolute. Dynamic Client Registration (RFC 7591, OIDC DCR) is the mechanism: the TPP presents a software statement assertion (SSA) — a JWT signed by the Open Banking directory — and the bank’s AS registers the TPP client and returns a client_id.
In SAMA’s framework, the SSA is issued by the Saudi Open Banking (SAOB) platform. A bank that runs its own DCR endpoint must validate the SSA signature against the SAOB JWKS before creating the client record.
// POST /register — DCR request body
{
"software_statement": "eyJhbGciOiJQUzI1NiIsImtpZCI6InNhb2ItMjAyNiJ9...",
"redirect_uris": ["https://app.acme-finance.sa/callback"],
"token_endpoint_auth_method": "private_key_jwt",
"grant_types": ["authorization_code", "urn:openid:ciba", "refresh_token"],
"response_types": ["code"],
"request_object_signing_alg": "PS256",
"backchannel_token_delivery_mode": "poll", // or "ping" / "push"
"scope": "openid accounts payments",
"tls_client_auth_subject_dn": "CN=acme-finance.sa,O=ACME Finance,C=SA"
}
// 201 response from AS
{
"client_id": "tpp-acme-finance",
"client_id_issued_at": 1747476600,
"registration_access_token": "rat-...",
"registration_client_uri": "https://as.saib.com.sa/register/tpp-acme-finance"
}
The registration_access_token (RAT) is the only credential the TPP uses to update or delete its own registration. Treat it like a long-lived client secret: store it encrypted, rotate it annually, revoke it immediately on suspected compromise.
If a TPP submits the same SSA twice (retried registration), the AS must return the existing client_id, not create a duplicate. The software_id claim inside the SSA is the deduplication key. Index the client table on software_id, not on the outer client object.
Consent creation
Consent creation is a two-step process. The TPP creates the consent resource (a POST /consents call that returns a consentId in AWAITING_AUTHORISATION status), then the customer authorises it via CIBA or redirect. Both steps are FAPI-secured: PAR + JAR on the authorization request, private_key_jwt on the token request.
# Step 1: TPP creates consent resource (server-to-server, not user-facing)
curl -sS https://api.saib.com.sa/open-banking/v3/aisp/consents \
--cert $TPP_CERT --key $TPP_KEY \ # mTLS: client auth + token binding
-H "Authorization: Bearer $CCG_TOKEN" \
-H "Content-Type: application/json" \
-H "x-fapi-interaction-id: $(uuidgen)" \
-d '{
"Data": {
"Permissions": ["ReadAccountsBasic","ReadBalances","ReadTransactionsCredits"],
"ExpirationDateTime": "2027-05-17T00:00:00Z",
"TransactionFromDateTime": "2026-01-01T00:00:00Z",
"TransactionToDateTime": "2026-12-31T23:59:59Z"
},
"Risk": {}
}''
# Returns: { "Data": { "ConsentId": "csnt-0e4f1a2b", "Status": "AwaitingAuthorisation" } }
# Step 2: TPP submits PAR to get request_uri, then initiates CIBA with login_hint + consent id
curl -sS https://as.saib.com.sa/par \
-d "response_type=code" \
-d "client_id=$CLIENT_ID" \
-d "scope=openid accounts" \
-d "claims=$(jq -c . claims.json)" \ # includes consentId in id_token claims
-d "request=$(sign_jar_jwt)" # JAR: request object signed PS256
Strong customer authentication
SAMA mandates SCA for all consent authorisations and for each payment initiation regardless of value. SCA requires two of: something you know (PIN), something you have (OTP device / app), something you are (biometric). For the mobile banking app flow, biometric + push notification satisfies the “have + are” combination without a PIN step — which is the UX the market expects.
The SCA event must be cryptographically linked to the consent. The approach:
- Consent hash in push payload. The FCM/APNs push notification includes a signed hash of the consent payload (permissions, accounts, dates). The app displays this to the customer; they are authenticating a specific consent object, not just a generic approval screen.
- Device binding check. The AS verifies the push was delivered to a device bound to the customer’s profile. A new device that has not completed device registration cannot receive open banking consent pushes — this is separate from the bank login device.
- Biometric attestation. On iOS/Android, the biometric challenge produces a platform-signed assertion (FIDO2 / WebAuthn authenticator assertion). The app forwards this to the AS. The AS verifies the assertion against the registered public key for that device.
- Consent status transition is atomic. The status update from AWAITING_AUTHORISATION to AUTHORISED is a single database transaction that also writes the authorisation_id (linking to the SCA event record). No intermediate states, no dual writes.
- auth_req_id expires independently. If the customer does not respond within the auth_req_id TTL (typically 120–300s), the consent status moves to REJECTED and the TPP’s poll returns
expired_tokenat the token endpoint. - Re-authorisation is a new consent. A rejected consent cannot be approved. The TPP must create a new consent resource and start the CIBA flow again. This is intentional: it closes the window where a delayed approval could be intercepted and replayed.
Revocation mechanics
Revocation is the part that most implementations get wrong. The consent is revoked — but are the tokens revoked? Is the customer’s dashboard updated? Is the TPP notified? There are three revocation paths, each with different propagation requirements.
| Initiator | Mechanism | Token effect | Propagation |
|---|---|---|---|
| Customer (bank portal / mobile) | PATCH or DELETE on consent resource via bank UI | All tokens under that consentId revoked immediately | TPP notified via event notification (webhook or polling the consent status endpoint) |
| TPP | DELETE /consents/{consentId} | TPP’s own tokens revoked; customer notified in bank portal | Bank marks consent REVOKED; no further calls accepted under that id |
| Bank (fraud/compliance) | Internal admin API; can revoke consent or suspend client | All tokens revoked; client registration can be suspended | TPP receives 403 on next call with specific error code indicating revocation |
| System (expiry) | Scheduled job runs every hour; marks consents past expiresAt as EXPIRED | Refresh tokens revoked; access tokens expiry enforced at resource API | No active notification; TPP discovers on next token refresh (401) |
If your resource API validates tokens by checking only the JWT signature and expiry claim (stateless validation), a revoked consent does nothing until the access token naturally expires — potentially 15 minutes after the customer clicked “remove access”. Under SAMA’s framework, this is a breach of the consent terms. The resource server must call the consent store on every request, or subscribe to consent revocation events and maintain a local revocation cache with <1s staleness.
TPP onboarding steps
TPP onboarding is an operational process that wraps the DCR protocol. In a production KSA deployment it looks like this:
- SAOB directory enrolment. The TPP registers with the Saudi Open Banking platform and receives a signed SSA. SAOB verifies the TPP’s licence, legal entity, and regulatory standing. The bank does not repeat this check — it trusts the SSA signature.
- Sandbox DCR and E2E test. The TPP calls the bank’s sandbox DCR endpoint with the SSA. The AS creates a sandbox client. The TPP runs the full consent + data retrieval flow against synthetic accounts. Sandbox must mirror production FAPI profile exactly.
- Production DCR. After sandbox sign-off, the TPP calls the production DCR endpoint. The AS validates the production SSA (different signing key from sandbox), creates the production client, and returns the client_id. No human approval step — the SAOB-signed SSA is the trust anchor.
- mTLS certificate exchange. The TPP’s transport certificate DN is registered against the client_id. The bank’s API gateway enforces mTLS on all TPP calls and maps the cert DN to the client_id for rate limiting and audit. A cert rotation by the TPP requires a DCR PATCH call, not an email.
- Consent notification endpoint registration. The TPP registers a webhook URL for consent event notifications (AUTHORISED, REVOKED, EXPIRED). The bank sends signed event notifications (JWS) to this URL within 5 minutes of a status change. The TPP webhook must accept and return 202 within 3 seconds — long processing goes async.
Token-consent binding
Every access token issued under a consent grant must carry the consentId as a claim. The resource API must verify the consentId claim on every call and check the live consent status. This is the binding that makes revocation work.
public class ConsentAwareResourceFilter implements ContainerRequestFilter {
private final ConsentStore consentStore;
private final JwtValidator jwtValidator;
@Override
public void filter(ContainerRequestContext ctx) {
String rawToken = extractBearer(ctx);
JwtClaimsSet claims = jwtValidator.validate(rawToken);
String consentId = claims.getStringClaim("consent_id");
if (consentId == null) {
throw new WebApplicationException(Response.Status.FORBIDDEN);
}
// Live consent check — cached with 5s TTL, refreshed on MISS
Consent consent = consentStore.findById(consentId)
.orElseThrow(() -> new WebApplicationException(Response.Status.FORBIDDEN));
if (consent.getStatus() != ConsentStatus.AUTHORISED) {
ctx.abortWith(
Response.status(403)
.entity("{\"error\":\"consent_revoked\",\"consentId\":\"" + consentId + "\"}")
.build()
);
return;
}
// Enforce account scope: only accounts listed in consent are accessible
String requestedAccount = extractAccountId(ctx);
if (requestedAccount != null
&& !consent.getAccounts().contains(requestedAccount)) {
ctx.abortWith(Response.status(403)
.entity("{\"error\":\"account_not_in_consent\"}").build());
return;
}
ctx.setProperty("consent", consent);
}
}
The cache TTL of 5 seconds is the trade-off between latency (avoiding a consent store roundtrip on every call) and revocation propagation. For payment initiation, use 0-second TTL (always live) — the additional latency on write-path APIs is acceptable; delayed revocation is not.
SAMA-specific requirements
The SAMA Open Banking framework adds requirements beyond the base FAPI 2.0 profile. The ones that change architecture decisions:
- Consent duration cap. SAMA Phase 1 allows consents up to 12 months for AIS. Phase 2 payment initiation consents for recurring payments have a separate duration regime defined per payment type. Do not hard-code 12 months into your consent model; parameterise by permission set and phase.
- Hijri date support. The SAMA framework requires that date fields in consent display (customer-facing UI) support Hijri calendar representation. The API uses Gregorian ISO 8601 internally; conversion happens in the display layer, not in the consent record.
- Arabic language consent screen. The SCA confirmation screen must be in Arabic for Arabic-speaking customers and must accurately represent the permission set in plain language. Regulators review the Arabic text, not the English technical specification. The mapping from
ReadTransactionsCreditsto customer-readable Arabic is a product decision with legal sign-off. - Audit retention: 10 years. SAMA requires consent lifecycle events to be retained for 10 years. Event sourcing the consent store is the right architecture: the current state is a projection, and the event log is the durable audit trail. A mutable status column in a relational table is not sufficient.
- SAOB event notification SLA: 5 minutes. Status change events to the central SAOB platform must be delivered within 5 minutes. This is a push, not a pull. Your consent service must publish events to the SAOB notification endpoint as part of the status transition, not in a downstream batch.
Every API call in the SAMA Open Banking framework must carry an x-fapi-interaction-id header with a UUID. The bank must echo it back in the response and log it against every operation. SAMA audits use this header to reconstruct call chains for dispute investigation. An implementation that drops or rewrites this header in the API gateway will fail the SAMA audit review. Log it end-to-end: gateway, consent service, resource API, core system adapter.
Common pitfalls
Described above under revocation — but it comes up consistently enough to name explicitly. Every team that has shipped an Open Banking Phase 1 implementation has an honest conversation at some point about whether they validate live consent status at the resource server or only at token issuance. The teams that chose the latter are having that conversation again now as they prepare for Phase 2. Fix it before Phase 2 is in production, not after.
A refresh token that references a revoked consent must return an error on the next refresh attempt, not issue a new access token. If the refresh token is stored without a consentId reference, the revocation path cannot propagate to it. The refresh token must carry or reference the consentId so the token endpoint can verify live consent status before issuing a new access token.
A DCR endpoint that does not rate-limit by IP and by SSA software_id will be abused. A credential stuffing attack against the DCR endpoint wastes compute and floods your client store. Rate limit: 10 DCR calls per software_id per hour, 50 per IP per hour. Responses to rate-limited requests must use HTTP 429, not 400 — some TPP SDK retry logic only backs off on 429.
Production checklist
- Consent store uses event sourcing; status column is a projection, not the source of truth.
- auth_req_id is cryptographically bound to the consent payload hash at creation and verified at token issuance.
- Resource API performs live consent status check on every call (cache TTL ≤5s for AIS, 0s for PIS).
- Revocation propagates tokens synchronously: revoking a consent immediately invalidates the refresh token at the token endpoint.
- DCR validates SSA signature against SAOB JWKS before creating client record.
- DCR is idempotent on
software_id: repeated registration returns existing client_id. x-fapi-interaction-idlogged end-to-end: gateway, consent service, resource API.- Consent event notifications to SAOB are delivered within 5 minutes of status change.
- DCR endpoint rate-limited by software_id and IP.
- SCA confirmation screen is in Arabic, legally reviewed, and maps permissions to plain-language descriptions.
- Consent duration enforced by permission type, not hard-coded to 12 months.
- Audit events retained for 10 years; tested restore path exists.