Authentication is not a single problem. It is at least three problems wearing a trench coat and pretending to be one. There is the problem of proving that a machine is who it claims to be. There is the problem of proving that a human is who they claim to be. And there is the problem of proving that a token was issued to the entity presenting it and has not been stolen, replayed, or forwarded by someone else.
Most production systems solve one of these problems well and paper over the other two with assumptions. Service-to-service calls use mutual TLS but hand the user a bearer token that anyone who intercepts it can use. User-facing applications deploy WebAuthn for phishing-resistant login but then issue a cookie that is just as stealable as a password hash once it leaves the browser. API gateways validate JWTs but have no mechanism to confirm the presenter is the entity the token was issued to.
This article walks through three protocols -- mutual TLS, WebAuthn, and DPoP -- that each address a different facet of the authentication problem. We will look at what each one provides, what it explicitly does not provide, and how the three compose into a stack that covers the gaps the others leave open. The goal is not a product recommendation. It is an engineering walkthrough that starts from the threat model and arrives at a deployment architecture.
The Authentication Problem Space
Before diving into protocols, it is worth being precise about what we mean by authentication in a distributed system. There are at least four distinct questions a service needs to answer about an incoming request:
Channel authentication. Is this connection coming from the machine I expect? Is the transport layer intact, or has an intermediary inserted itself between us? This is the question TLS answers, and mutual TLS extends it to both directions.
Entity authentication. Is the human or service account behind this request who they claim to be? This is the classic login problem. Passwords, OTPs, biometrics, and public key credentials all live here.
Token binding. Given that the request carries a token or credential, was that token issued to the entity presenting it? Or could it have been intercepted, exfiltrated, or replayed by someone else? This is the bearer token problem, and it is the gap that DPoP was designed to close.
Authorization context. What is this authenticated entity allowed to do? This is authorization, not authentication, and we will not cover it here except to note that getting authentication wrong makes authorization meaningless.
The mistake most architects make is conflating these questions. They deploy mTLS and assume the user is authenticated. They deploy WebAuthn and assume the token is bound. Each protocol answers its own question. None of them answer all four.
Mutual TLS: Authenticating the Channel
Standard TLS is one-sided. The client verifies the server's certificate to confirm it is talking to the right endpoint. The server learns nothing about the client's identity from the TLS handshake itself. Mutual TLS -- mTLS -- closes that gap by requiring the client to present a certificate during the handshake as well.
The mechanics are straightforward. During the TLS handshake, after the server presents its certificate, it sends a CertificateRequest message to the client. The client responds with its own certificate and a CertificateVerify message -- a signature over the handshake transcript using the private key corresponding to the certificate. The server validates the certificate chain and verifies the signature. If both checks pass, the connection is established with both sides cryptographically identified.
This gives you several things that are surprisingly hard to get any other way.
What mTLS Provides
Bidirectional identity at the transport layer. Both ends of the connection have proven possession of a private key. This happens before any application-layer data is exchanged. By the time your HTTP handler sees the request, the channel identity is already established.
No shared secrets. Unlike API keys or bearer tokens, no secret crosses the wire. The client proves identity by signing the handshake transcript with a private key that never leaves the client. An attacker who captures the entire TLS handshake cannot extract the private key from the transcript.
Replay resistance at the session level. The handshake transcript includes random nonces from both sides. A recorded handshake cannot be replayed because the server's nonce will be different. This is inherent to TLS and requires no additional application logic.
Channel binding. The TLS session has a unique binding value (the tls-exporter or tls-unique value) that can be referenced by higher-layer protocols. This is what allows mTLS to serve as a foundation for token binding -- a property we will return to when we discuss DPoP.
What mTLS Does Not Provide
The things mTLS lacks are not flaws in the protocol. They are boundaries. Understanding those boundaries is what separates a secure deployment from a deployment that feels secure.
No user identity. An mTLS certificate identifies a machine, a service account, or at most an organizational unit. It does not tell you which human initiated the request. If your service receives a request over a mutually authenticated TLS connection from a certificate with CN=payments-service, you know the payments service sent it. You do not know which engineer or which automated pipeline triggered that call. For service-to-service communication, this is exactly the right level of granularity. For user-facing flows, it is insufficient.
Revocation is operationally painful. Certificate revocation lists (CRLs) and the Online Certificate Status Protocol (OCSP) exist to handle compromised certificates. In practice, CRL distribution is slow and CRL checking is often disabled by clients for performance reasons. OCSP adds a synchronous network call to every TLS handshake, and OCSP stapling shifts that burden to the server but introduces its own caching and freshness problems. Short-lived certificates -- issued for hours rather than years -- are the modern answer, but they require a reliable automated issuance pipeline. If your CA infrastructure goes down and certificates cannot be renewed, every connection in your mesh fails.
Client certificate provisioning is hard. Getting a certificate onto a server is solved infrastructure. Getting a certificate onto a user's browser, phone, or laptop in a way that survives OS updates, device resets, and corporate MDM policies is an entirely different problem. This is why mTLS is dominant in service meshes and nearly absent in consumer-facing authentication. The provisioning story for end users is terrible.
No visibility into request content. mTLS authenticates the connection, not the request. Two different HTTP requests on the same TLS connection have the same channel identity. If the application layer issues a token after the first request, mTLS has no opinion about whether that token is used legitimately in the second request or stolen and used from a different connection.
mTLS in Practice: Service Meshes
The dominant deployment pattern for mTLS today is the service mesh. Istio, Linkerd, and Consul Connect all implement mTLS between sidecar proxies. The application code does not manage certificates directly. The sidecar handles certificate issuance, rotation, and the mTLS handshake. From the application's perspective, it receives plain HTTP on localhost. From the network's perspective, every hop is mutually authenticated.
This pattern works because it solves the provisioning problem by moving it to infrastructure. Certificates are issued by an internal CA (often SPIFFE-based), rotated automatically, and scoped to workload identity rather than machine identity. The application developer never touches a certificate. The platform team manages the CA.
But notice what this does not cover. The service mesh authenticates service-to-service calls. It does not authenticate the user who triggered the call chain. For that, you need something that understands humans.
WebAuthn: Authenticating the Human
WebAuthn -- formally the Web Authentication API, standardized as a W3C Recommendation and built on the FIDO2 specification -- is the first broadly deployed authentication protocol that eliminates shared secrets from the user authentication flow entirely.
The core insight is simple: instead of the user proving knowledge of a shared secret (a password), the user proves possession of a private key that never leaves their device. The relying party (your server) stores only the public key. There is no password database to breach. There is no credential to phish, because the private key is bound to the relying party's origin and will not be released to a different domain.
Registration and Assertion
WebAuthn has two ceremonies: registration (creating a credential) and assertion (using one).
During registration, the relying party sends a challenge -- a random byte string -- along with its relying party ID (typically the domain name) and parameters describing acceptable credential types. The user's authenticator generates a new asymmetric key pair, stores the private key internally, and returns the public key, a credential ID, and an attestation object that optionally proves the authenticator's make and model. The relying party stores the public key and credential ID.
During assertion, the relying party sends a new challenge and a list of acceptable credential IDs. The authenticator finds the matching credential, requires user verification (a PIN, biometric, or presence test depending on configuration), signs the challenge along with the relying party ID and other contextual data, and returns the signature. The relying party verifies the signature using the stored public key.
Authenticator Types
WebAuthn supports two categories of authenticator, and the distinction matters for your security model.
Platform authenticators are built into the device. The fingerprint sensor on a MacBook, Windows Hello, the secure enclave on an iPhone. The private key is bound to the hardware and cannot be extracted. This provides strong assurance that the registered device is present, but it means the credential cannot roam. If the user loses the device, the credential is gone.
Roaming authenticators are external hardware tokens -- YubiKeys, SoloKeys, Google Titan keys. They connect via USB, NFC, or BLE. The credential travels with the physical token. The user can authenticate from any machine by plugging in the token. The security boundary is now the physical security of a small piece of hardware rather than the security of an entire laptop or phone.
There is a third category emerging: synced passkeys. Apple, Google, and Microsoft all support synchronizing WebAuthn credentials across devices via their respective cloud platforms. This dramatically improves usability -- you register once and the credential appears on all your devices. It also changes the security model. The private key now exists in a cloud backup, protected by the vendor's account security. Whether this is acceptable depends on your threat model. For consumer applications, the usability gain is probably worth the tradeoff. For a system protecting nuclear launch codes, probably not.
What WebAuthn Lacks
WebAuthn solves the user authentication problem with an elegance that passwords never approached. But it creates a new problem at step 8 of the flow diagram above, and that problem is the reason this article exists.
The session token problem. After a successful WebAuthn assertion, the server issues a session token. That token is typically a cookie or a JWT. And that token is a bearer credential. Whoever holds it can use it. WebAuthn proved that the human was present at the moment of login. It says nothing about whether the human is present for subsequent requests. If an attacker steals the session cookie via XSS, a compromised CDN, or a malicious browser extension, they can use it from any device, any connection, any location. The strong authentication at the front door is undermined by a weak credential for the rest of the session.
No channel binding. The WebAuthn assertion is not bound to the TLS connection it was performed over. The signature covers the challenge, the rpId, and the client data hash. It does not cover the TLS session. This means a man-in-the-middle who terminates TLS (such as a corporate inspection proxy or a compromised CDN edge node) could theoretically relay the assertion to the real server and obtain a session token. The Level 3 WebAuthn specification is adding token binding extensions, but deployment is not yet widespread.
No API token binding. WebAuthn is designed for browser-based interactive login. It does not address the case of long-lived API tokens, refresh tokens, or access tokens issued by an OAuth2 authorization server. Once the authorization server issues an access token, that token is a bearer credential regardless of whether the user authenticated with WebAuthn or with a sticky note on their monitor.
DPoP: Binding Tokens to Key Pairs
Demonstration of Proof-of-Possession -- DPoP, defined in RFC 9449 -- was designed specifically to close the bearer token gap. The mechanism is straightforward: instead of presenting a token alone, the client also presents a proof that it holds the private key the token was bound to at issuance.
How DPoP Works
The protocol adds one step to token issuance and one step to token use.
At token issuance: The client generates an asymmetric key pair (typically ECDSA P-256 or Ed25519). When requesting a token from the authorization server, the client includes a DPoP proof -- a JWT signed with the private key, containing the public key in its header (the jwk claim), a unique identifier (jti), the target HTTP method and URL (htm and htu claims), and a timestamp (iat). The authorization server validates the proof, extracts the public key, and issues an access token that is cryptographically bound to that public key. The binding is typically implemented by including a hash of the public key in the token's cnf (confirmation) claim.
At token use: When the client makes an API request, it includes both the access token (in the Authorization header, using the DPoP scheme instead of Bearer) and a fresh DPoP proof JWT (in a DPoP header). The resource server validates the access token, extracts the expected public key hash from the cnf claim, validates the DPoP proof signature against that public key, and checks that the htm and htu claims match the actual request method and URL.
The result: an attacker who steals the access token cannot use it without also stealing the private key. And if the client stores the private key in a secure enclave, hardware token, or TPM, stealing the token alone is insufficient for impersonation.
DPoP Proof Structure
A DPoP proof is a compact JWT with a specific structure. The header contains the algorithm (alg), the type (typ, which must be "dpop+jwt"), and the public key (jwk). The payload contains the HTTP method (htm), the HTTP URI (htu), a unique token identifier (jti), the issued-at time (iat), and optionally an access token hash (ath) when the proof accompanies an access token.
The jti claim is critical for replay prevention. The authorization server and resource server must track recently seen jti values and reject duplicates within a time window. This is the same pattern used for JTI tracking in standard JWTs, but it is mandatory for DPoP rather than optional. Without jti tracking, an attacker who captures a DPoP proof and the access token could replay both within the proof's validity window.
The htm and htu claims bind the proof to a specific HTTP request. A DPoP proof generated for GET /api/accounts cannot be used for POST /api/transfers. This limits the damage from proof capture -- even if an attacker captures a proof, they can only replay it for the exact same endpoint with the exact same method, and only until the jti is rejected as a duplicate.
What DPoP Does Not Provide
No user authentication. DPoP binds a token to a key pair. It does not verify that the human behind the key pair is who they claim to be. A compromised device with a stolen DPoP key pair provides no protection. DPoP is an anti-theft mechanism for tokens, not an identity verification mechanism for humans.
No channel authentication. DPoP operates at the application layer. It does not authenticate the TLS connection, verify the server's identity, or provide any transport-layer guarantees. An attacker who can intercept traffic (via a rogue proxy or DNS hijack) could capture both the access token and the DPoP proof. The htm/htu binding limits what they can do with it, but it does not prevent interception.
Key management burden. Every client that uses DPoP needs to generate, store, and protect an asymmetric key pair. For browser-based clients, this means using the Web Crypto API and storing keys in IndexedDB or a platform secure enclave. For native mobile apps, it means using the Keychain (iOS) or Keystore (Android). For CLI tools and backend services, it means managing keys outside of the token lifecycle. This is not insurmountable, but it is a nontrivial addition to every client implementation.
Composing the Stack
Now we can see why these three protocols belong together. Each one covers a gap the others leave open.
mTLS authenticates the channel. It proves that both ends of the connection hold valid certificates. It provides channel binding values that higher layers can reference. It prevents machine impersonation and man-in-the-middle attacks at the transport layer. It does not know or care about humans.
WebAuthn authenticates the human. It proves that a specific person, verified by biometric or PIN, is present at the device. It is phishing-resistant because the credential is bound to the relying party's origin. It does not provide channel binding or token binding. After successful authentication, the session credential is a plain bearer token.
DPoP binds the token. It proves that the entity presenting an access token holds the private key the token was bound to at issuance. It prevents token theft from being sufficient for impersonation. It does not authenticate the human or the channel.
How the Layers Interact
In a fully composed deployment, a request travels through all three layers:
The TLS handshake establishes mutual authentication. The client presents its certificate. The server verifies the chain. Both sides contribute to a session with a unique channel binding value. At this point, the server knows it is talking to an authorized machine -- a specific browser instance, a specific mobile app installation, or a specific backend service.
The user authenticates via WebAuthn. The server issues a challenge. The authenticator signs it after user verification. The server verifies the signature against the stored public key. At this point, the server knows which human is present. The server issues an access token -- but instead of issuing a plain bearer token, it issues a DPoP-bound token.
Every subsequent API request carries a DPoP proof. The client signs a fresh proof for each request, binding it to the HTTP method, URL, and the access token hash. The server verifies the proof before processing the request. At this point, the server knows that the request comes from the same key pair that was present at token issuance.
The composition is not merely additive. Each layer strengthens the others. mTLS prevents the WebAuthn assertion from being relayed through a man-in-the-middle. DPoP prevents a stolen session token from being used outside the device that authenticated. WebAuthn ensures that the human who triggered the session was actually present and verified.
Certificate Lifecycle Management
The operational burden of this stack falls disproportionately on the mTLS layer. WebAuthn credentials are managed by the authenticator and the relying party, with no expiration to manage (though credential rotation policies are wise). DPoP key pairs are ephemeral or per-device, with lightweight management requirements. But mTLS certificates have issuers, expiration dates, revocation status, and chain validation requirements that demand active management.
Short-Lived Certificates
The modern answer to certificate lifecycle pain is to make certificates disposable. Instead of issuing a certificate valid for a year and worrying about revocation, issue one valid for 24 hours and do not bother with CRLs or OCSP at all. If a certificate is compromised, the window of exposure is bounded by the certificate's remaining lifetime.
This requires an automated issuance pipeline that can produce certificates on demand. SPIFFE (Secure Production Identity Framework For Everyone) and its reference implementation SPIRE are the most widely deployed solution in this space. SPIRE issues X.509 SVIDs (SPIFFE Verifiable Identity Documents) with configurable TTLs, typically in the range of 1 to 24 hours. The workload requests a new certificate before the current one expires. If the issuance infrastructure is down, the workload continues operating with its current certificate until it expires, at which point connections fail.
This failure mode is the tradeoff. Revocation complexity is replaced by availability requirements on the issuance infrastructure. In a well-operated service mesh, this is an acceptable trade. In environments where the CA must cross network boundaries or survive partitions, the availability requirement can be difficult to meet.
Certificate Rotation Without Downtime
Rotating a certificate means replacing the key pair and certificate that a service uses for mTLS while that service is actively handling connections. The naive approach -- replace the file and restart the process -- causes a brief outage. The production approach is one of two patterns.
Graceful reload. The server watches the certificate file or receives a signal (SIGHUP) and loads the new certificate for new connections while allowing existing connections to drain on the old certificate. Envoy, Caddy, and most modern proxies support this natively.
Dual certificate support. The server presents the new certificate to new clients but accepts the old certificate from clients that have not yet rotated. This requires the trust store to contain both the old and new CA certificates during the rotation window. Istio implements this as a configurable root CA rotation procedure.
The common mistake is testing rotation in a lab with one connection and declaring it production-ready. Real rotation issues appear under load, when connection pools cache TLS sessions, when DNS TTLs cause clients to hold stale addresses, and when clock skew causes certificate validity windows to disagree. Test rotation under realistic conditions or discover the failure mode in production.
Practical Deployment Sequence
If you are starting from a system that uses passwords and bearer tokens, you do not deploy all three layers simultaneously. The migration path matters. Here is a sequence that minimizes risk and maximizes the value of each step.
Phase 1: mTLS for Service-to-Service
Start with the infrastructure layer. Deploy a service mesh or configure mTLS directly between your backend services. This eliminates the largest class of lateral movement attacks -- an attacker who compromises one service and attempts to impersonate another on the internal network. mTLS requires a valid certificate, and without access to the certificate issuance pipeline, the attacker cannot obtain one.
This phase is entirely invisible to users. No UX changes. No frontend work. It is a pure infrastructure improvement that reduces your blast radius immediately.
Phase 2: WebAuthn for User Authentication
Replace password-based login with WebAuthn. Start with WebAuthn as a second factor alongside existing passwords. Once adoption reaches a threshold (typically 70-80% of active users have registered at least one credential), make WebAuthn the primary factor and relegate passwords to a recovery mechanism. Eventually, disable passwords entirely for users who have registered multiple credentials on multiple devices.
The critical implementation detail is recovery. What happens when a user loses all their authenticators? You need a recovery flow that does not undermine the security of WebAuthn. Common approaches include pre-registered recovery codes (printed and stored securely), a supervised in-person recovery process, or a time-delayed recovery via a verified secondary channel. Do not use email-based recovery as the sole fallback -- it reduces your authentication security to the security of the user's email account.
Phase 3: DPoP for Token Binding
Once WebAuthn is your primary authentication mechanism, add DPoP to your token issuance. This requires changes to the authorization server (to accept DPoP proofs and issue bound tokens), the resource server (to validate DPoP proofs on every request), and every client (to generate key pairs, sign proofs, and include them in requests).
The client changes are the bottleneck. Web clients need to use the Web Crypto API. Mobile clients need to use platform key storage. CLI tools need a local keystore. Each client type has different key management constraints. Roll out DPoP to one client type at a time, starting with the one that handles the most sensitive operations.
During the migration, support both Bearer and DPoP token types. Resource servers should accept both, logging which type each request uses. Set a deadline for Bearer deprecation and enforce it. Tokens presented as Bearer after the deadline are rejected.
Failure Modes and Fallback Design
Every authentication layer can fail. Your system needs to degrade gracefully rather than catastrophically when one does.
mTLS Failure
The most common mTLS failure is certificate expiration. The second most common is CA unavailability preventing issuance of new certificates. The third is a misconfigured trust store that rejects valid certificates.
For service-to-service calls, mTLS failure means the connection cannot be established. There is no graceful degradation -- falling back to unauthenticated connections would defeat the purpose. The correct response is to alert on certificate expiration well before it happens (days, not hours), to run the CA infrastructure with the same availability targets as the services it protects, and to ensure that trust store updates are tested in staging before production.
For user-facing connections where client certificates are used alongside WebAuthn, a more nuanced approach is possible. If the client certificate is unavailable (new device, corporate proxy stripping client certs), the server can still require WebAuthn and DPoP. The session proceeds with reduced channel assurance. Log this. Alert on it if the rate exceeds a threshold. But do not block the user entirely unless your threat model requires it.
WebAuthn Failure
WebAuthn fails when the user cannot produce a valid assertion. Common causes: the authenticator is lost or damaged, the browser does not support WebAuthn (rare but possible in embedded webviews), or the user verification mechanism fails repeatedly (wet fingers on a fingerprint sensor, for example).
The fallback must not be weaker than the primary. If your fallback is a password, an attacker will simply trigger the fallback. Acceptable fallbacks include a second registered authenticator (the reason you should require users to register at least two), a pre-generated recovery code (which is a one-time password and should be treated as such), or a supervised recovery process that requires out-of-band verification.
DPoP Failure
DPoP fails when the client cannot produce a valid proof. This happens when the client loses its key pair (cleared browser storage, app reinstall, factory reset), when clock skew causes the iat timestamp to be rejected, or when the jti tracking mechanism on the server incorrectly flags a legitimate proof as a replay.
For key pair loss, the correct response is to re-authenticate the user (via WebAuthn) and issue a new token bound to a new key pair. This is not a degradation -- it is a re-establishment of the full authentication chain. The user experiences it as a re-login, which is the appropriate response to losing the proof-of-possession key.
For clock skew, the specification recommends a grace period of a few seconds on the iat validation. In practice, 30 seconds is common. If your clients frequently exceed this, you have an NTP problem that needs to be fixed at the infrastructure level, not papered over with a larger grace period.
For false replay detection, the root cause is usually a jti tracking store that is too small or too aggressive in its expiration. Size the store for your traffic volume. Use a probabilistic data structure (a Bloom filter with a time-based rotation) if exact tracking is too expensive.
What This Stack Does Not Solve
Completeness demands honesty about the boundaries of this architecture. Three things this stack does not address:
Compromised endpoints. If the user's device is fully compromised -- a kernel-level rootkit, a malicious hypervisor, a supply-chain-attacked OS update -- then the attacker has access to the mTLS private key (if it is software-stored), can intercept the WebAuthn assertion, and can use the DPoP key pair. Hardware-backed keys (TPM, secure enclave) raise the bar significantly but do not eliminate the risk entirely. Device attestation is an active area of research but not a solved problem.
Insider threats with legitimate credentials. An authorized user with a valid certificate, a registered authenticator, and a bound token is indistinguishable from an attacker using those same credentials. Authentication verifies identity. It does not verify intent. Authorization, behavioral analytics, and access logging are the tools for insider threats, not stronger authentication.
Protocol downgrade attacks. If an attacker can force a client to fall back from DPoP to Bearer, or from mTLS to plain TLS, the layered protection collapses. Strict enforcement -- rejecting requests that do not meet the minimum authentication standard -- is the only defense. This must be configured at the server, not negotiated by the client. The server defines the minimum acceptable authentication for each endpoint, and requests that do not meet it are rejected regardless of what the client offers.
Implementation Notes
Authorization Server Changes
Your OAuth2 authorization server needs three additions for DPoP. First, it must accept a DPoP header on the token endpoint and validate the proof before issuing a token. Second, it must include the cnf claim (with a jkt member containing the JWK thumbprint of the client's public key) in the access token. Third, it must support the DPoP token type in token introspection responses so that resource servers can determine the expected key binding.
If you use an off-the-shelf authorization server, check DPoP support carefully. As of early 2026, Keycloak supports DPoP natively. Auth0 supports it in some configurations. Many smaller providers do not support it at all. If you are building a custom authorization server, the specification is clear enough to implement directly, but the jti tracking component needs careful attention to performance under load.
Resource Server Changes
Every protected endpoint must validate the DPoP proof in addition to the access token. This means extracting the DPoP header, validating the JWT signature, checking the htm and htu claims against the actual request, validating the iat against the server clock (with grace period), checking the jti against the replay cache, extracting the public key from the proof, computing the JWK thumbprint, and comparing it against the cnf.jkt claim in the access token.
This is five to ten milliseconds of additional computation per request, depending on the signature algorithm. For most services, this is negligible. For high-throughput, low-latency services (payment processing, real-time bidding), it may require careful benchmarking. ECDSA P-256 verification is faster than RSA 2048 verification. Ed25519 is faster still.
Client-Side Key Storage
The security of DPoP depends entirely on the security of the client's private key. The spectrum of options, from strongest to weakest:
Hardware-backed keys (TPM, Secure Enclave, hardware token). The private key is generated inside the hardware module and never leaves it. Signing operations are performed by the hardware. Even a fully compromised OS cannot extract the key. This is the gold standard but is not available in all environments.
OS-managed keystores (Keychain, Credential Manager, Android Keystore). The key is stored in an OS-managed secure area, protected by the user's device credentials. A compromised application cannot access it, but a compromised OS can. This is the practical choice for most mobile and desktop applications.
Web Crypto API with non-extractable keys. The browser generates a key pair with the extractable property set to false. The key exists in the browser's memory and IndexedDB but cannot be exported via JavaScript. A compromised browser or extension could still access it in memory, but a script injection (XSS) in the application cannot extract it. This is the best available option for browser-based clients.
Software keys in application storage. The private key is stored in a file, environment variable, or application database. Any process with read access can extract it. This is the weakest option and should only be used for development and testing.
Closing Observations
Authentication engineering is not about finding the one protocol that solves everything. It is about understanding what each protocol was designed to protect against, what it was not designed to protect against, and how the gaps in one are covered by the strengths of another.
mTLS, WebAuthn, and DPoP are not competitors. They operate at different layers of the stack, protect against different threat classes, and compose cleanly because their boundaries do not overlap. You do not choose between them. You deploy them together, in the order that maximizes security value at each step, and you design your fallback modes so that failure in one layer does not cascade into failure of the entire stack.
The engineering effort is real. Certificate lifecycle management, authenticator provisioning, DPoP proof generation, jti tracking at scale -- none of this is free. But the alternative -- bearer tokens that can be stolen, passwords that can be phished, and service calls that can be impersonated -- is not free either. It just defers the cost until the breach.
Start with the layer that reduces the most risk for the least effort in your specific environment. For most organizations, that is mTLS for service-to-service and WebAuthn for user login. DPoP comes third not because it is less important but because it requires changes to every client, and client changes are operationally expensive. But get all three deployed, and you have an authentication stack where stealing a token, phishing a credential, or impersonating a service each require compromising a different cryptographic key held by a different component in a different layer of the system. That is not invulnerable. But it is defensible.