DpopAuthProvider

class DpopAuthProvider(session: OAuthSession, signer: DpopSigner, sessionStore: OAuthSessionStore, refreshClient: HttpClient, json: Json = Json { ignoreUnknownKeys = true }, onPersistFailure: (Throwable) -> Unit = {}) : AuthProvider

AuthProvider implementation that attaches DPoP proof-of-possession headers on every XRPC request and handles token refresh transparently.

On each request, produces:

  • Authorization: DPoP <access_token>

  • DPoP: <signed-jwt-proof> (with ath, htm, htu, nonce)

When the PDS responds with HTTP 401:

  • If DPoP-Nonce header is present → stores the nonce, retries

  • If the access token is expired → refreshes via the token endpoint with the DPoP-bound refresh token, retries

  • If the refresh token is revoked (error=invalid_grant) → clears the session, throws OAuthSessionExpiredException

  • Otherwise (network error, 5xx/429, unparseable/captive-portal body, or any non-invalid_grant error) → throws the retryable OAuthRefreshFailedException and LEAVES THE SESSION INTACT, so a flaky connection can't sign the user out

Refresh operations are serialized with a Mutex to prevent concurrent refreshes from invalidating the session.

Constructors

Link copied to clipboard
constructor(session: OAuthSession, signer: DpopSigner, sessionStore: OAuthSessionStore, refreshClient: HttpClient, json: Json = Json { ignoreUnknownKeys = true }, onPersistFailure: (Throwable) -> Unit = {})

Functions

Link copied to clipboard
open suspend override fun authHeaders(method: String, url: String): Map<String, String>
Link copied to clipboard
open suspend override fun onUnauthorized(responseHeaders: Map<String, String>): Boolean

Called by XrpcClient on HTTP 401. Recovers every recoverable cause in one call so the single retry that XrpcClient performs always carries fresh state. Control flow: