From 5c91bcff93b583b134f4f61fb74d6b9dc5ef5b64 Mon Sep 17 00:00:00 2001 From: LeonMAG Date: Mon, 12 Oct 2026 05:08:47 +0200 Subject: [PATCH] docs(client): document DPoP client authentication --- docs/clients/middleware.md | 2 ++ docs/clients/oauth.md | 22 ++++++++++++++++++++++ examples/guides/clients/oauth.examples.ts | 15 +++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/docs/clients/middleware.md b/docs/clients/middleware.md index 2904850fea..c635dae864 100644 --- a/docs/clients/middleware.md +++ b/docs/clients/middleware.md @@ -95,6 +95,8 @@ const authed = new StreamableHTTPClientTransport(serverUrl, { `provider` is the same `OAuthClientProvider` you would hand to the transport directly. With `statusLevel: 400`, `withLogging` stays silent until a request fails. +When the provider implements `dpop()`, `withOAuth` already composes DPoP request signing and nonce handling for DPoP-bound access tokens. Do not wrap the same `fetch` again with `withDpop`. See [Enable DPoP](./oauth.md#enable-dpop) for the provider setup; use `withDpop` directly only when you manage the bound token and signing session yourself. + ::: tip For the common case, pass `authProvider` to the transport instead — see [OAuth](./oauth.md). `withOAuth` is for stacks that already own `fetch` and need auth composed with other layers. ::: diff --git a/docs/clients/oauth.md b/docs/clients/oauth.md index f713c2e04d..7b365ea66d 100644 --- a/docs/clients/oauth.md +++ b/docs/clients/oauth.md @@ -97,6 +97,28 @@ class MyOAuthProvider implements OAuthClientProvider { The SDK calls the `save*` methods as the flow produces values and reads them back through `tokens()`, `clientInformation()`, `codeVerifier()`, and `discoveryState()`. On a later `connect()` it reads `tokens()` before anything else, so a provider backed by durable storage skips the browser round trip. +## Enable DPoP + +DPoP (Demonstrating Proof of Possession) binds an OAuth access token to a signing key held by the client. To opt in, return a `DpopSession` from your provider's `dpop()` method: + +```ts source="../../examples/guides/clients/oauth.examples.ts#DpopOAuthProvider_class" +class DpopOAuthProvider extends MyOAuthProvider { + private readonly dpopSession = DpopSession.create(); + + dpop() { + return this.dpopSession; + } +} +``` + +Use `new DpopOAuthProvider()` in place of `new MyOAuthProvider()` in the connection example, and keep that provider for the callback and reconnect. `DpopSession.create()` returns a promise; storing it once keeps the same signing key and nonce state for the lifetime of the client registration. + +The SDK signs token requests with DPoP proofs. When the authorization server issues `token_type: "DPoP"`, the transports present the access token with the `DPoP` authorization scheme and a fresh proof for each request. A Bearer token still uses the Bearer scheme. Neither the transports nor `withOAuth` need an additional `withDpop` wrapper; see [Client middleware](./middleware.md#combine-middleware-with-an-auth-provider). + +::: warning Persist the signing key with the tokens +This example keeps both tokens and the DPoP session in memory. If your application persists DPoP-bound tokens across restarts, preserve their signing key in secure storage too, or discard those tokens and authorize again. A new `DpopSession.create()` generates a new key; it cannot prove possession of the key that existing tokens are bound to. The default private key is non-extractable, so persisting only the tokens is not sufficient. +::: + ## Finish the flow from the callback The authorization server redirects the end user to `redirectUrl` with `code` and `state` in the query. Compare `state`, hand the whole query to `finishAuth`, and reconnect. diff --git a/examples/guides/clients/oauth.examples.ts b/examples/guides/clients/oauth.examples.ts index e740c45d7d..ffeee06195 100644 --- a/examples/guides/clients/oauth.examples.ts +++ b/examples/guides/clients/oauth.examples.ts @@ -23,6 +23,7 @@ import type { import { checkResourceAllowed, Client, + DpopSession, IssuerMismatchError, resourceUrlFromServerUrl, StreamableHTTPClientTransport, @@ -98,6 +99,20 @@ class MyOAuthProvider implements OAuthClientProvider { } //#endregion MyOAuthProvider_class +// --------------------------------------------------------------------------- +// "Enable DPoP" +// --------------------------------------------------------------------------- + +//#region DpopOAuthProvider_class +class DpopOAuthProvider extends MyOAuthProvider { + private readonly dpopSession = DpopSession.create(); + + dpop() { + return this.dpopSession; + } +} +//#endregion DpopOAuthProvider_class + // --------------------------------------------------------------------------- // "Hand the transport an OAuth provider" // ---------------------------------------------------------------------------