[{"data":1,"prerenderedAt":336},["ShallowReactive",2],{"navigation":3,"/blog/fapi-transaction-binding":141,"/blog/fapi-transaction-binding-surround":325},[4,36,55,80,101,116,128],{"title":5,"path":6,"stem":7,"children":8,"icon":35},"ASP.NET Core","/aspnet-core","1.aspnet-core/1.index",[9,11,15,19,23,27,31],{"title":10,"path":6,"stem":7},"Cursor Pagination",{"title":12,"path":13,"stem":14},"Dapper - Micro ORM","/aspnet-core/dapper","1.aspnet-core/2.dapper",{"title":16,"path":17,"stem":18},"DbUp - Database Migrations","/aspnet-core/dbup","1.aspnet-core/3.dbup",{"title":20,"path":21,"stem":22},"Serilog - Structured Logging for .NET","/aspnet-core/serilog","1.aspnet-core/4.serilog",{"title":24,"path":25,"stem":26},"Class Inheritance in C#","/aspnet-core/inheritance","1.aspnet-core/5.inheritance",{"title":28,"path":29,"stem":30},"C# Collections","/aspnet-core/collections","1.aspnet-core/6.collections",{"title":32,"path":33,"stem":34},".NET gitignore Command","/aspnet-core/gitignore","1.aspnet-core/7.gitignore",false,{"title":37,"path":38,"stem":39,"children":40,"icon":35},"Oauth2.0 and OpenIdConnect","/oauth2andopenidconnect","10.oauth2andopenidconnect/1.index",[41,43,47,51],{"title":42,"path":38,"stem":39},"RFC 6749 - OAuth 2.0 Authorization Framework",{"title":44,"path":45,"stem":46},"Authentication (AuthN) vs Authorization (AuthZ)","/oauth2andopenidconnect/authnandauthz","10.oauth2andopenidconnect/2.authnandauthz",{"title":48,"path":49,"stem":50},"TOTP Authentication","/oauth2andopenidconnect/totp","10.oauth2andopenidconnect/3.totp",{"title":52,"path":53,"stem":54},"OAuth 2.0 Resource Indicators - RFC 8707","/oauth2andopenidconnect/oauth2resourceindicators","10.oauth2andopenidconnect/4.OAuth2ResourceIndicators",{"title":56,"path":57,"stem":58,"children":59,"page":35},"Azure","/azure","2.azure",[60,64,68,72,76],{"title":61,"path":62,"stem":63},"Azure Cost Management","/azure/azuremanagementandgovernance","2.azure/1.Azuremanagementandgovernance",{"title":65,"path":66,"stem":67},"Azure Policy","/azure/azurepolicy","2.azure/2.azurepolicy",{"title":69,"path":70,"stem":71},"Code Blocks","/azure/code-blocks","2.azure/3.code-blocks",{"title":73,"path":74,"stem":75},"Prose Components","/azure/prose-components","2.azure/4.prose-components",{"title":77,"path":78,"stem":79},"Images and Embeds","/azure/images-embeds","2.azure/5.images-embeds",{"title":81,"path":82,"stem":83,"children":84,"page":35},"Git","/git","3.git",[85,89,93,97],{"title":86,"path":87,"stem":88},"Git Rebase","/git/git-rebase","3.git/1.git-rebase",{"title":90,"path":91,"stem":92},"Git Stash","/git/git-stash","3.git/2.git-stash",{"title":94,"path":95,"stem":96},"SemVer","/git/semver","3.git/3.semver",{"title":98,"path":99,"stem":100},"Conventional Commits","/git/conventional-commits","3.git/4.conventional-commits",{"title":102,"path":103,"stem":104,"children":105,"icon":35},"Design Patterns","/design-patterns","7.design-patterns/1.index",[106,108,112],{"title":107,"path":103,"stem":104},"Introduction",{"title":109,"path":110,"stem":111},"Installation","/design-patterns/installation","7.design-patterns/2.installation",{"title":113,"path":114,"stem":115},"Usage","/design-patterns/usage","7.design-patterns/3.usage",{"title":117,"path":118,"stem":119,"children":120,"icon":35},"Software Principles","/principles","8.principles/1.index",[121,122,125],{"title":107,"path":118,"stem":119},{"title":109,"path":123,"stem":124},"/principles/installation","8.principles/2.installation",{"title":113,"path":126,"stem":127},"/principles/usage","8.principles/3.usage",{"title":129,"path":130,"stem":131,"children":132,"icon":35},"Software Architecture","/architecture","9.architecture/1.index",[133,135,138],{"title":134,"path":130,"stem":131},"CQRS Pattern",{"title":109,"path":136,"stem":137},"/architecture/installation","9.architecture/2.installation",{"title":113,"path":139,"stem":140},"/architecture/usage","9.architecture/3.usage",{"id":142,"title":143,"body":144,"date":314,"description":315,"extension":316,"featured":35,"meta":317,"navigation":318,"path":319,"readTime":320,"seo":321,"stem":322,"tag":323,"__hash__":324},"blog/blog/19.fapi-transaction-binding.md","Why FAPI Moved From \"Authenticate the Client\" to \"Bind the Transaction to the Client\"",{"type":145,"value":146,"toc":299},"minimark",[147,151,156,159,167,171,179,183,194,198,206,214,225,241,245,248,252,255,259,266,269,273],[148,149,150],"p",{},"Knowing who's calling an API used to be enough. In financial systems, it isn't anymore — what's needed instead is confidence that a specific action is tied to the legitimate client, the legitimate user, and the exact context it was actually approved in. That shift, from authenticating a caller to binding a transaction, is the throughline connecting most of what the Financial-grade API (FAPI) profiles actually require.",[152,153,155],"h2",{"id":154},"the-old-question-was-is-this-client-authenticated","The Old Question Was \"Is This Client Authenticated?\"",[148,157,158],{},"Traditional API security starts simple: authenticate the client, issue a token, let the client call the API with that token. For a huge share of applications, that's genuinely sufficient — reading a public feed or updating a user's display name doesn't need much more than proof that a legitimate, recognized client is making the request.",[148,160,161,162,166],{},"A bank transfer isn't that kind of request. A payment isn't just an HTTP call with a JSON body attached, and a customer identity change isn't a routine field update — each is a high-impact, often irreversible action, and \"the client that called this endpoint was authenticated at some point\" doesn't actually answer the question that matters: was ",[163,164,165],"em",{},"this specific action"," legitimately authorized, by the right user, in the right context, right now?",[152,168,170],{"id":169},"private-key-jwt-proving-ownership-not-just-presenting-a-password","Private Key JWT: Proving Ownership, Not Just Presenting a Password",[148,172,173,174,178],{},"The first piece of this shift replaces a shared secret with cryptographic proof of client ownership. Instead of a ",[175,176,177],"code",{},"client_secret"," both sides hold, the client signs a JWT with a private key only it possesses, and the authorization server verifies that signature against the client's registered public key. The server isn't trusting a password anyone with access to a config file could copy — it's verifying that whoever's making this specific request actually controls the private key tied to this specific client's registered identity.",[152,180,182],{"id":181},"mtls-binding-identity-at-the-connection-level","mTLS: Binding Identity at the Connection Level",[148,184,185,186,193],{},"Mutual TLS pushes proof-of-possession down a layer, binding identity to the TLS connection itself rather than to an individual signed token. The client presents a certificate; the server verifies its validity, its ownership, and its chain of trust, and the resulting connection is cryptographically tied to that authenticated client for its duration. ",[187,188,192],"a",{"href":189,"rel":190},"https://datatracker.ietf.org/doc/html/rfc8705",[191],"nofollow","RFC 8705"," formalizes this for OAuth specifically — certificate-bound access tokens that a resource server can confirm were actually issued to, and are being presented by, the same client that established the mTLS connection. It's a natural fit for bank-to-bank integrations and other controlled enterprise environments where certificate infrastructure is already part of how systems talk to each other.",[152,195,197],{"id":196},"dpop-binding-the-token-to-a-key-at-the-request-level","DPoP: Binding the Token to a Key at the Request Level",[148,199,200,205],{},[187,201,204],{"href":202,"rel":203},"https://datatracker.ietf.org/doc/html/rfc9449",[191],"DPoP (RFC 9449)"," takes the same underlying idea — prove possession of a key, not just presentation of a credential — and applies it per HTTP request. The client attaches a signed proof to each call, covering the method, the URI, a timestamp, and a hash of the access token itself, all signed with a private key bound to that token at issuance. A stolen token, on its own, is worthless to an attacker who doesn't also hold the key that has to sign every request made with it.",[152,207,209,210,213],{"id":208},"rich-authorization-requests-binding-the-transaction-not-just-the-session","Rich Authorization Requests: Binding the ",[163,211,212],{},"Transaction",", Not Just the Session",[148,215,216,217,220,221,224],{},"This is where FAPI's shift becomes most explicit, and it's the piece easy to miss if the story stops at \"who's the client.\" Client authentication and proof-of-possession answer ",[163,218,219],{},"who's calling",". They say nothing about ",[163,222,223],{},"what, exactly, was approved",". Traditional OAuth scopes are coarse — \"payments:write\" authorizes payment-initiation in general, but says nothing about which account, which amount, or which recipient a user actually consented to.",[148,226,227,232,233,236,237,240],{},[187,228,231],{"href":229,"rel":230},"https://datatracker.ietf.org/doc/html/rfc9396",[191],"RFC 9396, Rich Authorization Requests (RAR)",", closes exactly that gap. It introduces a structured ",[175,234,235],{},"authorization_details"," parameter that lets a client specify precisely what's being authorized — a payment of a specific amount, to a specific account, in a specific currency — as part of the authorization request itself, rather than relying on a generic scope string. A payment request under RAR isn't \"this client has payment-initiation access.\" It's \"this exact transaction, for this exact amount, to this exact recipient, is what the user is being asked to approve,\" and the token that's ultimately issued carries that specific detail rather than a broad grant that happens to cover it. That's the mechanism that actually lets a system claim it's binding the ",[163,238,239],{},"transaction"," to the client — not just the session, and not just the connection.",[152,242,244],{"id":243},"ciba-binding-the-approval-to-the-right-user-not-just-the-right-session","CIBA: Binding the Approval to the Right User, Not Just the Right Session",[148,246,247],{},"The remaining piece is making sure a human, specifically the right human, actually approved the action — which is where flows like Client-Initiated Backchannel Authentication earn their place in financial contexts. A customer approving a payment isn't just accessing an API endpoint. They're authorizing an irreversible business action, on a device the bank already recognizes as theirs, and a push-based approval model built on the same possession-based logic as the rest of this stack — approve this specific transfer, on this specific registered device, right now — closes the last gap between \"a valid token was presented\" and \"the actual account holder said yes to this specific thing.\"",[152,249,251],{"id":250},"the-pattern-across-all-of-it","The Pattern Across All of It",[148,253,254],{},"Private Key JWT proves client ownership. mTLS proves connection identity. DPoP proves possession of the signing key on every request. RAR proves the specifics of what's actually being authorized, not just that authorization exists in some general sense. CIBA strengthens the link between the approval and the actual human behind it. None of these mechanisms exist in isolation — together, they move financial APIs away from a single upfront credential check and toward continuous, specific, cryptographic proof that a particular client, a particular user, and a particular transaction genuinely belong together.",[152,256,258],{"id":257},"the-takeaway","The Takeaway",[148,260,261,262,265],{},"For high-value systems, authenticating the client was never the finish line — it was the floor. The real target is trusted ",[163,263,264],{},"authorization",": not just confidence that a recognized caller is on the other end of a request, but confidence that this specific action, at this specific moment, for this specific amount, was legitimately approved by the person it claims to represent. That's a meaningfully higher bar than \"did we check the token,\" and it's exactly the bar FAPI was built to enforce.",[267,268],"hr",{},[152,270,272],{"id":271},"references-and-further-reading","References and Further Reading",[274,275,276,285,292],"ul",{},[277,278,279,280,284],"li",{},"IETF, ",[187,281,283],{"href":229,"rel":282},[191],"RFC 9396: OAuth 2.0 Rich Authorization Requests"," — the standard letting clients specify fine-grained, transaction-level authorization details instead of coarse scopes.",[277,286,279,287,291],{},[187,288,290],{"href":202,"rel":289},[191],"RFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP)"," — binding access tokens to a client-held key at the individual request level.",[277,293,279,294,298],{},[187,295,297],{"href":189,"rel":296},[191],"RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"," — binding tokens to a client's TLS certificate for connection-level proof of possession.",{"title":300,"searchDepth":301,"depth":302,"links":303},"",1,2,[304,305,306,307,308,310,311,312,313],{"id":154,"depth":302,"text":155},{"id":169,"depth":302,"text":170},{"id":181,"depth":302,"text":182},{"id":196,"depth":302,"text":197},{"id":208,"depth":302,"text":309},"Rich Authorization Requests: Binding the Transaction, Not Just the Session",{"id":243,"depth":302,"text":244},{"id":250,"depth":302,"text":251},{"id":257,"depth":302,"text":258},{"id":271,"depth":302,"text":272},"2026-08-12","Knowing who's calling an API used to be enough. Private Key JWT, mTLS, DPoP, Rich Authorization Requests, and CIBA are the mechanisms that let FAPI bind a specific transaction to a specific client and user, not just a session.","md",{},true,"/blog/fapi-transaction-binding","6 min",{"title":143,"description":315},"blog/19.fapi-transaction-binding","OAuth2 / OpenID Connect","bw0TrMX8bIAidoLyxiDBK8VNvGi22Ssee5y7AwRBMBU",[326,331],{"title":327,"path":328,"stem":329,"description":330,"children":-1},"Your Secure APIs May Need More Than Bearer Tokens","/blog/beyond-bearer-tokens-proof-of-possession","blog/18.beyond-bearer-tokens-proof-of-possession","If possession of a stolen token is enough to access your API, the token has become the password. DPoP, mTLS, and why proof-of-possession asks a fundamentally different question than a bearer token ever could.",{"title":332,"path":333,"stem":334,"description":335,"children":-1},"Clean Architecture or Not? Why the Real Question Is Autonomy and Pragmatism","/blog/clean-architecture-best-practice-or-creativity-killer","blog/2.clean-architecture-best-practice-or-creativity-killer","Popularized by Robert C. Martin, Clean Architecture divides opinion for good reason. On the Big Ball of Mud, Vertical Slice Architecture, and why the real question is autonomy paired with pragmatism.",1785744836092]