INTRODUCTION
In Lessons 2.1 through 2.6, you built a perfect API—restful, contract-validated, idempotent, paginated, and error-resilient. But you have buried it behind a URL that the TPP must know. In a multi-bank environment (a TPP integrating with 50+ ASPSPs), hardcoding https://api.bank-a.com/open-banking/v4 is operationally brittle. When Bank A moves its API gateway to a new domain, every TPP’s application breaks.
This is why open banking mandates dynamic service discovery. The OBIE Directory Service, the CDR Register, and the OpenID Connect Discovery endpoint (.well-known/openid-configuration) provide a standardised mechanism for TPPs to discover the ASPSP’s base URLs, authorisation endpoints, token endpoints, and public JWKS (JSON Web Key Set) for JWT verification.
This lesson teaches you to implement the discovery stack. You will construct the /.well-known/openid-configuration JSON payload, understand the OBIE Directory API (which lists all participating banks and their endpoints), and implement dynamic client registration (where TPPs register their software statements with the ASPSP on-the-fly). We will quantify the latency impact of discovery (the TPP caches the directory response for 24 hours, so it adds 0ms to the critical path) and the security implications (the JWKS endpoint must be aggressively cached to prevent DoS attacks).
LEARNING OBJECTIVES
-
Construct the OpenID Connect Discovery endpoint (
/.well-known/openid-configuration)—returning theÂauthorization_endpoint,Âtoken_endpoint,Âjwks_uri, and the mandatoryÂscopes_supported andÂresponse_types_supported fields required by FAPI 1.0 Advanced. -
Design the OBIE Directory API integration—implementing a client that fetches the directory listing (includingÂ
FinancialId,ÂApiBaseUrl, andÂRegistrationEndpoint) and caches it with a TTL of 24 hours, with a fallback mechanism to a static configuration file if the directory is unreachable. -
Implement Dynamic Client Registration (RFC 7591)—receiving a TPP’s software statement (JWT signed by the OBIE directory), validating it, and returning aÂ
client_id andÂclient_secret (orÂclient_assertion method) with a scoped set of permissions. -
Calculate the caching strategy for JWKS—measuring the latency of fetching the JWKS endpoint (50-200ms for the first fetch, but 0ms for cached requests with a TTL of 12 hours) and designing a background refresh thread to keep the public keys warm.
-
Quantify the discovery overhead—computing the total latency added to the TPP’s first-time integration (directory lookup + JWKS fetch + registration = ~2 seconds) versus the critical path (token and API calls = 0ms overhead thanks to caching).
PART 1: OPENID CONNECT DISCOVERY — The Well-Known Endpoint
1.1 The Mandatory Discovery Endpoint
The FAPI 1.0 Advanced profile requires every ASPSP to expose an OpenID Connect Discovery endpoint at /.well-known/openid-configuration. This provides TPPs with all necessary endpoints in a single, machine‑parseable JSON object.
Explicit JSON Payload (OBIE v4.0 compliant)Â :
{ "issuer": "https://auth.bank.com", "authorization_endpoint": "https://auth.bank.com/authorize", "token_endpoint": "https://auth.bank.com/token", "jwks_uri": "https://auth.bank.com/jwks", "registration_endpoint": "https://auth.bank.com/register", "scopes_supported": [ "openid", "accounts:read", "payments:write", "consents:manage" ], "response_types_supported": [ "code", "code id_token" ], "grant_types_supported": [ "authorization_code", "refresh_token", "client_credentials" ], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["RS256", "PS256"], "request_object_signing_alg_values_supported": ["RS256", "PS256"], "token_endpoint_auth_methods_supported": [ "client_secret_basic", "private_key_jwt" ], "tls_client_certificate_bound_access_tokens": true, "dpop_signing_alg_values_supported": ["RS256", "PS256"], "authorization_signing_alg_values_supported": ["RS256", "PS256"] }
Regulatory anchor: The OBIE v4.0 specification requires that the issuer value must match the x-fapi-financial-id header. The TPP validates this to prevent DNS spoofing.
1.2 Serving the Discovery Endpoint in Production
from flask import Flask, jsonify, request import json import os app = Flask(__name__) @app.route('/.well-known/openid-configuration') def well_known(): # Serve from a static file for performance (no database calls) config = { "issuer": os.environ.get('ISSUER_URL', 'https://auth.bank.com'), "authorization_endpoint": os.environ.get('AUTH_ENDPOINT', 'https://auth.bank.com/authorize'), "token_endpoint": os.environ.get('TOKEN_ENDPOINT', 'https://auth.bank.com/token'), "jwks_uri": os.environ.get('JWKS_URI', 'https://auth.bank.com/jwks'), "registration_endpoint": os.environ.get('REG_ENDPOINT', 'https://auth.bank.com/register'), "scopes_supported": ["openid", "accounts:read", "payments:write"], "response_types_supported": ["code", "code id_token"], # ... rest of the fields } return jsonify(config)
Latency overhead: The endpoint is static JSON. Serving it adds ≤5ms (including network transmission). The TPP caches this response indefinitely (or at least for 24 hours), so the critical path remains untouched.
PART 2: THE OBIE DIRECTORY SERVICE — The UK’s Centralised Registry
2.1 What Is the OBIE Directory?
The OBIE Directory Service is a central registry that lists all authorised ASPSPs and TPPs in the UK ecosystem. It provides:
-
Financial Institution IDs (used in theÂ
x-fapi-financial-id header). -
API Base URLs (the root URL for each ASPSP’s open banking endpoints).
-
Registration Endpoint (where TPPs can register their software statements).
-
Public Certificates (for mTLS and JWT verification).
Directory API Endpoint (OBIE v4.0):
GET https://directory.openbanking.org.uk/api/v1/organisations
Response (truncated)Â :
{ "organisations": [ { "organisationId": "org-12345", "organisationName": "Bank A", "financialIds": [ { "financialId": "OBIE-UK-123456", "apiBaseUrl": "https://api.banka.com/open-banking/v4", "registrationEndpoint": "https://auth.banka.com/register" } ] } ] }
2.2 Integrating the Directory API — TPP‑Side Caching
TPPs must fetch the directory on startup and cache it for 24 hours (per OBIE guidance). If the directory is unreachable, the TPP must fall back to a static configuration file (embedded in the deployment) to ensure high availability.
Python Implementation:
import requests import json import time import threading class OBIEDirectoryClient: DIRECTORY_URL = "https://directory.openbanking.org.uk/api/v1/organisations" CACHE_TTL = 86400 # 24 hours def __init__(self, fallback_file='directory_fallback.json'): self.cache = None self.last_fetch = 0 self.fallback_file = fallback_file self.lock = threading.Lock() def get_organisation(self, financial_id): with self.lock: # Check if cache is stale now = time.time() if self.cache is None or (now - self.last_fetch) > self.CACHE_TTL: try: response = requests.get(self.DIRECTORY_URL, timeout=5.0) if response.status_code == 200: self.cache = response.json() self.last_fetch = now else: # Fallback to static file with open(self.fallback_file, 'r') as f: self.cache = json.load(f) self.last_fetch = now except requests.exceptions.Timeout: # Fallback to static file with open(self.fallback_file, 'r') as f: self.cache = json.load(f) self.last_fetch = now # Find the organisation by financial ID for org in self.cache.get('organisations', []): for fid in org.get('financialIds', []): if fid.get('financialId') == financial_id: return fid return None
Latency impact: The directory fetch (first time) adds ~200ms (p95). Subsequent requests are served from memory (0ms). The background thread can refresh the cache every 12 hours to keep it warm.
PART 3: DYNAMIC CLIENT REGISTRATION — RFC 7591
3.1 The Registration Flow
In open banking, TPPs do not pre‑register with each ASPSP individually. Instead, they present a Software Statement—a JWT signed by the OBIE Directory (or another trusted authority) that attests to their identity and permissions. The ASPSP validates the signature and returns a client_id and client_secret (or issues a client_assertion method).
+-----------------------------------------------------------------------+
| DYNAMIC CLIENT REGISTRATION — RFC 7591 FLOW |
+-----------------------------------------------------------------------+
| |
| TPP (Client) ASPSP (Registration Endpoint) |
| | | |
| |--(1) POST /register--------->| |
| | Headers: Content-Type: | |
| | application/jwt | |
| | Body: Software Statement | |
| | (JWT signed by OBIE) | |
| | | |
| | |--(2) Validate JWT signature---->|
| | | (via OBIE JWKS) |
| | | |
| | |--(3) Validate TPP permissions-->|
| | | (scopes, redirect URIs) |
| | | |
| | |--(4) Store registration-------->|
| | | (DB: client_id, secret) |
| | | |
| |<-(5) 201 Created--------------| |
| | Body: { | |
| | "client_id": "tpp-789", | |
| | "client_secret": "secret",| |
| | "client_id_issued_at": | |
| | 1691234567 | |
| | } | |
| | | |
+-----------------------------------------------------------------------+
3.2 Software Statement Validation
The Software Statement JWT contains the following claims (OBIE v4.0):
{ "alg": "RS256" } { "iss": "https://directory.openbanking.org.uk", "sub": "tpp-12345", "aud": "https://auth.bank.com/register", "iat": 1691234567, "exp": 1691238167, "software_id": "sw-001", "software_roles": ["PISP", "AISP"], "redirect_uris": ["https://tpp.com/callback"], "scope": "accounts:read payments:write" }
Validation logic:
import jwt import requests def validate_software_statement(jwt_token): # 1. Fetch OBIE directory JWKS jwks_response = requests.get('https://directory.openbanking.org.uk/jwks', timeout=5.0) jwks = jwks_response.json() # 2. Decode and verify the JWT try: claims = jwt.decode( jwt_token, jwks, algorithms=['RS256'], audience='https://auth.bank.com/register', issuer='https://directory.openbanking.org.uk', options={'require': ['iss', 'sub', 'software_roles', 'redirect_uris']} ) except jwt.InvalidTokenError as e: raise ValidationError(f"Invalid software statement: {e}") # 3. Verify that the TPP has the required roles if 'PISP' not in claims.get('software_roles', []): raise ValidationError("TPP is not registered as a PISP") return claims
Latency impact: JWKS fetch (first time) adds 50‑200ms. Subsequent validations use the cached JWKS (0ms). The registration endpoint itself adds ~100ms (database writes).
PART 4: CACHING STRATEGIES — Keeping Discovery Fast
4.1 Multi‑Level Caching Architecture
| Layer | Cache TTL | Update Strategy | Latency Impact |
|---|---|---|---|
| OBIE Directory | 24 hours | Background refresh every 12h | 0ms (in-memory) |
| JWKS (Public Keys) | 12 hours | Background refresh every 6h | 0ms (in-memory) |
| Discovery Endpoint | Indefinite (static) | No refresh needed | 0ms (static file) |
| Registration | N/A (per‑request) | Database lookup | ~100ms (once per TPP) |
| Consent/API | N/A | Per‑request | Critical path (covered in previous lessons) |
4.2 Background Refresh Implementation
import threading import time import requests class JWKSCache: def __init__(self, jwks_url, refresh_interval=21600): # 6 hours self.jwks_url = jwks_url self.refresh_interval = refresh_interval self.cache = None self.lock = threading.Lock() self._start_background_thread() def _start_background_thread(self): def refresh(): while True: try: response = requests.get(self.jwks_url, timeout=5.0) if response.status_code == 200: with self.lock: self.cache = response.json() except Exception: pass # Keep the old cache time.sleep(self.refresh_interval) thread = threading.Thread(target=refresh, daemon=True) thread.start() def get_jwks(self): with self.lock: return self.cache
CLOSING — OPERATIONAL RISK OF POOR DISCOVERY
If the ASPSP does not implement the discovery endpoints:
-
TPPs cannot find the correct authorisation endpoints → integration failures.
-
The OBIE Directory integration is broken → TPPs cannot register → the CMA issues a non‑compliance direction (Article 58).
-
If the JWKS endpoint is unavailable during a token validation, all authentication fails → denial of service.
Key takeaways:
-
/.well-known/openid-configuration is mandatory for FAPI compliance. -
The OBIE Directory provides centralised discovery of ASPSP endpoints.
-
Dynamic Client Registration (RFC 7591) eliminates manual TPP onboarding.
-
Caching (24h for directory, 12h for JWKS) reduces critical path latency to 0ms.
Transition to Lesson 2.8: You now have a fully discoverable, well‑known, and dynamically registered API. The final lesson of Module 2 is the capstone: Contract Testing, Mock Servers, and Compliance Validation—ensuring that your implementation stays aligned with the OpenAPI contract through the entire release lifecycle.