[{"data":1,"prerenderedAt":331},["ShallowReactive",2],{"navigation":3,"/blog/beyond-bearer-tokens-proof-of-possession":141,"/blog/beyond-bearer-tokens-proof-of-possession-surround":320},[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":309,"description":310,"extension":311,"featured":35,"meta":312,"navigation":313,"path":314,"readTime":315,"seo":316,"stem":317,"tag":318,"__hash__":319},"blog/blog/18.beyond-bearer-tokens-proof-of-possession.md","Your Secure APIs May Need More Than Bearer Tokens",{"type":145,"value":146,"toc":293},"minimark",[147,151,156,164,167,171,174,178,188,191,195,198,202,210,214,223,227,234,237,241,244,248,251,255,258,261,265],[148,149,150],"p",{},"If possession of a stolen token is enough to access your API, the token has become the password — and it inherited every weakness a password has, without most of the mitigations a password gets. That's the uncomfortable truth sitting underneath a lot of API designs protecting genuinely valuable resources: banking transactions, healthcare records, payment operations, systems where the cost of a wrong answer isn't hypothetical.",[152,153,155],"h2",{"id":154},"bearer-tokens-work-like-cash","Bearer Tokens Work Like Cash",[148,157,158,159,163],{},"A bearer token's entire security model is baked into its name: whoever bears it, spends it. The API checks whether the token is valid, whether it's expired, whether it carries the right scopes — and if all three check out, access is granted. What the API fundamentally doesn't know is ",[160,161,162],"em",{},"who"," is presenting the token. It only knows someone has it, the same way a cashier accepting a twenty-dollar bill has no way of knowing whether the person handing it over is the person it was originally given to.",[148,165,166],{},"That's a fine model for a lot of low-stakes situations. It becomes a liability the moment token theft is a realistic threat, because an attacker who obtains a valid access token — through malware, a compromised device, browser storage theft, a logging mistake, or a breached piece of infrastructure — doesn't need the user's password, the original device, or the original application. They need the token, full stop, and the API has no mechanism to tell the difference between the legitimate holder and the thief.",[152,168,170],{"id":169},"proof-of-possession-asks-a-different-question","Proof of Possession Asks a Different Question",[148,172,173],{},"The fix isn't a better token. It's a different question entirely. Instead of \"do you have the token,\" proof-of-possession mechanisms ask \"can you prove you're the entity this token was actually issued to.\" That reframing is the whole security improvement: the token alone stops being sufficient, because the requester also has to demonstrate control of a cryptographic key that never travels with the token itself.",[152,175,177],{"id":176},"dpop-binding-a-token-to-a-key-the-client-actually-holds","DPoP: Binding a Token to a Key the Client Actually Holds",[148,179,180,187],{},[181,182,186],"a",{"href":183,"rel":184},"https://datatracker.ietf.org/doc/html/rfc9449",[185],"nofollow","DPoP (Demonstrating Proof of Possession)",", standardized as RFC 9449 in 2023, is the OAuth mechanism built specifically for this. Instead of a bare bearer token, the client sends a signed JWT — the DPoP proof — alongside the request. That proof is a signature over specifics of the actual HTTP request: the method, the URI, a timestamp, a unique identifier, and a hash of the associated access token, all signed with a private key the client holds. The authorization server binds the original access token to the public part of that key pair, and the resource server, on every subsequent request, verifies three things: that the proof's signature is valid, that the key matches what the token was bound to, and that the request details in the proof actually match the request it's attached to.",[148,189,190],{},"The practical consequence is direct. An attacker who steals the access token — through any of the theft vectors above — doesn't get the private key alongside it, because the private key was never sent anywhere; it only ever produced signatures. Without that key, the stolen token has no value on its own. The attacker needs both the token and the key, and stealing the first no longer gets them the second.",[152,192,194],{"id":193},"public-keys-still-arent-the-vulnerability","Public Keys Still Aren't the Vulnerability",[148,196,197],{},"The same misconception that shows up around Private Key JWT shows up here too: if an attacker gets the public key, isn't the whole scheme compromised? No — the public key was designed to be shared from the start. Security comes entirely from protecting the private key, and an attacker who knows the public key, the algorithm, and the full structure of the token still can't produce a valid signature without the one piece they don't have.",[152,199,201],{"id":200},"shared-secrets-are-familiar-which-is-part-of-the-problem","Shared Secrets Are Familiar, Which Is Part of the Problem",[148,203,204,205,209],{},"Plenty of systems still authenticate clients with a ",[206,207,208],"code",{},"client_secret"," — a shared-secret model where both the client and the authorization server know the same value. It's familiar and easy to implement, and it comes with the same structural weaknesses shared secrets always have: they leak, get copied into configuration files, complicate rotation, and multiply every time a new environment needs its own copy. For high-value systems, asymmetric cryptography — where only one side ever holds the private key — closes exactly that gap.",[152,211,213],{"id":212},"different-environments-different-proof-of-possession-mechanisms","Different Environments, Different Proof-of-Possession Mechanisms",[148,215,216,217,222],{},"DPoP isn't the only option, and the right choice depends on the client, not a blanket preference. DPoP fits public clients, mobile apps, and browser-based applications, where certificate management is genuinely difficult but binding a token to an application-managed key is straightforward. ",[181,218,221],{"href":219,"rel":220},"https://datatracker.ietf.org/doc/html/rfc8705",[185],"Mutual TLS (RFC 8705)"," — which binds tokens to a client's TLS certificate rather than a separate signed proof — tends to fit machine-to-machine communication and controlled enterprise environments, including financial institution integrations where certificate infrastructure already exists. Private Key JWT, covered on its own terms elsewhere, fits confidential clients able to securely hold and protect a private key long-term. The governing principle isn't \"pick one mechanism and use it everywhere.\" It's \"don't lean on a weaker mechanism once the risk in front of you justifies a stronger one.\"",[152,224,226],{"id":225},"security-should-scale-with-what-the-api-actually-does","Security Should Scale With What the API Actually Does",[148,228,229,230,233],{},"It's worth pushing this a step further than \"payments and health records get the strong stuff, everything else gets bearer tokens.\" Any API that requires authentication at all has already declared itself worth protecting — that's what authentication ",[160,231,232],{},"is for",". A public product catalog doesn't need a login. The moment an API sits behind an auth check, it's guarding something: a user's data, an internal system, an action that shouldn't be available to just anyone. Treating that whole category as \"medium risk, bearer tokens are fine\" undersells what authentication is signaling in the first place.",[148,235,236],{},"That doesn't collapse every authenticated API into the same bucket — a read-only endpoint returning a user's own display name and an endpoint that transfers their money still don't carry equal stakes, and pretending otherwise would justify the wrong kind of blanket answer in the opposite direction. But the baseline assumption is worth resetting: the question isn't \"is this API important enough to deserve proof-of-possession.\" It's \"why would this specific, already-authenticated API be the exception that doesn't need it.\" The higher the real-world cost of misuse, the stronger the authorization model should be — and the floor for \"real-world cost\" starts the moment a token grants access to anything a user or a business would notice losing, not just the handful of endpoints that touch money or medical records directly.",[152,238,240],{"id":239},"convenience-isnt-a-security-architecture","Convenience Isn't a Security Architecture",[148,242,243],{},"\"Shared secrets are easier to implement\" is true and also not the question that matters. The question that matters is what happens if the credential gets stolen. If the honest answer is \"the attacker can act as the client until someone notices,\" that design deserves a second look — not because convenience is worthless, but because it was never actually weighed against the cost of the failure mode it's accepting.",[152,245,247],{"id":246},"the-direction-this-is-heading","The Direction This Is Heading",[148,249,250],{},"The broader trend across identity and authentication is moving the same direction DPoP moves: away from \"whoever holds the credential is trusted\" and toward \"prove you're authorized to use this credential.\" It's the same underlying logic behind signed JWTs, hardware-backed keys, passkeys, certificate-based authentication, and transaction signing more generally — identity is increasingly something proven at the moment of use, not something simply presented and taken at face value.",[152,252,254],{"id":253},"the-takeaway","The Takeaway",[148,256,257],{},"The line isn't between \"high-value APIs\" and everything else — it's between APIs that require authentication and APIs that don't, and the first group is bigger than the \"payments and medical records\" framing suggests. If an API needed a login to begin with, something behind it was worth protecting, and that's the point at which \"a stolen token is enough to get in\" deserves scrutiny rather than a pass because the endpoint doesn't sound dramatic on its own. DPoP and its relatives don't stop tokens from ever being stolen — they make sure that theft alone isn't enough. The attacker still needs the key, and without it, the token they took is worth exactly nothing.",[259,260],"hr",{},[152,262,264],{"id":263},"references-and-further-reading","References and Further Reading",[266,267,268,277,284],"ul",{},[269,270,271,272,276],"li",{},"IETF, ",[181,273,275],{"href":183,"rel":274},[185],"RFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP)"," — the standard defining DPoP proofs and how access tokens get bound to a client-held key.",[269,278,271,279,283],{},[181,280,282],{"href":219,"rel":281},[185],"RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"," — the mTLS-based alternative for sender-constraining tokens in enterprise and machine-to-machine contexts.",[269,285,286,287,292],{},"Authlete, ",[181,288,291],{"href":289,"rel":290},"https://www.authlete.com/kb/oauth-and-openid-connect/proof-of-possession-pop-tokens/certificate-bound-access-tokens/",[185],"\"Issuing Mutual-TLS Certificate-Bound Access Tokens\""," — a practical look at how certificate-bound tokens work in an implementation, distinct from mTLS client authentication itself.",{"title":294,"searchDepth":295,"depth":296,"links":297},"",1,2,[298,299,300,301,302,303,304,305,306,307,308],{"id":154,"depth":296,"text":155},{"id":169,"depth":296,"text":170},{"id":176,"depth":296,"text":177},{"id":193,"depth":296,"text":194},{"id":200,"depth":296,"text":201},{"id":212,"depth":296,"text":213},{"id":225,"depth":296,"text":226},{"id":239,"depth":296,"text":240},{"id":246,"depth":296,"text":247},{"id":253,"depth":296,"text":254},{"id":263,"depth":296,"text":264},"2026-08-11","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.","md",{},true,"/blog/beyond-bearer-tokens-proof-of-possession","8 min",{"title":143,"description":310},"blog/18.beyond-bearer-tokens-proof-of-possession","OAuth2 / OpenID Connect","sPJk-EwjU-9ffwd_cya4f6sRz6xEe-nFnRu4t1jbUWc",[321,326],{"title":322,"path":323,"stem":324,"description":325,"children":-1},"FIPS Compliance: It's More Than Running Your Application on Kubernetes","/blog/fips-compliance-beyond-kubernetes","blog/17.fips-compliance-beyond-kubernetes","FIPS doesn't certify applications, orchestrators, or clouds — it certifies specific, versioned cryptographic modules. \"We deployed to Kubernetes\" was never an answer to the question FIPS is actually asking.",{"title":327,"path":328,"stem":329,"description":330,"children":-1},"Why FAPI Moved From \"Authenticate the Client\" to \"Bind the Transaction to the Client\"","/blog/fapi-transaction-binding","blog/19.fapi-transaction-binding","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.",1785744836093]