FAPI 2.0 Security Profile: OAuth 2.0 Hardening for Open Banking with PAR, DPoP and mTLS

FAPI 2.0 Security Profile: OAuth 2.0 Hardening for Open Banking with PAR, DPoP and mTLS

FAPI 2.0 Security Profile: OAuth 2.0 Hardening for Open Banking with PAR, DPoP and mTLS

OAuth 2.0 was designed to let a user delegate access without handing over a password, and it does that job well. It was not designed to guard a payment initiation endpoint against an attacker who can read a browser history, a proxy log or a misconfigured redirect. A plain bearer token is a cash note: whoever holds it can spend it. The financial-grade answer is not a new protocol but a tightly constrained profile of the existing ones, and that is what FAPI 2.0 is.

The OpenID Foundation published the FAPI 2.0 Security Profile and the companion Attacker Model as final specifications on 22 February 2025. The headline change from the first generation is subtraction: fewer options, no signed request objects required, no ID token tricks, and a short list of mandatory mechanisms (pushed authorization requests, PKCE, and sender-constrained access tokens) that together close the known holes in the authorization code flow.

This article walks through what the profile requires, why each requirement exists, and which attacks it blocks. It includes a runnable Python sketch of a pushed authorization request with a DPoP proof, and a decision matrix for choosing between DPoP and mutual TLS binding.

What this covers: the specification family and status, the mandatory flow, PAR, PKCE and issuer identification, sender-constrained tokens with DPoP and mTLS, client authentication, the attacker model, consent and grant management, regional adoption, failure modes, and a checklist.

Educational systems analysis only. This is not legal, compliance, security-certification or financial advice.

Context and Background

Open banking and open finance rest on a simple trade: a regulated third party (an account information service provider, a payment initiation service provider, or a data aggregator) gets scoped access to a customer’s bank data or payment rails, and the bank gets a standard way to authorize it. OAuth 2.0 (RFC 6749) and OpenID Connect became the default authorization layer because they separate the customer’s credentials from the third party’s access. What they did not do, in their base form, was constrain the way tokens and requests travel.

Base OAuth leaves a lot open. The authorization request travels through the browser as query parameters and can be altered or logged. An authorization code can be injected into a victim’s session. A bearer access token works for whoever presents it, so a token leaked from a log, a proxy or a compromised microservice is immediately usable. The IETF responded with a long series of extensions, and the OAuth 2.0 Security Best Current Practice document collects them. Those extensions are optional, which is exactly the problem for a regulator or an industry scheme that needs every participant to behave the same way.

The first FAPI generation, now called FAPI 1.0, solved this by writing a high-security profile. Part 1 (Baseline) and Part 2 (Advanced) pinned down algorithms, required signed request objects, and demanded sender-constrained tokens using mutual TLS. FAPI 1.0 Advanced was adopted widely by open banking ecosystems because it was one of the few places a regulator could point at a testable profile and a conformance suite. The OpenID Foundation now positions FAPI 2.0 as the successor.

Why a second generation at all? Three pressures. First, FAPI 1.0 allowed several paths (hybrid flow with ID tokens, request objects, JARM) that made implementations large and interoperability harder. Second, the formal analysis of the OAuth family, led by researchers at the University of Stuttgart and collaborators, gave the group a precise way to say which attacks are in scope and to prove the profile prevents them. Third, the ecosystem found that mutual TLS is operationally painful behind modern API gateways and content delivery networks, and application-layer proof of possession (DPoP, RFC 9449) became mature enough to offer as an alternative.

If you work on open banking plumbing, this profile sits next to the regulatory layer rather than replacing it. Our walkthrough of FDX API versus PSD3 open banking data sharing covers the data-model and regulatory differences between US and EU regimes, and the PSD3 and PSR API architecture analysis covers the legal API surface. FAPI 2.0 is the transport-security and authorization layer underneath either.

A word on the name. FAPI originally stood for Financial-grade API. The working group has said the profile is applicable beyond finance, to any high-value API, and the Security Profile itself describes its scope as protecting high-value APIs. Healthcare and government data are the commonly cited second use cases. Keep that in mind when a vendor tells you FAPI is “only for banks.”

The specification family

FAPI 2.0 is not one document. It is a small family, and confusing the members is the most common source of bad claims.

The Security Profile is the normative core: what authorization servers, clients and resource servers must do. The Attacker Model is the threat description the profile is designed against. The Message Signing specification adds optional signed requests, responses and introspection responses for use cases that need non-repudiation. The Grant Management specification (separate from the security profile) gives clients an API to query and revoke consent grants. The OpenID Foundation also runs a conformance suite and certification program that tests an authorization server or client against these profiles.

Per the foundation’s publication pages, the Security Profile and Attacker Model were finalized on 22 February 2025, and the Message Signing specification was finalized on 25 September 2025. The status of Grant Management and of individual conformance test plans changes more often, so check the foundation’s pages before you quote them to an auditor.

The FAPI 2.0 Reference Architecture: Four Mandatory Moves

FAPI 2.0 is an OAuth 2.0 authorization code profile that makes four things mandatory: the client pushes its authorization request directly to the server (PAR), proves the code belongs to it (PKCE with S256), receives an access token that only its own key can use (DPoP or mTLS), and authenticates with a key rather than a shared secret (private_key_jwt or mTLS).

FAPI 2.0 reference architecture showing client, PAR endpoint, authorization endpoint, token endpoint and resource server

Figure 1: FAPI 2.0 reference architecture. The request is pushed over a back channel, the browser carries only a reference, and the token is bound to a client key.

Figure 1 shows the shape. The client talks to the authorization server’s PAR endpoint over a direct, authenticated channel. The browser only ever carries a short reference, the request_uri. After the user authenticates and consents, the authorization code returns with an iss parameter. The client redeems the code at the token endpoint with its PKCE verifier and a client credential, and receives a token that is cryptographically tied to a client key. Every call to the resource server then carries proof of possession of that key.

Read the profile as a series of subtractions from base OAuth. There is exactly one grant type for user-delegated access (the authorization code grant), exactly one response type (code), only confidential clients, no resource owner password credentials, and no bearer tokens. Public clients such as bare single-page apps and mobile apps without a backend are out of scope; they are expected to be fronted by a confidential backend or to use a different profile. That is a deliberate narrowing, not an oversight.

Move 1: Push the request (PAR)

Pushed Authorization Requests, defined in RFC 9126 (September 2021), change where the authorization parameters travel. Instead of building a long URL with scope, redirect_uri, state and other parameters and sending the user’s browser to it, the client POSTs those parameters to the authorization server’s PAR endpoint. The server authenticates the client, validates the request, stores it, and returns a request_uri plus an expires_in lifetime. The browser is then redirected to the authorization endpoint with only client_id and request_uri.

The security benefit is threefold. Integrity: the parameters never pass through the browser, so a malicious browser extension, a compromised intermediary or a user editing the URL cannot change the requested scope or redirect URI. Confidentiality: sensitive values, such as a payment amount or an account identifier in a rich authorization request, stay out of browser history and server access logs. Authentication: the authorization server learns who is asking before it shows a consent screen, so it can reject unregistered clients and unauthorized scopes early.

FAPI 2.0 tightens PAR further. The Security Profile requires the authorization server to reject authorization requests that did not arrive via PAR, requires redirect_uri in the pushed request, and caps the request_uri lifetime below 600 seconds. In practice, a few tens of seconds is plenty, because the user is redirected immediately.

PAR also replaces the main reason FAPI 1.0 required signed request objects. The Security Profile states that PAR takes the place of JAR (JWT-Secured Authorization Request, RFC 9101) for protecting the request. A request pushed over an authenticated TLS channel is already integrity-protected and bound to the client. JAR remains available through the Message Signing specification when you need a signed artifact for non-repudiation, but it is no longer the default.

Move 2: Bind the code (PKCE, S256 and iss)

PKCE (Proof Key for Code Exchange, RFC 7636) was created for mobile apps but is now required for all clients under the profile. The client generates a random code_verifier, sends the SHA-256 hash as code_challenge with method S256 in the pushed request, and presents the verifier at the token endpoint. An attacker who steals or injects an authorization code cannot redeem it without the verifier.

The profile requires S256 specifically. The plain method, where the challenge equals the verifier, defeats the purpose if the request is observable, so it is out. The profile also asks that the verifier be generated per request and bound to the client and the user agent. A single static verifier shared across sessions would reduce PKCE to a static secret.

The second binding is the issuer. RFC 9207 defines an iss parameter in the authorization response, so the client can check which authorization server produced it. This defends against mix-up attacks, where a client that supports multiple authorization servers is tricked into sending a code from the honest server to the attacker’s token endpoint. FAPI 2.0 requires the authorization server to return iss and the client to verify it against the issuer it started the flow with. The Security Profile also requires clients to use only endpoint metadata from the discovery document and to confirm that the issuer used for discovery matches the issuer value inside it.

Authorization codes are short-lived by design. The profile caps their lifetime at 60 seconds and requires the server to reject a code that has been used before. Reuse detection is more than hygiene: a replayed code is a signal that something intercepted it, and many servers revoke tokens already issued from that code.

Move 3: Sender-constrain the token (DPoP or mTLS)

This is the big one. A sender-constrained access token is cryptographically bound to a key held by the client, so the resource server can reject a token that is presented without proof the presenter holds that key. FAPI 2.0 requires authorization servers to issue only sender-constrained tokens, using either mutual TLS (RFC 8705, February 2020) or DPoP (RFC 9449, September 2023). Clients must support one or both; resource servers must be able to verify whichever the ecosystem selects.

With mTLS, the client authenticates at the TLS layer with an X.509 certificate. The authorization server records the certificate’s SHA-256 thumbprint in the token as cnf.x5t#S256. When the client calls the resource server, the TLS handshake presents the certificate, and the resource server compares its thumbprint with the one in the token.

With DPoP, the client holds an asymmetric key pair (usually ES256) and attaches a signed JWT, the DPoP proof, as a DPoP header on every request. The proof states the HTTP method (htm), the target URI (htu), a unique identifier (jti), a creation time (iat), and, when presented with an access token, a hash of that token (ath). The token’s cnf.jkt member holds the thumbprint of the proof key. We cover the details in the deeper analysis below.

Either way, the outcome is that a stolen token is useless on its own. The attacker needs the private key too, and the key can live in a hardware security module or secure enclave where it is non-exportable.

Move 4: Authenticate the client with a key, not a secret

The profile lets clients authenticate with mTLS (tls_client_auth or self_signed_tls_client_auth) or with private_key_jwt, where the client signs a short-lived JWT assertion with its private key. client_secret_basic and client_secret_post are not permitted. A shared secret can be copied from a config file, a CI variable or a debug log. A private key in a hardware-backed store cannot.

The private_key_jwt assertion uses the client identifier as both iss and sub, the authorization server’s issuer identifier as aud, a unique jti, and a short expiry. The profile requires the authorization server to accept the issuer identifier as a string in aud. This tightens an old ambiguity in which different servers accepted different endpoint URLs as the audience, which in turn enabled cross-endpoint assertion replay.

Cryptographic floor

The profile allows PS256, ES256 and EdDSA (Ed25519) for JWT signatures, forbids none, requires RSA keys of at least 2048 bits and elliptic-curve keys of at least 224 bits, and requires non-user credentials to carry at least 128 bits of entropy. TLS must be 1.2 or later, following the IETF’s BCP 195, and server-to-server endpoints restrict cipher suites to the recommended set. RS256 is absent: PKCS#1 v1.5 signatures remain valid but carry a long history of implementation bugs, and the profile prefers PSS.

What FAPI 2.0 deliberately does not require

Compared with FAPI 1.0 Advanced, three things disappear from the mandatory list: signed request objects (JAR), signed authorization responses (JARM), and OpenID Connect hybrid flow with an ID token acting as a detached signature over the response. The profile’s own section on this explains the substitution: PAR covers what JAR protected, and a plain response containing only the authorization code (plus iss) covers what JARM protected. The profile does not need OpenID Connect at all, which is why it can also be used for non-identity APIs. Where an ecosystem still needs signed artifacts, the Message Signing specification layers them on, and it requires clients to verify signed ID tokens when ID tokens are in use.

This subtraction matters more than it looks. Every optional feature in a security profile is a code path to test, a configuration to get wrong, and a source of interoperability failures. One mandatory path is easier to certify than three optional ones.

Deeper Analysis: The Flow, the Proofs and the Code

Reading the requirements as a list hides the sequencing. Figure 2 shows the full authorization code flow with every FAPI 2.0 mandatory element in place, from the pushed request to a DPoP-protected API call.

FAPI 2.0 sequence diagram of PAR, PKCE, authorization code and DPoP protected resource access

Figure 2: End-to-end FAPI 2.0 flow using pushed authorization requests, PKCE, issuer identification and a DPoP-bound access token.

Three properties of this sequence deserve attention. First, the only values that cross the browser are the request_uri, the authorization code and the iss; nothing in them is a secret that grants access by itself. Second, the code is useless without the PKCE verifier and the client credential. Third, the access token is useless without the DPoP key. An attacker has to defeat all three independent layers, not one.

How a DPoP proof works

A DPoP proof is a JWT with typ set to dpop+jwt, an asymmetric alg, and the client’s public key in the jwk header. The payload carries jti, htm, htu and iat; the ath claim (a base64url SHA-256 hash of the access token) is required whenever the proof accompanies an access token; and a nonce is required if the server has supplied one. The token response signals binding by returning token_type of DPoP, and the token carries a cnf claim whose jkt member is the SHA-256 JWK thumbprint of the key.

The resource server validates the proof against a checklist: the signature verifies against the embedded jwk; htm and htu match the actual request (query and fragment removed from the URI); iat is recent; the jti has not been seen before within the acceptance window; ath matches the presented token; and the proof key’s thumbprint equals the cnf.jkt in the token. A resource server that also accepts bearer tokens must reject a DPoP-bound token presented as a bearer token, otherwise the downgrade silently removes the protection.

Under FAPI 2.0 an authorization server using DPoP must also support Authorization Code Binding to the DPoP key. The client adds a dpop_jkt parameter (the thumbprint of its DPoP key) to the pushed authorization request. At the token endpoint the server computes the thumbprint from the DPoP proof and refuses the exchange if it differs. This closes a gap: without it, an attacker who obtains an authorization code could redeem it with the attacker’s own DPoP key and receive a token bound to that key.

DPoP nonces are optional in the profile. A server-provided nonce limits the time window in which a pre-generated proof can be replayed, because the client has to ask the server for a fresh value. Clients should expect a use_dpop_nonce error with a DPoP-Nonce header and retry once. The profile also lets servers accept iat values slightly in the future (up to 10 seconds) and requires rejection beyond 60 seconds, to tolerate clock skew without opening a large replay window.

A runnable PAR and DPoP sketch in Python

The following sketch is educational, not production code. It uses PyJWT and cryptography, generates a DPoP key and a client authentication key, pushes an authorization request with PKCE and dpop_jkt, and shows how to build the DPoP proof for the token request and for a resource call. Endpoints are placeholders; real deployments must read them from the discovery document.

import base64, hashlib, json, secrets, time, uuid
import jwt, requests
from cryptography.hazmat.primitives.asymmetric import ec
from jwt.algorithms import ECAlgorithm

ISSUER   = "https://auth.example-bank.test"
PAR_URL  = f"{ISSUER}/par"
TOKEN_URL = f"{ISSUER}/token"
API_URL  = "https://api.example-bank.test/accounts"
CLIENT_ID = "tpp-client-123"
REDIRECT  = "https://tpp.example.test/callback"

def b64u(raw: bytes) -> str:
    return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()

# 1. Keys. In production both live in an HSM or KMS, not in process memory.
dpop_key   = ec.generate_private_key(ec.SECP256R1())
client_key = ec.generate_private_key(ec.SECP256R1())
dpop_jwk   = ECAlgorithm.to_jwk(dpop_key.public_key(), as_dict=True)

def jwk_thumbprint(jwk: dict) -> str:
    # RFC 7638 canonical form for EC keys: required members, sorted, no spaces
    canon = {k: jwk[k] for k in ("crv", "kty", "x", "y")}
    data = json.dumps(canon, separators=(",", ":"), sort_keys=True).encode()
    return b64u(hashlib.sha256(data).digest())

def client_assertion() -> str:
    now = int(time.time())
    claims = {"iss": CLIENT_ID, "sub": CLIENT_ID, "aud": ISSUER,
              "jti": str(uuid.uuid4()), "iat": now, "exp": now + 60}
    return jwt.encode(claims, client_key, algorithm="ES256")

def dpop_proof(method: str, url: str, access_token: str = None,
               nonce: str = None) -> str:
    claims = {"jti": str(uuid.uuid4()), "htm": method,
              "htu": url.split("?")[0], "iat": int(time.time())}
    if access_token:
        claims["ath"] = b64u(hashlib.sha256(access_token.encode()).digest())
    if nonce:
        claims["nonce"] = nonce
    headers = {"typ": "dpop+jwt", "jwk": dpop_jwk}
    return jwt.encode(claims, dpop_key, algorithm="ES256", headers=headers)

# 2. PKCE
verifier  = b64u(secrets.token_bytes(32))
challenge = b64u(hashlib.sha256(verifier.encode()).digest())

# 3. Pushed authorization request
par = requests.post(PAR_URL, data={
    "client_id": CLIENT_ID,
    "client_assertion_type":
        "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
    "client_assertion": client_assertion(),
    "response_type": "code",
    "redirect_uri": REDIRECT,
    "scope": "accounts",
    "state": secrets.token_urlsafe(16),
    "code_challenge": challenge,
    "code_challenge_method": "S256",
    "dpop_jkt": jwk_thumbprint(dpop_jwk),
}, timeout=10)
par.raise_for_status()
request_uri = par.json()["request_uri"]

# 4. Browser step: send the user to the authorization endpoint with only
#    client_id and request_uri. Receive code and iss; verify iss == ISSUER.
auth_url = f"{ISSUER}/authorize?client_id={CLIENT_ID}&request_uri={request_uri}"
code = "<authorization code returned to the redirect URI>"

# 5. Token request with DPoP proof, retrying once on a server nonce
def token_request(nonce=None):
    return requests.post(TOKEN_URL, data={
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": REDIRECT,
        "code_verifier": verifier,
        "client_id": CLIENT_ID,
        "client_assertion_type":
            "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
        "client_assertion": client_assertion(),
    }, headers={"DPoP": dpop_proof("POST", TOKEN_URL, nonce=nonce)},
       timeout=10)

resp = token_request()
if resp.status_code == 400 and resp.json().get("error") == "use_dpop_nonce":
    resp = token_request(resp.headers["DPoP-Nonce"])
resp.raise_for_status()
tok = resp.json()
assert tok["token_type"] == "DPoP", "token is not sender-constrained"

# 6. Resource call: Authorization scheme is DPoP, proof carries ath
api = requests.get(API_URL, headers={
    "Authorization": f"DPoP {tok['access_token']}",
    "DPoP": dpop_proof("GET", API_URL, access_token=tok["access_token"]),
}, timeout=10)
print(api.status_code)

Several details in that sketch are load-bearing. The assertion’s aud is the issuer string, as the profile requires. The ath claim is computed from the exact access token string. The htu is stripped of its query. The token_type assertion prevents the silent downgrade the RFC warns about: if the server returns a plain Bearer token, a client that cares about sender constraint should discard it. The sketch omits error handling, key rotation, jti caching, and the browser step; a production client would also verify iss in the callback and compare state.

How mTLS binding works

With mutual TLS, there is nothing to sign per request. The client presents an X.509 certificate during the TLS handshake. At the token endpoint, the authorization server stores the thumbprint of that certificate in the token, either inside the JWT as cnf with x5t#S256 or in introspection output for opaque tokens. The resource server terminates TLS, extracts the client certificate, and compares thumbprints.

The cryptography is strong and the per-request cost is zero, since TLS session resumption amortises the handshake. The cost is operational. TLS often terminates at a load balancer, a web application firewall or a CDN, and the certificate has to be forwarded to the application in a header the application trusts. Every hop that re-originates TLS breaks the binding. Open banking schemes that use mTLS therefore usually run a dedicated mTLS ingress, with a separate hostname for mTLS endpoints advertised in the authorization server’s mtls_endpoint_aliases metadata, because browser-facing endpoints cannot demand client certificates from end users.

Choosing between DPoP and mTLS

Neither is universally better; the right answer depends on the client population and the infrastructure. Figure 3 gives a decision path, and the matrix below summarises the trade-offs.

Decision flow for choosing DPoP or mTLS sender-constrained tokens in a FAPI 2.0 deployment

Figure 3: Decision flow for choosing between DPoP and mTLS sender-constrained tokens in a FAPI 2.0 deployment.

Dimension mTLS (RFC 8705) DPoP (RFC 9449)
Binding layer Transport (TLS handshake) Application (signed JWT per request)
Key material X.509 certificate and key Bare asymmetric key pair, no PKI
Per-request client cost None beyond TLS One signature per call
Resource server cost Certificate thumbprint compare Signature check, htm/htu/ath checks, jti replay cache
Works through TLS-terminating proxy or CDN Only with trusted header forwarding Yes, proof travels in an HTTP header
Replay protection Inherent to TLS session Requires jti cache and optional nonce
Certificate lifecycle burden High (issuance, rotation, trust anchors) Low (self-generated keys)
Client authentication reuse Same certificate can authenticate the client Separate private_key_jwt needed
Best fit Server-to-server TPP backends in a regulated PKI scheme Heterogeneous clients, gateways, mobile backends

A pragmatic pattern is to let the ecosystem choose one, require resource servers to verify both, and avoid mixing within a single client. Supporting both on the server doubles the test matrix, but it lets regulated TPPs with existing certificate infrastructure and newer fintech clients coexist.

Authorization at the resource server

Sender constraint is only one check. The profile requires the resource server to accept access tokens in the Authorization header only, never in query strings (which leak into logs), to verify validity, integrity, expiry and revocation, and to check that the token authorizes the specific request. In open banking this last step is where most real vulnerabilities hide. A token valid for accounts read must not authorize a payment. A token for consent A must not read the account of consent B. These are authorization bugs, not protocol bugs, and FAPI cannot fix them.

The Attacker Model: What Each Mechanism Defends Against

A security profile is only as convincing as the threats it names. The FAPI 2.0 Attacker Model deliberately does not list concrete attacks; it defines capabilities so that unknown variants are not overlooked, and then states the goals the profile must preserve. Those goals are authorization (no attacker accesses another user’s protected resources), authentication (no attacker logs in at a client as another user) and session integrity (no attacker forces a user into the attacker’s session or resources).

FAPI 2.0 attacker model mapped to the mitigations PAR, PKCE, issuer identification and sender-constrained tokens

Figure 4: FAPI 2.0 attacker capabilities mapped to the mechanism that neutralises each one.

The attacker capabilities

The model defines a small set of attackers who may collaborate in any combination. The A1 web attacker controls endpoints on the internet, can behave as an ordinary user, and can make a victim’s browser send requests, but cannot break cryptography. A1a is the same attacker acting as an authorization server inside the ecosystem, able to replay messages from honest servers and lure users to them. The A2 network attacker can read, block and alter traffic between parties but still cannot break cryptography, which means TLS neutralises most of what it can do.

The A3a attacker passively reads authorization requests from the user’s browser through app URL registration, browser history or cross-site scripting. The model excludes reading responses, because that would defeat most redirect-based schemes. A5 reads requests after the resource server has processed them, for example through logs from a TLS-intercepting proxy; again, reading responses is excluded because it contradicts the authorization goal. The A4 token endpoint attacker, who controls a rogue token endpoint, is retained for information only: FAPI 2.0 requires endpoints to come from authoritative metadata, which removes the opening FAPI 1.0 misconfigurations allowed.

Mapping threats to mechanisms

The mapping in Figure 4 is the heart of the design, and it is worth stating each link as a causal argument.

Authorization request tampering and exposure (A1, A3a). If parameters ride in the URL, a web attacker can craft a link with a different redirect_uri or scope, and a reader of browser history sees the whole request. PAR moves the parameters to an authenticated back channel; the server only honors registered redirect URIs. This is why the profile makes redirect_uri mandatory in the pushed request and forbids plain HTTP redirect URIs except loopback for native clients.

Authorization code injection and theft (A1, A3a). If the attacker obtains a code, whether by observing a redirect or by planting their own code in the victim’s session, PKCE makes that code unredeemable without the verifier, and DPoP binding (dpop_jkt) or mTLS client authentication stops the attacker from getting a token bound to their own key. A code that is short-lived (60 seconds maximum) and single-use reduces the window further.

Mix-up (A1a). A client dealing with several authorization servers can be induced to send an honest server’s code to the attacker’s server. The iss response parameter lets the client notice that the response came from a server other than the one it started with, and the rule to use only discovery-document endpoints removes the lookup the attacker would poison.

Token theft and replay (A2, A5). A bearer token that leaks through a log, a proxy or a compromised downstream service can be replayed from anywhere. A sender-constrained token cannot: without the DPoP key or the client certificate, the stolen token fails verification. This is the principal reason a FAPI 2.0 deployment is meaningfully harder to breach than a plain OAuth deployment, and it is why the profile does not allow bearer tokens.

Credential theft from the client. A shared client secret that leaks gives the attacker the identity of the client. Key-based client authentication, particularly with a non-exportable key, changes the attacker’s job from copying a string to compromising a signing service.

What is out of scope

The attacker model is honest about its limits. Broken TLS is assumed away. JWKS key distribution failures, compromised user devices or browsers, identity proofing and end-user authentication, credential exposure through misconfigured databases or remote code execution on the authorization server, weak random number generation, firewall setups and development practices are all out of scope. The model also does not cover threats that emerge over time.

That list is where real incidents happen. A FAPI 2.0 certificate on the wall does not stop a developer from logging full tokens and keys to a debug stream, or an administrator from exposing a key store. Treat the profile as a set of guarantees conditional on correct implementation of everything it excludes.

Formal analysis

The profile’s design was checked by formal analysis. The Attacker Model cites work by Hosseyni, Küsters and Würtele that models the Security Profile against the attacker model and states that the specifications were updated to align with the analysis and its results. Formal analysis does not prove an implementation correct, but it does give some confidence that the protocol design contains no subtle logical flaw of the kind that plagued earlier OAuth deployments.

Open banking differs from a typical consumer OAuth deployment in one respect: consent is a regulated object. A customer authorizes a specific scope, for a specific period, possibly for a specific payment amount, and must be able to see and revoke it. Neither OAuth nor the FAPI 2.0 Security Profile defines a consent data model. They define how a grant is obtained securely.

The Security Profile does set expectations about refresh tokens. Section 6.1 advises short-lived access tokens combined with refresh tokens for long-lived grants, as guidance rather than a mandatory requirement. Authorization servers are told to avoid refresh token rotation except in extraordinary circumstances, and if rotation is used, to allow a time-limited retry with the old token. The reason is practical: a network failure after the server rotates a refresh token but before the client stores the new one would otherwise lock the customer out of the grant.

Clients, for their part, must support refresh tokens and rotation. Because refresh tokens are sender-constrained in the same way as access tokens when DPoP or mTLS is used, a stolen refresh token is as useless as a stolen access token. That matters more than it sounds, since a long-lived refresh token is a bigger prize than a five-minute access token.

Rich authorization requests and grant management

Scopes are coarse. A payment needs an amount, a currency, a creditor account and perhaps a date; a scope string such as payments cannot express that. Rich Authorization Requests (RFC 9396) carry structured authorization_details in the pushed request. PAR is the natural carrier, because structured JSON in a URL is awkward and exposes financial details to the browser. This is a design argument for why PAR is mandatory, not merely recommended.

The OpenID Foundation’s separate Grant Management specification adds an API for clients to query, update and revoke grants by identifier. I did not verify its final status for this article, so check the foundation’s specification index before relying on it. Whether or not an ecosystem adopts it, the architectural requirement stands: the authorization server must be the system of record for consent, and the resource server must evaluate each call against that record rather than trusting only the token’s scope string. If you want to see how a regional regime defines consent objects and permissions, the PSD3 and PSR API architecture article covers the legal side.

Strong customer authentication is a separate layer

FAPI 2.0 does not authenticate the user; the attacker model puts end-user authentication out of scope. In European open banking the authorization server must perform strong customer authentication under PSD2, which typically combines two of knowledge, possession and inherence. Passkeys are an increasingly practical way to satisfy that requirement on the authorization server’s own login page, and our analysis of passkeys, FIDO2 and PSD2 SCA payment authentication explains how that fits. The relationship is clean: FAPI 2.0 secures the delegation, passkeys secure the person approving it.

Regional Adoption: What I Could and Could Not Verify

Open banking ecosystems adopt security profiles on their own timelines, and public claims about who requires what are often out of date. Sources I checked disagree with each other, so treat the following as a map of uncertainty rather than a ruling.

What is solid: the OpenID Foundation finalized FAPI 2.0 in 2025, runs a certification program under which an authorization server can be tested and display a certified mark, and has long positioned FAPI 1.0 Advanced as the profile used by major open banking schemes. A vendor guide I read lists UK Open Banking, the Berlin Group in the EU, Financial Data Exchange in the US and the Australian Consumer Data Right as FAPI 1.0 users, and says Open Finance Brazil continues to rely on JAR and JARM, which are Advanced-profile features. That guide mentions FAPI 2.0 securing healthcare APIs in Norway.

What conflicts: another tracker site states that UK Open Banking, the UAE’s AlTareq framework and Brazil Open Finance have adopted FAPI 2.0. I could not confirm this against the scheme owners’ own specifications, and it contradicts the other source. I am not repeating it as fact. Likewise, I did not verify whether FDX’s security profile mandates FAPI 2.0, and I did not verify the status of the Australian Consumer Data Right’s security standards against the 2.0 profile.

The takeaway for an architect is procedural. If you build to a regulated scheme, read the scheme’s current technical specification and its conformance tests, not a blog post, including this one. If you build a private or contractual API (bank to fintech partner, or an internal high-value API), FAPI 2.0 is the cleaner starting point because it has fewer optional paths than FAPI 1.0.

Migration is the practical issue. A scheme that mandates FAPI 1.0 Advanced cannot switch overnight: participants have signed request object libraries, hybrid-flow clients and certificates deployed. Expect transition periods during which authorization servers support both profiles, with the heavier code path remaining until the last client moves. Building a new authorization server now, supporting FAPI 2.0 first and adding Advanced compatibility as needed for a given scheme, is usually simpler than the reverse.

Trade-offs, Gotchas, and What Goes Wrong

PAR adds a hard dependency and a state store. Every authorization now begins with a synchronous call to the PAR endpoint, and the server must persist pushed requests until they expire or are consumed. If the PAR endpoint is down, no new authorizations start. Size it like the token endpoint: it deserves the same availability target, rate limiting and horizontal scaling, and it needs storage that survives node restarts within the request_uri lifetime.

DPoP replay protection needs shared state. A resource server that checks jti uniqueness must remember identifiers for the acceptance window across every instance. With many gateway replicas, that means a shared cache or a deliberately accepted small replay window. Teams often implement signature verification and skip the replay check, leaving the proof valid for replay within its iat window. A server nonce narrows the window but adds a round trip on first use.

Clock skew is a quiet failure source. DPoP proofs and client assertions depend on iat, nbf and exp. The profile tolerates small future-dated values (up to 10 seconds accepted, beyond 60 rejected), so a client whose clock drifts a minute will be rejected in a way that looks like a random authentication failure. Run NTP on every client and log the rejection reason server-side.

mTLS breaks at proxies. The most common mTLS bug is a load balancer or CDN that terminates TLS and either drops the client certificate or forwards it in a header that an attacker can also set. If your gateway trusts a forwarded-certificate header, ensure the edge strips any client-supplied copy. A spoofable header silently turns sender-constrained tokens back into bearer tokens.

Downgrade through a permissive resource server. If an API accepts both Authorization: Bearer and Authorization: DPoP, an attacker with a stolen token can present it as a bearer token unless the server rejects bound tokens used that way. The same applies to a token endpoint that still issues bearer tokens to some clients “for compatibility.”

Public clients are out of scope. A mobile app calling the API directly cannot hold a confidential credential safely in the way the profile assumes. The typical answer is a backend-for-frontend that is the OAuth client, with the app authenticating to the backend. Attestation-based approaches exist but sit outside the Security Profile.

Key management is the new secret management. Moving from client_secret to keys shifts risk from copying a string to protecting and rotating a key. Plan for rotation using jwks_uri publication, overlap windows, unique kid values, and a revocation process when a key is suspected compromised. The profile advises that jwks_uri endpoints use TLS and avoid x5u and jku headers, which invite server-side request forgery and key-substitution issues.

Certification is not a guarantee. Passing conformance tests shows that an implementation behaves correctly in the scenarios the suite exercises. It does not cover your logging, your administrators, or your authorization logic. The most damaging open banking incidents tend to be broken object-level authorization on the resource server, not OAuth protocol failures.

Complexity moves, it does not vanish. Removing JAR and JARM simplifies the base profile, but ecosystems that need signed non-repudiable artifacts will add Message Signing, and the combined deployment is again large. Be honest about whether you need the signatures or merely feel you should.

Practical Recommendations

Start from the threat you are actually defending against, then pick mechanisms. For most new high-value API programs, the FAPI 2.0 Security Profile is the right baseline because it is small, formally analysed, and testable. If you are bound to a regulated scheme, treat the scheme’s technical specification as the source of truth and use FAPI 2.0 as the design reference for anything the scheme leaves open.

Choose one sender-constraint mechanism per client population. Use mTLS where you already operate a certificate authority and a dedicated mTLS ingress; use DPoP where clients are heterogeneous or traffic passes through TLS-terminating infrastructure. Keep keys in an HSM or cloud key management service wherever the platform allows non-exportable keys.

Instrument the whole flow. Log the PAR client, request_uri identifier, code redemption, dpop_jkt mismatches, nonce errors and replay rejections with correlation identifiers, and never log tokens, proofs or private material. Rate limit PAR and token endpoints per client. Treat every authorization code reuse as a security event.

Finally, put your effort where the profile stops. Invest in object-level authorization at the resource server, consent lifecycle management, and operational controls on the authorization server itself.

  • Publish discovery metadata and make clients consume endpoints only from it.
  • Require PAR, PKCE S256, iss verification and single-use 60-second codes.
  • Issue only sender-constrained tokens; reject bearer presentation of bound tokens.
  • Authenticate clients with private_key_jwt or mTLS; ban shared secrets.
  • Restrict signing to PS256, ES256 or EdDSA; enforce TLS 1.2 or later.
  • Share a jti replay cache across resource server instances and consider DPoP nonces.
  • Run the OpenID Foundation conformance suite in CI before each release.
  • Test authorization logic with negative cases across consents, scopes and users.

Disclaimer: this article is educational systems analysis, not security, legal, regulatory or financial advice. Verify requirements against the current official specifications and your regulator before implementation.

Frequently Asked Questions

What is FAPI 2.0?

FAPI 2.0 is a family of OpenID Foundation specifications that profile OAuth 2.0 for high-value APIs such as open banking. The Security Profile and Attacker Model reached final status on 22 February 2025. The profile mandates the authorization code flow with pushed authorization requests, PKCE using S256, sender-constrained access tokens through DPoP or mutual TLS, and key-based client authentication. It was designed against a formally analysed attacker model rather than a checklist of known attacks.

What is the difference between FAPI 1.0 Advanced and FAPI 2.0?

FAPI 1.0 Advanced relied on signed request objects, response protection through JARM or hybrid flow with ID tokens, and mTLS-bound tokens. FAPI 2.0 replaces signed request objects with pushed authorization requests, replaces JARM with a plain code response carrying an issuer identifier, and adds DPoP as an alternative to mTLS. The result is fewer optional paths and a smaller conformance surface. Signed artifacts remain available through the separate Message Signing specification.

Is DPoP or mTLS better for sender-constrained tokens?

Neither wins everywhere. mTLS binds the token at the transport layer, costs nothing per request and can reuse the certificate for client authentication, but it needs certificate lifecycle management and breaks easily behind TLS-terminating proxies. DPoP works at the HTTP layer with self-generated keys and survives proxies, but requires per-request signing, a replay cache and careful validation of method, URL and token hash. The profile allows either, and ecosystems typically standardise on one.

Why is PAR mandatory in FAPI 2.0?

Pushed authorization requests move the authorization parameters off the browser and onto an authenticated back channel. This protects request integrity and confidentiality, lets the server reject unknown clients before showing consent, and carries structured data such as payment details safely. The profile also treats PAR as the replacement for signed request objects. The authorization server must reject authorization requests that did not arrive through PAR, and request_uri values must expire in under 600 seconds.

Does FAPI 2.0 replace PSD2 or PSD3 requirements?

No. FAPI 2.0 is a technical security profile for how authorization and API access are protected. PSD2 and PSD3, along with regional schemes, define who may access what data, consent and liability rules, and strong customer authentication. A scheme may adopt, require or ignore a given FAPI profile, so confirm what your scheme’s current technical specification mandates. This article could not verify every regional mandate and recommends reading the scheme documents directly.

Do I need JAR or JARM with FAPI 2.0?

Not for the Security Profile. PAR replaces the request-protection role of JAR, and a response containing only the authorization code plus the issuer identifier replaces the role of JARM. You would add JAR, JARM or signed introspection responses through the Message Signing specification, finalized on 25 September 2025, if your ecosystem needs non-repudiation of requests or responses. Many deployments will not need them.

Further Reading

Internal:

External primary sources:

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *