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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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) :

json
{
  "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

python
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):

text
GET https://directory.openbanking.org.uk/api/v1/organisations

Response (truncated) :

json
{
  "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:

python
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).

text
+-----------------------------------------------------------------------+
|         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):

json
{
  "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:

python
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

python
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.

Â