Skip to main content

Open finance consent enforcement at the gateway: a how-to for UAE LFIs

A step-by-step guide to open finance consent enforcement at the API gateway for UAE LFIs: mTLS TPP profiles, consent checks per request, and audit-ready logs.

  • how-to
  • banking
  • workflows
  • compliance
Zerq team

Open finance consent enforcement works differently in the UAE than almost anywhere else. Under the Central Bank of the UAE's Open Finance Regulation (Circular No. 03/2025), consent is not something each bank captures and stores on its own edge. The framework provides an API Hub with a Trust Framework and Common Infrastructural Services, including a centralized Consent and Authorization Manager that supports the creation, management, enforcement and revocation of consents. Licensed financial institutions (LFIs) connect to that central platform instead of running bilateral integrations with every third-party provider.

Central management does not make enforcement someone else's problem. As a Data Holder you maintain a dedicated interface that gives framework participants access to accounts and products, and every data-sharing call that reaches it must be honored or refused based on the consent state at that moment. The security profile keeps access tokens short lived, but a refresh token can live as long as the consent it belongs to, and Article 22 lets a user withdraw consent at any time through a process no harder than granting it. A TPP can therefore present a technically valid credential for a consent the customer revoked minutes ago. The only place you can close that gap is the request path of your account-data API.

This post builds the complete enforcement flow in Zerq: a dedicated client and mTLS profile for TPP traffic, then a workflow in front of the account-data proxy that validates the request shape, checks consent status, returns a structured 403 when consent is missing, expired or revoked, and forwards when it is valid. It ends with the request-log evidence a compliance officer pulls afterwards and a short go-live checklist. If your account APIs are not behind a gateway collection yet, start with the banking and open banking use case and come back.

Why existing consent approaches fail under a centralized model

Most gateway consent tooling was designed for the UK and EU pattern: bilateral APIs between each bank and each TPP, consent captured at the bank's edge, onboarding handled bank by bank. Kong and Apigee both express consent logic through custom plugins or policy bundles that assume the gateway is where consent gets created and stored. The UAE model inverts that. Consent creation, user authentication and revocation run through the central framework, and your gateway's job narrows to enforcement: a per-request status decision against consent state your institution holds or queries. A consent-capture flow bolted onto your edge duplicates machinery the framework already owns and drifts out of sync with it.

The second failure mode is where the decision lives. Teams enforce consent in backend middleware or in an AWS Lambda authorizer in front of API Gateway, and both work until an auditor asks for evidence. A backend check leaves nothing in the gateway log: your API log shows a 200 or a 403 with no consent context, and the reasoning is buried in application logs on another system. Lambda authorizers cache decisions for their configured TTL, which is precisely the window in which a revoked consent keeps returning data. MuleSoft can express the check in a policy, but the decision detail still lands outside the request record you would hand a supervisor.

What the centralized model actually demands from an LFI gateway is narrow and testable: on every call, resolve the consent, evaluate its status, refuse or forward, and record all of it in one place. That is a workflow, and it belongs in the gateway request path.

What the CBUAE framework expects your gateway to prove

Circular No. 03/2025 has been in force since July 2025, replacing the original regulation from 2023, and makes participation mandatory for CBUAE licensees in scope, with banks and insurers in the first onboarding phase. Data sharing and service initiation are in all cases subject to the user's express consent, appropriate authentication and secure communication. Article 22 requires consent to be specific to its purpose, informed, unambiguous and freely given, and users may withdraw it at any time. Article 13 obliges providers to keep records of user consents. The framework's security profile builds on FAPI 2.0 and mandates mutual TLS, with digital certificates issued through the Trust Framework.

Read as engineering requirements for the Data Holder side, CBUAE open finance asks four things of your gateway: machine identity for TPP-originated traffic, consent validation at the API gateway on every request rather than at token issuance, refusals that state their reason in a structured way, and per-request records that tie identity, consent and outcome together. Each of these maps onto a Zerq capability you configure rather than code you deploy.

Building the open finance consent enforcement workflow in Zerq

Here is how to enforce open finance consent at the API gateway, end to end.

Step 1: create a dedicated client and mTLS profile per TPP

Give every TPP integration its own identity, so that logs, rate limits and revocation act on one partner at a time.

  1. In the sidebar, open Clients and click New Client. Name it after the TPP, for example tpp-fintapp, and under Collections grant access to the account-data collection only. Optionally attach a rate limit Policy for per-TPP throttling.
  2. Open the new client and click Add Profile. Name it production, set Auth Type to mtls, set Allowed Methods to GET, and add IP Restrictions covering the ranges this traffic legitimately originates from.
  3. Copy the generated X-Client-ID and X-Profile-ID values for your ingress configuration.

With auth type mtls, Zerq takes identity from the TLS layer instead of an Authorization header. Your ingress validates the client certificate against your CA trust and sets the identity headers from the validated subject. The mapping needs one deliberate choice: either issue TPP client certificates whose CN and OU carry the Zerq client and profile IDs you copied in step 1.3, or configure an ingress map that translates each validated certificate subject into those ID values before forwarding. Zerq then enforces collection access, method and IP restrictions, and policy limits against that identity before any workflow runs: a valid certificate bound to the wrong profile is refused with a 403, and a blocked method with a 405. The header mapping is only safe behind trusted ingress where callers cannot inject those headers themselves; the security model covers this TPP mTLS pattern in detail.

Step 2: validate the request shape

  1. Go to Collections, open the account-data collection, click into the Proxy that fronts your accounts backend, and click Edit Workflow.
  2. A new workflow starts as http_trigger -> response_node. Click Add node and place a validate_node directly after the trigger.

Configure it to reject anything that does not carry a consent identifier and a well-formed account reference:

{
  "id": "validate_request",
  "type": "validate_node",
  "inputs": {
    "value": "{{ $json['http_trigger'].request }}"
  },
  "config": {
    "schema": {
      "type": "object",
      "required": ["headers", "path_params"],
      "properties": {
        "headers": {
          "type": "object",
          "required": ["x-consent-id"],
          "properties": {
            "x-consent-id": { "type": "string", "minLength": 8 }
          }
        },
        "path_params": {
          "type": "object",
          "required": ["accountId"],
          "properties": {
            "accountId": { "type": "string", "pattern": "^acc-[a-z0-9]+$" }
          }
        }
      }
    }
  }
}

The value to validate goes in inputs.value, not in config; the runtime reads inputs.value and validates it against the JSON Schema in config.schema. The node branches to valid when the schema passes and invalid when it fails. Wire invalid to a response_node returning a 400 with a body like {"error": "malformed_request"}, so the consent store never sees garbage and the TPP gets a spec-shaped error instead of a timeout.

Step 3: look up consent status

On the valid branch, add an http_request_node that asks your consent store for the current state of the consent. Depending on your integration, that is the LFI-side consent record you synchronize from the central platform, or an internal API in front of it.

{
  "id": "check_consent",
  "type": "http_request_node",
  "config": {
    "timeout_ms": 2000,
    "retry_config": { "max_attempts": 2, "backoff_ms": 200 }
  },
  "inputs": {
    "url": "https://consent.internal.example.ae/v1/consents/{{ $json['http_trigger'].request.headers['x-consent-id'] }}",
    "method": "GET",
    "headers": { "Accept": ["application/json"] }
  }
}

Transport settings live in config and the request itself lives in inputs; url is the one required input. The two-second timeout matters: without it, a slow consent store stalls your regulated data path. The node branches success and error, and the error branch must fail closed. Wire it to a response_node returning 503 with {"error": "consent_check_unavailable"}. Never route a failed consent lookup toward the backend.

Step 4: branch on consent status

Add a condition_node after the lookup. Conditions are plain JavaScript over $json (no template braces), evaluated in order, first match wins:

{
  "id": "consent_gate",
  "type": "condition_node",
  "config": {
    "conditions": [
      {
        "condition": "$json['check_consent'].response.status_code === 200 && $json['check_consent'].response.body.status === 'active' && new Date($json['check_consent'].response.body.expires_at) > new Date()",
        "output": "allowed"
      }
    ],
    "default_output": "denied"
  }
}

Note what is deliberately absent: there is no condition listing the deny cases. Only an explicit active status inside its validity window reaches the allowed output. Revoked, expired, suspended, a 404 for an unknown consent identifier, a malformed store response, all of it falls through to default_output and gets denied. Under a regime where users may withdraw consent at any time, and a provider may act only on explicit consent, fail-closed is the only defensible default for consent revocation enforcement.

Step 5: deny with a structured 403, forward when valid

On the denied edge, add a response_node:

{
  "id": "deny_403",
  "type": "response_node",
  "config": {
    "status": 403,
    "headers": { "Content-Type": "application/json" },
    "body": "{{ JSON.stringify({ error: 'consent_not_active', consent_id: $json['http_trigger'].request.headers['x-consent-id'], consent_status: $json['check_consent'].response.body.status || 'not_found', checked_at: new Date().toISOString() }) }}"
  }
}

The body names the consent, the status that caused the refusal, and when the check ran. The TPP can surface something meaningful to its user, and every one of those fields becomes searchable in your logs. On the allowed edge, add a proxy_node with the id forward_backend: it forwards the in-flight request through the proxy's configured upstream mapping, so the workflow carries no hardcoded backend URL. Close the graph with a response_node that returns {{ $json['forward_backend'].response.status_code }} as the status and the upstream body as the body, and wire the proxy node's error output to a 502 response so upstream failures are explicit too.

Step 6: validate, enable, and test both paths

  1. Click Validate. It checks for exactly one entry trigger, missing required fields, invalid expression syntax, disconnected nodes and circular dependencies.
  2. Run Execute Workflow with sample data, or use the http_trigger Listen for Request test mode to capture a real call from your test client. Confirm one allowed path and one denied path.
  3. Click Save, then toggle Enable Workflow. Save and Enable are independent: a saved workflow does nothing in production until it is enabled and the proxy revision is published. While disabled, the proxy falls back to plain pass-through forwarding, which in this context means unenforced consent, so verifying the enabled state belongs on your go-live checklist.

What the request log records for allowed and denied calls

Zerq logs every request with client identity, latency, headers and both bodies. Because the deny decision executes inside the gateway, an allowed and a denied call land in the same log with the same fields. An allowed call looks like this:

{
  "request_id": "7c9e4b2a-51d3-4f8e-9a06-2b7d1c3e8f45",
  "timestamp": "2026-09-21T09:14:07Z",
  "method": "GET",
  "path": "/accounts/acc-8c04/transactions",
  "target_endpoint": "https://accounts.core.internal/v1/accounts/acc-8c04/transactions",
  "status_code": 200,
  "latency_ms": 131,
  "client_id": "b1f6c3d0-8a2e-4f7b-9c11-5e2d4a6f8b30",
  "profile_id": "e4a9d2c7-6b13-48f0-a5c2-9d7e1f3b6a84",
  "collection": "account-information",
  "client_ip": "10.40.12.7",
  "request_headers": { "x-consent-id": "cns-4f19ab27" }
}

And a denied call from the same TPP, after the customer withdrew consent:

{
  "request_id": "d3b81f6e-2c47-49a0-b8e5-7f1a9c04d2e6",
  "timestamp": "2026-09-21T09:31:44Z",
  "method": "GET",
  "path": "/accounts/acc-8c04/transactions",
  "target_endpoint": null,
  "status_code": 403,
  "latency_ms": 38,
  "client_id": "b1f6c3d0-8a2e-4f7b-9c11-5e2d4a6f8b30",
  "profile_id": "e4a9d2c7-6b13-48f0-a5c2-9d7e1f3b6a84",
  "collection": "account-information",
  "client_ip": "10.40.12.7",
  "response_body": {
    "error": "consent_not_active",
    "consent_id": "cns-4f19ab27",
    "consent_status": "revoked",
    "checked_at": "2026-09-21T09:31:44Z"
  }
}

Two details carry the audit weight. The denied entry records no forwarded backend URL; the target endpoint field only ever holds the URL a request was forwarded to, so its absence shows the backend was never called after revocation. (The JSON snippets above are illustrative renderings of the documented log fields.) And the response body preserves the consent identifier and the status that caused the refusal, so a single log entry answers who asked, under which consent, what state that consent was in, and what the gateway did about it. The request ID is also returned to the caller in the X-Request-ID header, which gives you a correlation handle when a TPP raises a dispute through the central platform.

How a compliance officer filters for denials

  1. Open Logs in the sidebar, set Status code to 403, Path to /accounts/*, and the time preset to Last 7 days.
  2. Add a Payload filter for consent_not_active. Payload search runs across request and response bodies in one query, so it isolates consent denials from other 403s such as IP restriction hits.
  3. To review one partner, add that TPP's Client ID. To trace one dispute, filter by Request ID instead.
  4. Bookmark the result. Every active filter syncs into the browser URL, so the "consent denials, last 7 days" view becomes a link the compliance team opens each week without touching the filter panel.

Set request log retention in gateway settings to your regulatory retention period, and keep log access role-scoped. Configuration changes, including who edited this workflow and when, land separately in the audit log, which the observability surface exposes to a dedicated auditor role, so reviewers do not need admin rights.

Go-live checklist

  • Ingress validates TPP certificates against the trust anchors you accept and maps CN and OU to X-Client-ID and X-Profile-ID; identity headers cannot be injected from outside.
  • One client per TPP, collection access scoped to account data, an active mtls profile, GET-only methods, IP restrictions on.
  • Workflow saved, enabled, and the proxy revision published; confirmed the proxy is not running in pass-through fallback.
  • Fail-closed paths tested: consent store unreachable returns 503, unknown consent returns 403, revoked consent returns 403 on the next request after revocation.
  • One allowed and one denied request verified in the request log, with the consent identifier visible in both.
  • Log retention configured to your regulatory requirement, and the denial filter view bookmarked by the compliance team.
  • After every certificate rotation, one success test and one deny test before the change is closed.

What this looks like in practice

A retail bank preparing for phase-one onboarding had consent checks implemented as middleware inside its accounts service. Functionally it worked. Operationally it failed the first serious test: when a revocation scenario was exercised, proving the enforcement behavior meant joining gateway access logs with application logs from the accounts service, reconstructing which requests arrived after the withdrawal timestamp and what the middleware decided for each one. The exercise consumed the platform team for days, and the resulting evidence was a spreadsheet stitched from two systems, exactly the kind of artifact that invites follow-up questions.

After moving enforcement into a gateway workflow of the shape above, the same question became a filtered log view. Revocation now takes effect on the next request, because every request performs the lookup and there is no decision cache waiting to expire. When the compliance officer needs the denial record for a specific consent, she filters the request log by payload for the consent identifier and reads the structured 403s directly: backend untouched, reason stated, timestamp included. The accounts service shed its consent middleware entirely and went back to serving account data.

The UAE built the consent layer centrally so that LFIs would not each reinvent it. What remains with you, honoring consent state on every single data-sharing call, is a gateway problem, and treating it as one turns a regulatory obligation into a workflow you can draw, test, version and prove. Everything in this post is configuration: no plugin builds, no authorizer deployments, no backend release cycle between you and enforcement that holds up under review.


Zerq is an enterprise API gateway built for regulated industries — one platform for API management, AI agent access, compliance audit, and developer portal, running entirely in your own infrastructure. See how it works or request a demo to walk through your specific requirements.