A Solution to Rampant Token Theft: Proof of Possession

bhouston1 pts0 comments

A Solution to Rampant Token Theft: Proof of PossessionTL;DR: Bind every API token to a non-exportable signing key, require a fresh short-lived signature on every request, and a stolen credential string becomes useless. This is basically OAuth's DPoP, made stricter for developer tokens.

Static API keys are obsolete. Not because providers are careless or developers are bad at handling secrets, but because the operating environment changed. Developer laptops, CI runners, build scripts, package installs, and editor extensions are now active security boundaries, and most API credentials were designed for a simpler world where a secret string in an environment variable was good enough.

The recent run of credential-stealing supply-chain compromises makes this clear. The Axios npm compromise delivered cross-platform malware during package installation. The TanStack npm compromise abused CI behavior to publish malicious packages that exfiltrated credentials from install hosts. The Red Hat Consulting GitLab incident involved copied repository and consulting data that may have included tokens. The pattern is endemic: attackers want the tokens, keys, and credentials sitting on developer machines and build systems.

The usual response is to rotate everything after a breach. That is necessary today, but it is also an admission that the model is wrong. If one copied string gives an attacker full access from anywhere, the string has too much authority.

A stolen string should not be enough to call an API.

The Better Model: DPoP, But Stricter#

The fix is proof-of-possession . In OAuth terms this is a sender-constrained token , and the closest standard is OAuth 2.0 Demonstrating Proof of Possession, RFC 9449, usually called DPoP . DPoP binds an access token to a public/private key pair: the client signs a per-request proof (a JWT covering the HTTP method, URL, timestamp, a unique ID, and a hash of the access token), and the server verifies the token is bound to the key that signed the proof.

The mechanics:

A user, machine, service, or organization creates a public/private key pair.

The private key is stored behind a non-exportable signing interface (Secure Enclave, TPM, YubiKey, HSM, cloud KMS, a workload identity system, or similar).

The API provider stores the corresponding public key and issues a token bound to it.

Every API request includes a fresh signed proof created by the private key.

The provider verifies both the token and the proof before allowing the request.

The application can still keep a token in an environment variable, but that token is no longer a complete credential. It is a scoped capability that only works when paired with a fresh signature.

That changes the failure mode. If malware steals the API token, it is useless without the signer. If malware steals a signed request, the request expires almost immediately. If the proof is single-use, even replaying it within the validity window fails after the first use.

A developer-credential profile should go further than DPoP in three ways:

Non-exportable keys. DPoP proves possession of a private key but does not require it to live in hardware. A private key sitting in a normal file on disk is better than a bearer token, but malware can still copy it. For developer credentials, the key should be behind a Secure Enclave, TPM, hardware key, HSM, or managed signing service.

Beyond OAuth. Plain API keys, personal access tokens, npm tokens, registry tokens, cloud credentials, and webhook secrets should all become proof-of-possession credentials, whether the mechanism is DPoP-compatible or adapted for non-OAuth APIs.

Single-use by default for high-risk APIs. DPoP's jti lets servers detect replay; for sensitive operations, replay prevention should be mandatory, not optional.

What Should Be Signed?#

A signed proof should be narrow. It should not say "this machine can use this API token." It should say "this machine can make this specific request right now."

At minimum, the signed payload should include:

the API token identifier or access token hash

the HTTP method

the canonical URL or resource identifier

the issued-at and expiration timestamps

a nonce or unique request ID

optionally, a hash of the request body

So a proof for POST /v1/packages/publish is not reusable for DELETE /v1/packages/latest, a proof for one host does not work on another, and a proof does not validate if the body has been changed.

The shorter the validity window, the better. For many calls, 30 seconds is generous; for sensitive operations, a few seconds; for the highest-risk operations, the proof should be single-use. Single-use needs only a short-lived replay cache keyed on the nonce — operationally realistic for providers who already run rate-limit and idempotency-key stores. Reserve it for package publishing, production deploys, cloud administration, payment operations, and credential management.

The Signer, Not The Device, Is The Boundary#

The key does not have to...

token proof request dpop tokens credentials

Related Articles