···
5. Find matching key by `kid` from JWT header
6. Cache the JWKS for 1 hour
113
+
## DPoP Token Binding
115
+
DPoP (Demonstrating Proof-of-Possession) binds access tokens to client-controlled cryptographic keys, preventing token theft and replay attacks.
119
+
DPoP is an OAuth extension (RFC 9449) that adds proof-of-possession semantics to bearer tokens. When a PDS issues a DPoP-bound access token:
121
+
1. Access token contains `cnf.jkt` claim (JWK thumbprint of client's public key)
122
+
2. Client creates a DPoP proof JWT signed with their private key
123
+
3. Server verifies the proof signature and checks it matches the token's `cnf.jkt`
125
+
### CRITICAL: DPoP Security Model
127
+
> โ ๏ธ **DPoP is an ADDITIONAL security layer, NOT a replacement for token signature verification.**
129
+
The correct verification order is:
130
+
1. **ALWAYS verify the access token signature first** (via JWKS, HS256 shared secret, or DID resolution)
131
+
2. **If the verified token has `cnf.jkt`, REQUIRE valid DPoP proof**
132
+
3. **NEVER use DPoP as a fallback when signature verification fails**
134
+
**Why This Matters**: An attacker could create a fake token with `sub: "did:plc:victim"` and their own `cnf.jkt`, then present a valid DPoP proof signed with their key. If we accept DPoP as a fallback, the attacker can impersonate any user.
139
+
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
140
+
โ Client โ โ Server โ
141
+
โ โ โ (Coves) โ
142
+
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
144
+
โ 1. Authorization: Bearer <token> โ
145
+
โ DPoP: <proof-jwt> โ
146
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ>โ
148
+
โ โ 2. VERIFY token signature
149
+
โ โ (REQUIRED - no fallback!)
151
+
โ โ 3. If token has cnf.jkt:
152
+
โ โ - Verify DPoP proof
153
+
โ โ - Check thumbprint match
156
+
โ<โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
159
+
### When DPoP is Required
161
+
DPoP verification is **REQUIRED** when:
162
+
- Access token signature has been verified AND
163
+
- Access token contains `cnf.jkt` claim (DPoP-bound)
165
+
If the token has `cnf.jkt` but no DPoP header is present, the request is **REJECTED**.
167
+
### Replay Protection
169
+
DPoP proofs include a unique `jti` (JWT ID) claim. The server tracks seen `jti` values to prevent replay attacks:
172
+
// Create a verifier with replay protection (default)
173
+
verifier := auth.NewDPoPVerifier()
174
+
defer verifier.Stop() // Stop cleanup goroutine on shutdown
176
+
// The verifier automatically rejects reused jti values within the proof validity window (5 minutes)
179
+
### DPoP Implementation
181
+
The `dpop.go` module provides:
184
+
// Create a verifier with replay protection
185
+
verifier := auth.NewDPoPVerifier()
186
+
defer verifier.Stop()
188
+
// Verify the DPoP proof
189
+
proof, err := verifier.VerifyDPoPProof(dpopHeader, "POST", "https://coves.social/xrpc/...")
191
+
// Invalid proof (includes replay detection)
194
+
// Verify it binds to the VERIFIED access token
195
+
expectedThumbprint, err := auth.ExtractCnfJkt(claims)
197
+
// Token not DPoP-bound
200
+
if err := verifier.VerifyTokenBinding(proof, expectedThumbprint); err != nil {
201
+
// Proof doesn't match token
205
+
### DPoP Proof Format
207
+
The DPoP header contains a JWT with:
210
+
- `typ`: `"dpop+jwt"` (required)
211
+
- `alg`: `"ES256"` (or other supported algorithm)
212
+
- `jwk`: Client's public key (JWK format)
215
+
- `jti`: Unique proof identifier (tracked for replay protection)
216
+
- `htm`: HTTP method (e.g., `"POST"`)
217
+
- `htu`: HTTP URI (without query/fragment)
218
+
- `iat`: Timestamp (must be recent, within 5 minutes)
233
+
"jti": "unique-id-123",
235
+
"htu": "https://coves.social/xrpc/social.coves.community.create",
## Security Considerations
···
- Required claims validation (sub, iss)
- Secure error messages (no internal details leaked)
250
+
- **DPoP proof verification** (proof-of-possession for token binding)
251
+
- **DPoP thumbprint validation** (prevents token theft attacks)
252
+
- **DPoP freshness checks** (5-minute proof validity window)
253
+
- **DPoP replay protection** (jti tracking with in-memory cache)
254
+
- **Secure DPoP model** (DPoP required AFTER signature verification, never as fallback)
### โ ๏ธ Not Yet Implemented
126
-
- DPoP validation (for replay attack prevention)
258
+
- Server-issued DPoP nonces (additional replay protection)
- Scope validation (checking `scope` claim)
- Audience validation (checking `aud` claim)
···
189
-
- [ ] DPoP proof validation
321
+
- [ ] DPoP nonce validation (server-managed nonce for additional replay protection)
- [ ] Scope-based authorization
- [ ] Audience claim validation
- [ ] Token revocation support