Google OAuth DPoP: Which Stolen Tokens Does It Actually Protect?
A signed DPoP proof goes out with the request, yet Google still returns token_type: Bearer. Is the binding working, and what does an attacker get from a stolen token?

- Google applies DPoP key binding to refresh tokens used to obtain new access tokens.
- Each refresh needs a fresh proof signed with the same private key.
- Existing-user migration and nonce challenges are part of the implementation.
A copied refresh token is no longer sufficient
Apps that access Drive or Calendar while the user is away rely on refresh tokens to renew API access. Google's October 15 DPoP announcement describes binding that renewal capability to a client-generated key pair. Stealing the token string alone does not give an attacker the private key needed to use it.
Access tokens still use Bearer authentication. Google describes this as a way to protect long-lived credentials without adding asymmetric proof verification to every resource API call. Receiving token_type: Bearer after a DPoP exchange is therefore expected.
| Credential | After adopting Google DPoP | Remaining concern |
|---|---|---|
| DPoP-bound refresh token | Renewal requires proof of key possession | Protect the private key |
| Issued access token | Still a Bearer credential | Prevent exposure and revoke when appropriate |
| Web session cookie | Outside this feature's protection | Secure the web session separately |
RFC 9449 permits Bearer access-token issuance. The presence of DPoP in a flow does not establish that every credential in that flow is key-bound.
Both issuance and refresh need signed proofs
The client creates a key pair and sends a signed JWT in the token request's DPoP header. Google's adoption guide uses EC P-256 with ES256. The proof carries public-key information; the private key stays with the client.
After obtaining a bound refresh token, sign each new refresh proof with that same key. One Google-specific detail in the web-server guide is easy to miss: the authorization-code exchange uses a Base64URL-encoded SHA-256 hash of the code for jti. Refresh requests instead use a unique per-request identifier.
The proof also identifies the HTTP method through htm, the token endpoint through htu, and its issue time through iat. Reusing a single pre-signed JWT is not the implementation model.
Existing refresh tokens need a new issuance flow
Do not mark existing users as protected merely because new code can create proofs. Google's migration note requires either revoking the existing grant or using prompt=consent in the authorization request to obtain a new bound refresh token. Web-server flows also need access_type=offline to receive refresh tokens.
A frontend-only application needs an architectural check. The current adoption guide directs secretless SPAs through a Backend-for-Frontend because of client-secret requirements and CORS restrictions on the nonce header. That server handles token operations on the browser's behalf.
Handle a nonce challenge by constructing another proof
Under the nonce challenge protocol, a 400 use_dpop_nonce response supplies a fresh value in DPoP-Nonce. Put that value in a newly signed proof and retry. Editing an already signed JWT is insufficient.
For invalid_dpop_proof, check the method, URI, timestamp, identifier reuse, and signature listed in Google's error guidance. A nonce challenge and a failed proof need different handling.
Token-only theft protection does not cover an attacker who also obtains the private key. Keep the existing storage controls: encrypt server-held user tokens at rest, revoke and delete unused tokens, and retain PKCE for authorization-code protection. Adopting DPoP means delivering key management, migration, and refresh recovery together.