Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.flexslot.gg/llms.txt

Use this file to discover all available pages before exploring further.

DPoP (Demonstrating Proof-of-Possession, RFC 9449) cryptographically binds an access token to a public/private keypair your client holds. A stolen DPoP-bound token is useless to an attacker without the matching private key — every API call requires a freshly-signed proof JWT. This page is for integrations where bearer tokens aren’t enough.

Should you use DPoP?

SituationUse DPoP?
Server-side app handling its own tokensOptional. Bearer is fine if your server is secure.
Public client (SPA, mobile) handling high-value scopesYes, strongly recommended.
Server-side app where token leakage is a known concern (shared logging, third-party error trackers)Yes.
Any client with decks:write, sideboards:write, guides:write and a security mandateYes.
Internal scripts using a PATNo, doesn’t apply
You’re not sureStart with Bearer. Add DPoP when you ship to production scale.
DPoP is opt-in at Flexslot. Set dpop_bound_access_tokens: true on your OAuth client in the partner admin to require DPoP for all tokens issued to that client.

How DPoP works

The key insight: every single API request gets its own short-lived proof JWT, signed by the client’s private key, that names the URL, method, and the hash of the access token. An attacker who steals the access token can’t make the proof.

Set up your keypair

DPoP supports ES256 (ECDSA P-256), ES384, EdDSA, and RS256. Use ES256 unless you have a reason not to.
import { generateKeyPair, exportJWK } from 'jose'

const { publicKey, privateKey } = await generateKeyPair('ES256')

const publicJwk = await exportJWK(publicKey)
publicJwk.alg = 'ES256'
publicJwk.use = 'sig'
Persist this keypair securely. The same key must be used across /token and all subsequent API calls.

Building a DPoP proof JWT

A DPoP proof is a JWT with: Header:
{
  "typ": "dpop+jwt",
  "alg": "ES256",
  "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}
Payload:
{
  "jti": "01HX2K6F8N9P-unique-per-proof",
  "htm": "POST",
  "htu": "https://api.flexslot.gg/api/public/v1/oauth/token",
  "iat": 1717177200
}
When the proof accompanies an access token (i.e., when calling a resource server endpoint), also include:
{
  "ath": "BASE64URL(SHA256(access_token))"
}

Helper

import { SignJWT, exportJWK } from 'jose'
import crypto from 'node:crypto'

async function dpopProof({ privateKey, publicKey, method, url, accessToken }) {
  const jwk = await exportJWK(publicKey)

  const payload = {
    jti: crypto.randomUUID(),
    htm: method.toUpperCase(),
    htu: url,
    iat: Math.floor(Date.now() / 1000),
  }

  if (accessToken) {
    const ath = crypto.createHash('sha256').update(accessToken).digest('base64url')
    payload.ath = ath
  }

  return new SignJWT(payload)
    .setProtectedHeader({ typ: 'dpop+jwt', alg: 'ES256', jwk })
    .sign(privateKey)
}

Token exchange with DPoP

Add a DPoP header to the /token request:
DPOP_PROOF=$(node make-proof.js POST https://api.flexslot.gg/api/public/v1/oauth/token)

curl -X POST https://api.flexslot.gg/api/public/v1/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "DPoP: $DPOP_PROOF" \
  -d "grant_type=authorization_code" \
  -d "code=$CODE" \
  -d "redirect_uri=$REDIRECT_URI" \
  -d "code_verifier=$VERIFIER"
The response now has "token_type": "DPoP" and a cnf claim with the thumbprint of your key:
{
  "access_token": "fst_at_dpop_01HX2K…",
  "token_type": "DPoP",
  "expires_in": 3600,
  "refresh_token": "fst_rt_01HX2K…",
  "scope": "decks:read",
  "cnf": {
    "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
  }
}
cnf.jkt is the JWK Thumbprint of your public key. The resource server checks that every incoming request’s DPoP proof was signed by the key that hashes to this thumbprint.

Calling the resource server with DPoP

Two changes to every API call:
  1. The Authorization scheme is DPoP, not Bearer.
  2. Add a DPoP header with a fresh proof that includes ath.
DPOP_PROOF=$(node make-proof.js GET https://api.flexslot.gg/api/public/v1/decks/ "$ACCESS_TOKEN")

curl https://api.flexslot.gg/api/public/v1/decks/ \
  -H "Authorization: DPoP $ACCESS_TOKEN" \
  -H "DPoP: $DPOP_PROOF"
Every request needs a new DPoP proof. The jti must be unique (the server rejects replays) and the htm/htu must match the request. You can’t reuse proofs.

Refreshing DPoP-bound tokens

Refresh tokens for a DPoP client are also bound to the key. The /token request to refresh must include a DPoP proof signed by the same key as the original token exchange.
DPOP_PROOF=$(node make-proof.js POST https://api.flexslot.gg/api/public/v1/oauth/token)

curl -X POST https://api.flexslot.gg/api/public/v1/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "DPoP: $DPOP_PROOF" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=$REFRESH_TOKEN"
If your key is lost, the refresh token is useless and the user must re-consent. Keep your key safe and back it up.

DPoP errors

ErrorHTTPMeaning
invalid_dpop_proof400 / 401Proof JWT is malformed, signature doesn’t verify, or required claims missing
invalid_token with WWW-Authenticate: DPoP401Token is bound to a different key than the one signing the proof
use_dpop_nonce401Server is enforcing a nonce; retry with nonce claim (see below)

Nonce mode

Flexslot may issue a DPoP-Nonce: <value> header at any time. When it does, your next DPoP proof must include the nonce as a top-level claim:
{
  "jti": "...",
  "htm": "GET",
  "htu": "...",
  "iat": ...,
  "nonce": "the-nonce-from-the-header",
  "ath": "..."
}
This defends against pre-computed proofs and helps Flexslot tighten the proof-acceptance window. If you see use_dpop_nonce, read the DPoP-Nonce response header and retry.

Trade-offs

ProCon
Stolen token alone is worthlessEvery request needs cryptographic signing — measurable CPU cost
Refresh token replay defense is built-inKey management becomes part of your ops
Provable client identity at the RSMore complex to debug — DPoP proofs are JWTs you’ll squint at
Per-request audit (jti)Requires backend support — many SDK paths don’t have DPoP yet

When NOT to use DPoP

  • For very low-value reads where the convenience of Bearer outweighs the marginal risk.
  • For machine-to-machine flows where mTLS (RFC 8705) is a better fit (continuous mutual auth, not per-request).
  • For environments where your TLS terminator strips the DPoP header — fix the infra first.

Reference

Build DPoP support after you have a working Bearer flow. Don’t try to debug PKCE and DPoP at the same time — flip on dpop_bound_access_tokens in partner admin only once everything else is green.