DPoP (Demostración de prueba de posesión)
El problema con los Bearer Tokens
La especificación OAuth 2.0 Bearer Token (RFC 6750) define tokens que conceden acceso a cualquiera que los posea. La palabra “bearer” es literal: quien porta (tiene) el token puede usarlo. No hay verificación de que el presentador del token sea la misma parte a quien se emitió.
Esto crea una clase de ataques:
| Ataque | Mecanismo |
|---|---|
| Exfiltración de token via XSS | JavaScript malicioso lee el token de localStorage y lo envía al servidor del atacante |
| Filtración de token via logs | Access tokens registrados accidentalmente por proxies, CDNs o servidores de aplicación |
| Repetición de token | Un token interceptado se repite desde un dispositivo o IP diferente |
| Robo de token via middleware comprometido | Un proxy inverso o API gateway almacena tokens para uso posterior |
Qué hace DPoP
DPoP (Demonstrating Proof of Possession), definido en RFC 9449, resuelve este problema vinculando los tokens al par de claves criptográficas del cliente. Un token vinculado con DPoP es inútil sin la clave privada correspondiente.
La idea central:
- El cliente genera un par de claves efímero (RSA o EC)
- Al solicitar un token, el cliente crea una prueba DPoP — un JWT firmado con la clave privada
- El servidor de autorización (Auris) extrae la clave pública de la prueba y vincula el token emitido a esa clave mediante un JWK thumbprint (claim
jkt) - En cada llamada a la API, el cliente envía tanto el access token vinculado como una prueba DPoP fresca firmada con la misma clave privada
- El resource server verifica que la clave pública de la prueba coincide con el binding
jktdel token
Si un atacante roba el access token pero no la clave privada (que se mantiene en memoria y nunca se transmite), el token es inútil.
Cómo funciona: paso a paso
Paso 1: El cliente genera un par de claves efímero
const keyPair = await crypto.subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
false, // no extraíble: la clave privada no puede exportarse
['sign', 'verify']
)Paso 2: El cliente crea una prueba DPoP JWT
// Encabezado de la prueba DPoP JWT
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "coordenada-x-codificada-base64url",
"y": "coordenada-y-codificada-base64url"
}
}
// Payload de la prueba DPoP JWT
{
"jti": "id-prueba-unico-abc123",
"htm": "POST",
"htu": "https://auth.example.com/api/auth/token",
"iat": 1739880000,
"nonce": "nonce-proporcionado-por-servidor"
}Paso 3: La prueba se envía con la solicitud de token
POST /api/auth/token HTTP/1.1
DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7...
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=...&code_verifier=...Paso 4: Auris vincula el token emitido a la clave
El access token emitido contiene el claim cnf.jkt:
{
"sub": "usr_abc123",
"cnf": {
"jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
}
}El jkt es el thumbprint SHA-256 de la JWK pública del cliente.
Paso 5: En cada solicitud a la API
GET /api/users/me HTTP/1.1
Authorization: DPoP eyJhbGciOiJSUzI1NiJ9...
DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7...Auris admite DPoP como característica opt-in. Habilítalo en Consola → Aplicaciones → [tu app] → Seguridad avanzada → Requerir DPoP. Una vez habilitado, todas las solicitudes de token desde esa aplicación deben incluir una prueba DPoP.
DPoP vs mTLS
Ambos vinculan tokens a claves criptográficas del cliente, pero con diferentes trade-offs:
| Aspecto | DPoP | mTLS |
|---|---|---|
| Infraestructura requerida | Ninguna (usa Web Crypto API) | Certificados de cliente + PKI |
| Funciona en browsers | Sí | No (los browsers no exponen certificados de cliente a JS) |
| Gestión de claves | Efímero, por sesión | Certificados de larga duración que deben renovarse |
| Revocación | Inmediata (la clave privada está en memoria) | Requiere CRL o OCSP |
| Resistencia a exfiltración de token | Alta | Alta |
| Complejidad de implementación | Baja | Alta |