DPoP

OAuth 2.0 Demonstrating Proof-of-Possession

Demonstrating Proof-of-Possession (DPoP) is a protocol-level security mechanism defined in RFC 9449.

It ensures that an access token alone is no longer sufficient to access a resource server. Instead, each request must also include a cryptographic proof showing possession of a private key held by the client.

As a result, access tokens become much less sensitive: if a token leaks, it cannot be replayed from another device or context without the corresponding private key.

DPoP is supported by Keycloak and an increasing number of other identity providers and resource server stacks.


Enabling DPoP

oidc-spa exposes a single configuration option to control DPoP behavior:

  • "auto": enable DPoP only if supported by the authorization server, otherwise fall back to classic Bearer tokens (Recommended)

  • "disabled": never use DPoP (default)

  • "enabled": require DPoP support; oidc-spa will refuse to start if the authorization server does not support it. See support history in Keycloak.

src/oidc.ts
createOidc({
    // ...
    dpop: "auto"
});

What does enabling DPoP require?

Enabling DPoP in oidc-spa does not require changes elsewhere in your stack:

  • Identity Provider (Keycloak or other) No configuration change is required. With dpop: "auto", If the authorization server supports DPoP, oidc-spa will detect and use it.

  • Frontend codebase No changes are required. Authenticated requests continue to use Authorization: Bearer <access_token> and are automatically upgraded at runtime.

  • Backend API / resource server No changes are required. Even if you are not using oidc-spa/server, a correct implementation of OAuth 2.0 token validation will reject DPoP-bound access tokens when the corresponding DPoP proof is missing or invalid.

In other words, this configuration option is the only change required to securly enable DPoP support in your all stack.

How it works

When DPoP is enabled, oidc-spa automatically upgrades authenticated HTTP requests sent by your application.

You continue sending requests as usual:

At runtime, oidc-spa transparently transforms the request into:

In addition, oidc-spa automatically:

  • tracks and reuses DPoP nonces issued by resource servers

  • retries requests when a nonce is required

To achieve this transparently, oidc-spa installs fetch() and XMLHttpRequest interceptors:

  • via the Vite plugin, or

  • during the execution of oidcEarlyInit()

From your application’s point of view, nothing changes: you keep using Authorization: Bearer <access_token>, and oidc-spa handles DPoP internally.

Last updated

Was this helpful?