SSOGEN / Free tools / DPoP Proof Checker: Test a DPoP Proof in Your Browser

Free tool

DPoP Proof Checker: Test a DPoP Proof in Your Browser

A DPoP proof is a small signed JWT the client sends in the DPoP request header with every call. This free checker runs the checks a resource server runs, in your browser, with nothing uploaded.

Check a proof here

Paste the proof, add the access token, method and URL if you have them, then select Check the proof.

Private by design. The checks run in this page and nothing is sent anywhere. A DPoP proof holds only a public key, but a real access token is a credential: do not paste a live production token on a shared screen.

Free, in your browser. Decoding, hashing and signature checks use your browser’s built-in cryptography. Nothing is stored or uploaded.

If the proof’s jwk contains a private key, the checker flags it and never uses it.

The steps in order:

  1. Paste the proof (the value of the DPoP header).
  2. To check the key binding and the ath claim, also paste the access token it was sent with.
  3. Enter the HTTP method and URL of the request, and the server nonce if the server issued one.
  4. Select Check the proof.

What a DPoP proof is

DPoP proof. A DPoP proof is a JWT the client signs with its own private key and sends in the DPoP request header on every call, alongside its access token.

What it asserts. It asserts “this exact request, to this exact URL, right now, from the holder of this key”.

The checker. The checker above decodes a proof and runs the checks a resource server runs. It is free, needs no sign-up and makes no network request.

Try it: a good proof, then a stolen token

Each sample button generates fresh keys in your browser, so every run shows new values.

Load a matching token and proof. The access token is bound to a key, and the proof is signed with that same key.

Select Check the proof and every line passes:

  • typ is dpop+jwt and the algorithm is asymmetric.
  • The signature verifies with the public key in the header.
  • htm and htu match the request fields, iat is recent and jti is present.
  • ath equals the SHA-256 hash of the token.
  • The token’s cnf.jkt equals the thumbprint of the proof’s key.

Load a stolen-token replay. The access token is the same, but the proof is signed by a different key.

That is the only option a thief has: they hold the token, not the owner’s private key. Note which lines fail and which pass.

The signature check still passes, because the proof is genuinely signed by the key in its own header; that proves only that the sender holds some key.

The key-binding check fails: the token is bound to one thumbprint, and the proof was signed with another.

A server that verifies the signature but skips this comparison accepts the thief. The comparison is what gives DPoP its value.

What the proof claims mean

RFC 9449, section 4.2 defines the proof’s header and claims (read 2026-10-08), and each one closes a specific gap. For the full background, read the DPoP explainer.

Field What it holds Attack it stops
typ dpop+jwt in the header An ordinary JWT, such as an ID token, being passed off as a proof
alg An asymmetric algorithm, never none or a shared-secret one A forged proof signed with none or with a secret the attacker can guess
jwk The client’s public key, with no private part Gives the server the key to verify with, and the thumbprint to compare with the token
htm The HTTP method A proof captured on a GET being used for a DELETE
htu The request URL, without query and fragment A proof captured for one endpoint being used at another
iat When the proof was made An old captured proof being used long after
jti A unique id, at least 96 bits of randomness per the RFC The same proof being sent twice, if the server remembers recent ids
ath Base64url SHA-256 of the access token A proof made for one token being attached to a different token
nonce A value the server issued Proofs signed in advance with future iat values (section 11.2)

The checks in the order a server runs them

Section 4.3 lists what a server must verify, and section 7 adds the access-token checks. The tool runs the same sequence:

  1. One DPoP header, and it is a well-formed JWT with the required claims present.
  2. typ is dpop+jwt.
  3. alg is asymmetric and one your server accepts.
  4. jwk is a public key with no private material. A private key in a proof means that key is exposed and must be retired.
  5. The signature verifies with the key in jwk.
  6. htm equals the request method.
  7. htu equals the request URL, ignoring query and fragment.
  8. iat is inside your acceptance window.
  9. jti is new within that window.
  10. nonce matches the value you issued, if you issue them.
  11. ath equals the hash of the access token in the Authorization header.
  12. The token’s binding matches the proof’s key. In a JWT access token that is the cnf.jkt claim; for an opaque token, the introspection response carries it.

The order keeps cost down. Cheap string comparisons can reject a request before the signature is verified, and the replay lookup goes last because it touches storage.

The RFC does not rank the checks, so this order is our recommendation.

htu and htm mistakes

Most real failures are not attacks: the client built a proof for one URL and the server sees another.

The checker parses the URL and compares scheme, host and path. It ignores host capitalization and a default port, and it flags a trailing slash.

Symptom Likely cause Fix
htu does not match, and it looks the same A reverse proxy or load balancer rewrote the host or scheme, so the app sees http://internal-host/... Validate against the public URL the client called. Build it from configuration, or from trusted forwarded headers, never from the raw connection
Mismatch only on some endpoints The proxy strips or adds a path prefix Compare against the external path, prefix included
/invoices versus /invoices/ A trailing slash is a different path Make the client sign exactly the URL it requests, and redirect or normalize on the server consistently
Proof includes ?page=2 The client put the query in htu Sign the URL without query and fragment, as section 4.2 requires
Port in one place, not the other :443 on an https URL is the default and the same address Normalize before comparing; RFC 3986 scheme-based normalization covers this (section 4.3 recommends it)
htm mismatch after a redirect The client re-sent a POST as a GET, or the reverse Create a new proof for the redirected request; each request needs its own

Clock and nonce problems

iat. RFC 9449 leaves the acceptance window to the server. Section 11.1 advises allowing a little time in the future for clock skew and keeping the total lifetime short.

This tool’s window. This tool accepts a proof within five minutes either side of your computer’s clock, a common choice rather than an RFC rule.

Clock check. If the checker reports a proof as in the future or very old, check the clock on the machine that created it first.

Nonces. A server can require a nonce so that proofs cannot be signed in advance. The two servers signal it differently:

  • The authorization server responds with HTTP 400, the error use_dpop_nonce and a DPoP-Nonce header (section 8).
  • A resource server responds with a 401, a WWW-Authenticate: DPoP challenge and the same header (section 9).

The client repeats the request with that value in the proof’s nonce claim.

To test this, paste the issued value into the checker’s nonce box. A nonce error on the first call is expected, not a failure; retry once with the new value.

Once a server issues a nonce, it must not accept proofs without one (section 11.3).

What a checker cannot do

  • It cannot remember jti values. To detect a repeated proof, the server must store recent ids for the length of its window (section 11.1). The checker confirms that a jti exists, not that it is unused.
  • It cannot see your server’s policy. Your algorithms, window and nonce rules are yours to set.
  • It does not validate the access token’s own signature. It reads the token’s cnf.jkt to test the binding. To verify the token itself, use the JWT decoder with the issuer’s keys from the JWKS inspector.
  • It cannot read an opaque token. The binding of an opaque token is in your authorization server’s introspection response, so the checker reports that it could not read it.
  • It does not prove a token is safe to trust. It tests a proof against a request you describe; a real server must do the same on the real request.

What SSOGEN does with DPoP-bound agent credentials

SSOGEN requires AI agents to prove possession of a key on every request:

  • The agent sends Authorization: DPoP <access token> plus a fresh proof signed with the key the token is bound to. A Bearer token is refused.
  • The access token lives about four minutes, never more than 270 seconds.
  • An agent acting on its own receives no refresh token.

More on how SSOGEN handles it:

  • Nonce. SSOGEN can answer a token request with use_dpop_nonce and a DPoP-Nonce header, and the agent repeats the request with the nonce in its proof.
  • Acting for a person. A client acting for a person also sends its DPoP key’s thumbprint as dpop_jkt in its pushed authorization request.
  • Discovery. The realm’s discovery document lists the accepted proof algorithms in dpop_signing_alg_values_supported.
  • MCP route. On a protected MCP route, the MCP Gateway answers an unauthenticated request with WWW-Authenticate: DPoP and resource metadata.
  • Every tool call. It then checks the credential and the key proof on every tool call, and it does not forward the agent’s token to your MCP server.
  • Setup. The exact requests are in the setup guide for MCP authorization.
  • Tool choice. To choose which tools an agent should hold, use the AI agent permission builder.
What is a DPoP proof?
A JWT the client signs with its private key and sends in the DPoP header. It names the request’s method and URL, carries a unique id and a timestamp, and, when sent with a token, a hash of that token. The server uses it to confirm that the sender holds the key the access token is bound to.
What does the DPoP header look like?
One header named DPoP, whose value is a compact JWT of three base64url parts. The access token travels separately in Authorization: DPoP <token>. A bound token uses the DPoP scheme, not Bearer.
Is a DPoP token different from a bearer token?
Yes. A DPoP-bound access token is tied to a client key through the cnf.jkt thumbprint. A copy is useless without the matching private key, because every request needs a proof signed with it.
Why does my proof fail with invalid_dpop_proof?
Run it through the checker with the real method and URL. Common causes: an htu that differs from the public URL, a clock off by more than your window, a missing ath, or a proof that was reused or built for a different token.
Can I paste a production token?
Do not. The tool sends nothing, but a live token is a credential. Use a test or expired one.

Where to go next

Sources, all read 2026-10-09: RFC 9449, OAuth 2.0 Demonstrating Proof of Possession.