Implementar DPoP
DPoP (Demonstrating Proof of Possession, RFC 9449) vincula los access tokens a un par de claves criptográficas específico del cliente. A diferencia de los tokens Bearer estándar que puede usar cualquier persona que los posea, los tokens vinculados con DPoP son inútiles para un atacante que los robe — no puede producir una prueba válida sin la clave privada.
Por qué usar DPoP:
- Prevenir el robo de tokens: Los ataques XSS que exfiltran tokens de localStorage o cookies no pueden usar los tokens robados sin la clave privada correspondiente
- Prevenir filtraciones en logs: Los tokens registrados accidentalmente en logs de servidor o sistemas de seguimiento de errores no pueden reproducirse
- Prevenir ataques de repetición: Cada prueba DPoP incluye el método HTTP y la URL, vinculándola a una solicitud específica
- Cumplimiento normativo: Algunos estándares de seguridad y APIs financieras requieren tokens vinculados al emisor
Cómo Funciona DPoP
- El cliente genera un par de claves pública/privada (una vez, al inicio o por sesión)
- Al solicitar un token, el cliente incluye un JWT de prueba DPoP en la cabecera
DPoP. La prueba contiene la clave pública, el método HTTP y la URL, y un identificador único. - Auris valida la prueba, vincula el token a la clave pública (mediante un claim
jkt— JWK Thumbprint), y devuelve un tipo de tokenDPoPen lugar deBearer - En cada llamada a la API, el cliente incluye tanto el access token (
Authorization: DPoP <token>) como una prueba DPoP fresca (DPoP: <proof>) - El servidor de recursos valida la prueba contra el claim
jktdel token
Configuración en la Consola
Habilitar DPoP en la Aplicación
En la Consola de Auris, ve a Aplicaciones y selecciona tu aplicación. En la pestaña Configuración, encuentra la sección DPoP:
| Configuración | Descripción |
|---|---|
| Habilitar DPoP | Aceptar pruebas DPoP. Los tokens solicitados con DPoP estarán vinculados al emisor. Los tokens sin DPoP siguen siendo aceptados. |
| Requerir DPoP | Rechazar todas las solicitudes de token sin una prueba DPoP válida. Habilita esto solo después de que todos los clientes hayan migrado. |
| Requerir Nonces | Nonces emitidos por el servidor en las pruebas DPoP. Añade protección contra repetición al precio de un viaje de ida y vuelta adicional. |
Planificar la Migración
Si tienes clientes existentes usando tokens Bearer, utiliza un enfoque de migración gradual:
- Habilitar DPoP (pero no requerirlo) — los clientes pueden optar por participar
- Actualizar todos los clientes para enviar pruebas DPoP
- Monitorear que todas las solicitudes de token incluyan pruebas DPoP (comprobar los logs de Auris)
- Habilitar “Requerir DPoP” para rechazar solicitudes sin pruebas
Implementación para SPAs
Generar un Par de Claves
Genera un par de claves ECDSA P-256 usando la API Web Crypto. Haz esto una vez por sesión del navegador y almacena el par de claves en memoria (no en localStorage — es no exportable por diseño):
const dpopKeyPair = await crypto.subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
false, // no exportable — la clave privada no puede exportarse
['sign', 'verify'],
)Crear una Prueba DPoP
Una prueba DPoP es un JWT firmado con la clave privada. Contiene la clave pública (como jwk en el encabezado), el método HTTP y la URL objetivo, un jti único, y la marca de tiempo actual:
async function createDpopProof(keyPair, method, url, nonce) {
// Exportar la clave pública como JWK
const publicKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey)
// Crear el encabezado de la prueba
const header = {
typ: 'dpop+jwt',
alg: 'ES256',
jwk: {
kty: publicKeyJwk.kty,
crv: publicKeyJwk.crv,
x: publicKeyJwk.x,
y: publicKeyJwk.y,
},
}
// Crear el payload de la prueba
const payload = {
jti: crypto.randomUUID(),
htm: method,
htu: url,
iat: Math.floor(Date.now() / 1000),
...(nonce && { nonce }),
}
// Firmar la prueba (función auxiliar para crear un JWT compacto)
return await signJwt(header, payload, keyPair.privateKey)
}Solicitar un Token con DPoP
Incluye la prueba DPoP en la cabecera DPoP al solicitar un token:
const tokenUrl = 'https://auth.yourdomain.com/api/auth/token'
const proof = await createDpopProof(dpopKeyPair, 'POST', tokenUrl)
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'DPoP': proof,
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: 'https://your-app.com/callback',
client_id: 'your-client-id',
code_verifier: pkceCodeVerifier,
}),
})
const token = await response.json()
// token.token_type será "DPoP" en lugar de "Bearer"Realizar Llamadas a la API con DPoP
Cada llamada a la API debe incluir tanto el token vinculado con DPoP como una prueba fresca:
async function dpopFetch(url, method, dpopKeyPair, accessToken, options = {}) {
const proof = await createDpopProof(dpopKeyPair, method, url)
return fetch(url, {
...options,
method,
headers: {
...options.headers,
'Authorization': `DPoP ${accessToken}`,
'DPoP': proof,
},
})
}
// Uso
const users = await dpopFetch(
'https://auth.yourdomain.com/api/users',
'GET',
dpopKeyPair,
token.access_token,
)Implementación para Node.js
Para aplicaciones Node.js del lado del servidor, usa la librería jose para la generación de claves y la firma de JWT:
import * as jose from 'jose'
// Genera un par de claves al inicio del servicio
const { publicKey, privateKey } = await jose.generateKeyPair('ES256')
async function createDpopProof(method: string, url: string, nonce?: string) {
const publicJwk = await jose.exportJWK(publicKey)
const proof = await new jose.SignJWT({
htm: method,
htu: url,
...(nonce && { nonce }),
})
.setProtectedHeader({
typ: 'dpop+jwt',
alg: 'ES256',
jwk: publicJwk,
})
.setJti(crypto.randomUUID())
.setIssuedAt()
.sign(privateKey)
return proof
}
// Solicitar un token con DPoP
const tokenUrl = 'https://auth.yourdomain.com/api/auth/token'
const proof = await createDpopProof('POST', tokenUrl)
const tokenResponse = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'DPoP': proof,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.AURIS_CLIENT_ID,
client_secret: process.env.AURIS_CLIENT_SECRET,
}),
})Soporte en el SDK
El SDK @auris/js incluye un helper createDpopProof que gestiona la generación de claves, la creación de pruebas y el manejo de nonces:
import { AurisClient, createDpopProof } from '@auris/js'
// El SDK puede generar y gestionar claves DPoP automáticamente
const auris = new AurisClient({
domain: 'auth.yourdomain.com',
clientId: 'your-client-id',
useDpop: true, // Habilita la generación automática de pruebas DPoP
})
// loginWithRedirect() y handleRedirectCallback() incluirán
// automáticamente pruebas DPoP en las solicitudes de token
await auris.loginWithRedirect({ scope: 'openid profile' })Manejo de Nonces
Cuando “Requerir Nonces” está habilitado en la aplicación, Auris emite un nonce del servidor que debe incluirse en la prueba DPoP. Esto proporciona protección contra repetición — cada prueba solo puede usarse una vez.
El flujo para el manejo de nonces:
- El cliente envía una solicitud de token con una prueba DPoP (sin nonce en el primer intento)
- Si se requiere un nonce, Auris responde con
HTTP 400y una cabeceraDPoP-Nonceque contiene el valor del nonce - El cliente crea una nueva prueba DPoP incluyendo el nonce y reintenta la solicitud
- Auris acepta la prueba y devuelve el token
async function requestTokenWithNonce(dpopKeyPair, tokenUrl, body) {
let nonce = undefined
for (let attempt = 0; attempt < 2; attempt++) {
const proof = await createDpopProof(dpopKeyPair, 'POST', tokenUrl, nonce)
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'DPoP': proof,
},
body: new URLSearchParams(body),
})
// Comprobar si se requiere un nonce
const newNonce = response.headers.get('DPoP-Nonce')
if (response.status === 400 && newNonce) {
nonce = newNonce
continue // reintentar con nonce
}
return await response.json()
}
throw new Error('Failed to obtain token after nonce retry')
}Comprueba siempre la cabecera DPoP-Nonce en cada respuesta — no solo en las respuestas de error. El servidor puede rotar el nonce en respuestas exitosas también, y deberías usar el último nonce en solicitudes posteriores.
Solución de Problemas
Problemas comunes al implementar DPoP:
| Problema | Causa | Solución |
|---|---|---|
invalid_dpop_proof | El JWT de prueba está malformado o la firma es inválida | Verifica que la prueba es un JWT válido firmado con el mismo par de claves |
invalid_dpop_proof (htm/htu no coincide) | El htm o htu de la prueba no coincide con el método/URL real de la solicitud | Asegúrate de que htm coincide con el método HTTP y htu con la URL completa (incluyendo esquema y host, excluyendo query/fragment) |
use_dpop_nonce | El servidor requiere un nonce pero no se proporcionó ninguno | Lee la cabecera DPoP-Nonce de la respuesta e inclúyela en la siguiente prueba |
dpop_proof_replay | El mismo jti se usó dos veces | Genera un jti único (UUID) para cada prueba |
| Token rechazado por el servidor de recursos | El jkt del token no coincide con la clave de la prueba | Asegúrate de usar el mismo par de claves tanto para la solicitud de token como para las llamadas a la API |
iat demasiado antiguo | Desajuste de reloj entre el cliente y el servidor | Asegúrate de que el reloj del cliente es preciso. Auris permite hasta 60 segundos de desajuste. |
Pruebas con curl
DPoP es difícil de probar directamente con curl porque cada solicitud requiere un JWT de prueba único y firmado. Para fines de prueba, puedes generar pruebas con un script:
# Genera una prueba DPoP con Node.js y la pasa a curl
PROOF=$(node -e "
const jose = require('jose');
(async () => {
const { privateKey, publicKey } = await jose.generateKeyPair('ES256');
const jwk = await jose.exportJWK(publicKey);
const proof = await new jose.SignJWT({ htm: 'POST', htu: 'https://auth.yourdomain.com/api/auth/token' })
.setProtectedHeader({ typ: 'dpop+jwt', alg: 'ES256', jwk })
.setJti(require('crypto').randomUUID())
.setIssuedAt()
.sign(privateKey);
process.stdout.write(proof);
})();
")
curl -X POST https://auth.yourdomain.com/api/auth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "DPoP: $PROOF" \
-d "grant_type=client_credentials" \
-d "client_id=your-client-id" \
-d "client_secret=your-client-secret"Formato del Token DPoP
Cuando se usa DPoP, el access token emitido incluye un claim jkt (JWK Thumbprint) que lo vincula a la clave pública del cliente:
{
"sub": "user-id",
"iss": "https://auth.yourdomain.com",
"aud": "https://auth.yourdomain.com",
"exp": 1735000000,
"iat": 1734996400,
"jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I",
"cnf": {
"jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
}
}El token_type en la respuesta del token será "DPoP" en lugar de "Bearer", indicando que el token debe presentarse con una prueba DPoP.
Permisos Requeridos
| Operación | Permiso |
|---|---|
| Habilitar/configurar DPoP en una aplicación | manage:applications |
| Gestionar la configuración DPoP | manage:dpop_config |
| Solicitar tokens con DPoP | Sin permiso especial (solo autenticación del cliente) |
Guías Relacionadas
- Credenciales de Cliente M2M — Sección DPoP para tokens M2M
- Login Alojado (PKCE) — DPoP puede combinarse con PKCE para máxima seguridad
- Protección contra Ataques — Otras capas de seguridad que complementan DPoP