---
title: OAuth DPoP验证与持有者证明(htu/htm/jti/iat)最佳实践
keywords:
- DPoP
- JWS
- htu
- htm
- jti
- iat
- cnf
- jkt
description: 通过验证DPoP JWS中的htu/htm/jti/iat并绑定访问令牌的cnf.jkt,实施持有者证明与重放防护,强化OAuth资源访问的安全性。
categories:
- 文章资讯
- 技术教程
---
背景与价值
DPoP通过绑定客户端密钥与请求上下文,降低令牌被盗后滥用风险。严格验证字段与重放窗口可保障持有者证明生效。
统一规范
- 字段校验:
htu与htm必须与当前请求完全匹配。 - 重放防护:
jti一次性使用,iat提供±300秒容忍窗口。 - 令牌绑定:访问令牌
cnf.jkt必须与DPoP公钥拇指指对应。
核心实现
DPoP验证
type Req = { method: string; url: string; headers: Record<string, string | undefined> }
type DpopClaims = { htu: string; htm: string; jti: string; iat: number; cnf?: { jkt?: string } }
function base64urlToBuf(s: string): ArrayBuffer { const b = atob(s.replace(/-/g,'+').replace(/_/g,'/')); const u = new Uint8Array(b.length); for (let i=0;i<b.length;i++) u[i] = b.charCodeAt(i); return u.buffer }
async function importDpopKey(jwk: any): Promise<CryptoKey> { return crypto.subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']) }
function sha256Hex(buf: ArrayBuffer): Promise<string> { return crypto.subtle.digest('SHA-256', buf).then(d => { const u = new Uint8Array(d); let s = ''; for (let i=0;i<u.length;i++) s += u[i].toString(16).padStart(2,'0'); return s }) }
function timeNow(): number { return Math.floor(Date.now() / 1000) }
class JtiStore { used = new Set<string>(); has(j: string) { return this.used.has(j) } add(j: string) { this.used.add(j) } }
async function verifyDpop(dpop: string, req: Req, jwk: any, expectedJkt?: string, leewaySec = 300, store = new JtiStore()): Promise<boolean> {
const parts = dpop.split('.')
if (parts.length !== 3) return false
const [h, p, s] = parts
const header = JSON.parse(new TextDecoder().decode(base64urlToBuf(h)))
if (header.typ !== 'dpop+jwt') return false
if (header.alg !== 'ES256') return false
const key = await importDpopKey(header.jwk)
const sig = base64urlToBuf(s)
const ok = await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, sig, new TextEncoder().encode(h + '.' + p))
if (!ok) return false
const claims: DpopClaims = JSON.parse(new TextDecoder().decode(base64urlToBuf(p)))
const url = new URL(req.url, 'https://app.example.com')
const htu = url.origin + url.pathname
if (claims.htm.toUpperCase() !== req.method.toUpperCase()) return false
if (claims.htu !== htu) return false
if (store.has(claims.jti)) return false
const now = timeNow()
if (claims.iat - leewaySec > now) return false
if (claims.iat + leewaySec < now) return false
const jwkThumb = await sha256Hex(await crypto.subtle.exportKey('raw', key))
if (expectedJkt && expectedJkt !== jwkThumb) return false
store.add(claims.jti)
return true
}
令牌绑定(cnf.jkt)示例
type AccessToken = { cnf?: { jkt?: string } }
function tokenBound(ok: boolean, token: AccessToken, jkt: string): boolean { return ok && token.cnf?.jkt === jkt }
落地建议
- 在资源服务器验证DPoP并绑定访问令牌的
cnf.jkt,拒绝未绑定或不匹配请求。 - 维护
jti一次性使用存储与合理窗口,阻断重放与时钟偏移误判。
验证清单
htu/htm是否与当前请求完全匹配;jti/iat是否通过窗口与一次性校验。cnf.jkt是否与DPoP公钥拇指指一致。

发表评论 取消回复