// cat ./projects/openbanking-load-testing.md

DELIVERED · 2026

Open Banking Auth Flow Load Testing

k6, mTLS, FAPI/OAuth2 with PAR, browser-driven SCA

A load-testing harness driving a complete FAPI-compliant open banking authorization flow — client credentials, consent creation, pushed authorization requests, and strong customer authentication — against a regulated sandbox over mutual TLS.

k6mTLSOAuth2FAPIPARTLSLoad TestingHeadless Chromium

## Problem

The authorization flow for a regulated open banking API is six chained steps across three hosts, every one requiring mutual TLS, ending in an interactive consent screen. Load-testing it means reproducing the entire sequence programmatically, including the part explicitly designed to require a human.

## Approach

The harness performs the client credentials grant, creates an account-access consent, builds the pushed authorization request object locally, submits it, drives strong customer authentication in a real headless browser, and exchanges the resulting code for a user-level access token. Building the request object locally was possible because the sandbox advertises `none` as a supported signing algorithm — saving a mutual-TLS round trip per iteration against the server's signing helper.

## The bug that defined the project

Every request failed with an invalid-client error stating no mTLS session was established, while curl and openssl s_client succeeded with the same certificate. The cause is in Go's TLS stack, which k6 is built on: it silently declines to send a client certificate whose issuer does not appear in the server's advertised AcceptableCAs list. Not an error, not a warning — no certificate at all. curl does not apply that filter, which is exactly why the manual check kept passing and misdirected the investigation toward credentials.

## The fix

The certificate chain was padded by extracting the sandbox's own CA from its TLS handshake and appending it to the leaf, so the issuer matched what the server advertised. The certificate also had to be bound explicitly to all three hostnames — its subject was a bare UUID with no subject alternative names, leaving nothing for the client to match on automatically.

## What only appeared under live execution

Two values carried over from the reference collection were inert until the flow actually ran: a consent expiry hardcoded to a date that had since passed, which the server rejected outright, and a placeholder value in the authorization request that the server refused by name. Strong customer authentication also turned out to be two screens rather than one — a login form followed by a separate account-selection and consent step. None of this is visible by reading the specification.

## Outcome

The full flow passes 7 of 7 checks, and 28 of 28 across a concurrent run, obtaining real user-level access tokens end to end. Certificates, credentials, and host configuration are supplied through environment variables and kept out of version control.