AXIAM PHP SDK

DpopVerifier
in package

FinalYes

DPoP proof verification — CONTRACT.md §21.7.2 (RFC 9449), contract 1.16.

The resource-server half of DPoP: given the DPoP header a caller presented, decide whether it proves possession for this request and this access token, and return the key thumbprint that JwksVerifier::verifyTokenBinding() then matches against the token's cnf.jkt.

Why this lives in the SDK

§21.7.2 is a ten-check list, and the contract is blunt about partial implementations: "Partial verification is worse than none, because it produces a guard that reports success." Nine of the ten look optional until someone builds an attack out of the one that was skipped, so they belong in one audited place rather than in every application that guards an endpoint.

The two most often missing, and what they cost:

  • typ — without pinning it to dpop+jwt, any other JWT signed by the same key (an access token, an ID token) is replayable as a proof.
  • ath — without it, a proof captured on one request can be re-aimed at a different token held by the same key. ath binds the proof to the token rather than merely to the key.

The algorithm comes from the key, never from the header

alg: none and RSA-public-key-as-HMAC-secret are the same bug wearing different clothes: the token told the verifier how to check the token. This class derives the expected algorithm from the embedded key's own kty/crv and hands that one algorithm to the decoder, so an HMAC verifier is never a candidate.

firebase/php-jwt additionally refuses a header whose alg disagrees with the key it was given, so a lying alg header is rejected here rather than ignored — the same posture as the Rust SDK, and stricter than Python's and TypeScript's. Either way the header never selects the algorithm.

PS256 needs phpseclib

firebase/php-jwt implements RSASSA-PSS through phpseclib/phpseclib, because PHP's own openssl_verify() has no PSS padding. phpseclib is already in this SDK's dependency graph, so all three permitted algorithms work out of the box; on an installation where it is somehow absent, a PS256 proof fails closed (rejected) rather than being waved through.

Table of Contents

Constants

IAT_LEEWAY_SECONDS  : mixed = 60
§21.7.2 check 7 — the `iat` acceptance window, applied in **both** directions.

Methods

accessTokenHash()  : string
Compute the `ath` claim value for an access token — RFC 9449 §4.2.
canonicalHtu()  : string
Reduce a URI to its `htu` comparison form — §21.7.2 check 6.
thumbprintS256()  : string
Compute the RFC 7638 SHA-256 thumbprint of a JWK — the `jkt`.
verifyProof()  : string
Verify a DPoP proof against this request — all ten §21.7.2 checks.

Constants

IAT_LEEWAY_SECONDS

§21.7.2 check 7 — the `iat` acceptance window, applied in **both** directions.

public mixed IAT_LEEWAY_SECONDS = 60

RFC 9449 recommends a small window without fixing a number; 60 seconds is the contract's RECOMMENDED value. A named constant, because a bare 60 three call frames deep is a number nobody ever revisits.

Methods

accessTokenHash()

Compute the `ath` claim value for an access token — RFC 9449 §4.2.

public static accessTokenHash(string $accessToken) : string

base64url-unpadded SHA-256 over the token's bytes exactly as they travelled in the Authorization header, not over anything decoded out of them.

Parameters
$accessToken : string

The token as it arrived.

Return values
string

The 43-character base64url hash.

canonicalHtu()

Reduce a URI to its `htu` comparison form — §21.7.2 check 6.

public static canonicalHtu(string $uri) : string

Query and fragment removed, and nothing else. No case folding, no default-port elision, no percent-decoding, no trailing-slash fixing: a normalising comparison is precisely where two unequal URIs become equal, and an attacker who finds such a pair can aim a proof at an endpoint it was never minted for.

Parameters
$uri : string

The URI to reduce.

Return values
string

The same URI without its query string or fragment.

thumbprintS256()

Compute the RFC 7638 SHA-256 thumbprint of a JWK — the `jkt`.

public static thumbprintS256(array<string, mixed> $jwk) : string

Only the members RFC 7638 names for the key type take part, serialised as compact JSON with lexicographically ordered keys. Members outside that set (kid, use, alg, x5c) are excluded by the spec, which is what makes the thumbprint stable across two encodings of the same key.

Parameters
$jwk : array<string, mixed>

The public key to fingerprint.

Tags
throws
AuthError

If the key type is unsupported or a required member is missing.

Return values
string

The 43-character base64url thumbprint.

verifyProof()

Verify a DPoP proof against this request — all ten §21.7.2 checks.

public static verifyProof(string $proof, DpopRequest $request, JtiStore $store) : string

Returns the proof key's RFC 7638 thumbprint (jkt) on success. Feed it to JwksVerifier::verifyTokenBinding() as the DPoP half of PresentedProofs; returning it rather than true is deliberate, so the value a guard passes onward could only have come from a proof that actually verified.

Parameters
$proof : string

The raw DPoP header value.

$request : DpopRequest

The method, URI and access token this proof must match.

$store : JtiStore

The replay guard. Required — there is no default, because every default here is either a silent skip of replay protection or a per-process store masquerading as a global one.

Tags
throws
AuthError

On any failing check.

Return values
string

The proof key's jkt.

On this page

Search results