Skip to Content

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

  1. El cliente genera un par de claves pública/privada (una vez, al inicio o por sesión)
  2. 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.
  3. Auris valida la prueba, vincula el token a la clave pública (mediante un claim jkt — JWK Thumbprint), y devuelve un tipo de token DPoP en lugar de Bearer
  4. En cada llamada a la API, el cliente incluye tanto el access token (Authorization: DPoP <token>) como una prueba DPoP fresca (DPoP: <proof>)
  5. El servidor de recursos valida la prueba contra el claim jkt del 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ónDescripción
Habilitar DPoPAceptar pruebas DPoP. Los tokens solicitados con DPoP estarán vinculados al emisor. Los tokens sin DPoP siguen siendo aceptados.
Requerir DPoPRechazar todas las solicitudes de token sin una prueba DPoP válida. Habilita esto solo después de que todos los clientes hayan migrado.
Requerir NoncesNonces 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:

  1. Habilitar DPoP (pero no requerirlo) — los clientes pueden optar por participar
  2. Actualizar todos los clientes para enviar pruebas DPoP
  3. Monitorear que todas las solicitudes de token incluyan pruebas DPoP (comprobar los logs de Auris)
  4. 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:

  1. El cliente envía una solicitud de token con una prueba DPoP (sin nonce en el primer intento)
  2. Si se requiere un nonce, Auris responde con HTTP 400 y una cabecera DPoP-Nonce que contiene el valor del nonce
  3. El cliente crea una nueva prueba DPoP incluyendo el nonce y reintenta la solicitud
  4. 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:

ProblemaCausaSolución
invalid_dpop_proofEl JWT de prueba está malformado o la firma es inválidaVerifica 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 solicitudAsegú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_nonceEl servidor requiere un nonce pero no se proporcionó ningunoLee la cabecera DPoP-Nonce de la respuesta e inclúyela en la siguiente prueba
dpop_proof_replayEl mismo jti se usó dos vecesGenera un jti único (UUID) para cada prueba
Token rechazado por el servidor de recursosEl jkt del token no coincide con la clave de la pruebaAsegúrate de usar el mismo par de claves tanto para la solicitud de token como para las llamadas a la API
iat demasiado antiguoDesajuste de reloj entre el cliente y el servidorAsegú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ónPermiso
Habilitar/configurar DPoP en una aplicaciónmanage:applications
Gestionar la configuración DPoPmanage:dpop_config
Solicitar tokens con DPoPSin permiso especial (solo autenticación del cliente)

Guías Relacionadas