Revocación de Tokens

Autenticación Security Notes Jan 9, 2026 HTTP
oauth token security logout rfc-7009

Definition

¿Qué sucede cuando un usuario hace clic en “Cerrar sesión” en tu aplicación? ¿O cuando te das cuenta de que un token podría estar comprometido? ¿O cuando un usuario elimina el acceso de una aplicación de terceros desde la configuración de su cuenta? No puedes simplemente esperar a que el token expire pronto - necesitas invalidarlo activamente. Ahí es donde entra la revocación de tokens.

La Revocación de Tokens (RFC 7009) es una extensión de OAuth 2.0 que proporciona un endpoint estandarizado para que los clientes notifiquen al servidor de autorización que un token obtenido previamente debe ser invalidado inmediatamente. Esto aplica tanto a access tokens como a refresh tokens. Cuando un token es revocado, se vuelve inmediatamente inválido incluso si no ha alcanzado su tiempo de expiración.

Esto es crítico para la seguridad y el control del usuario. Sin revocación, un access token robado permanece válido hasta su expiración (potencialmente horas), y un refresh token robado podría usarse para obtener nuevos access tokens indefinidamente. La revocación da a los usuarios y aplicaciones la capacidad de cortar el acceso instantáneamente, haciéndola esencial para flujos de cierre de sesión, incidentes de seguridad y eliminación de acceso iniciada por el usuario.

Example

Seguridad de Cuenta Google: Cuando visitas la página de seguridad de tu Cuenta Google y ves una lista de dispositivos/aplicaciones con acceso a tu cuenta, hacer clic en “Eliminar acceso” en cualquiera de ellos activa la revocación de tokens. El servidor de autorización de Google invalida inmediatamente todos los access y refresh tokens para esa aplicación, incluso si estaban configurados para expirar días después.

Aplicaciones OAuth de GitHub: Cuando revocas el acceso de una aplicación OAuth de GitHub desde tu página de configuración, GitHub llama a su endpoint de revocación interno. Todos los access tokens para esa combinación app-usuario se vuelven inválidos instantáneamente. La próxima vez que la aplicación intente usar el token para acceder a tus repositorios, recibe un 401 Unauthorized.

Incidente de Seguridad Corporativa: Un empleado reporta su portátil robado. El equipo de seguridad inmediatamente revoca todos los tokens OAuth asociados con ese dispositivo. Aunque los access tokens podrían haber sido válidos por otra hora y los refresh tokens por otro mes, todos están muertos segundos después de la revocación.

Cierre de Sesión en Aplicación Bancaria: Terminas de revisar tu saldo bancario en una computadora pública y haces clic en “Cerrar sesión”. La aplicación bancaria llama al endpoint de revocación tanto para el access token como para el refresh token. Si olvidaste cerrar sesión y alguien más se sienta en esa computadora, no pueden usar los tokens antiguos incluso si todavía están en la memoria del navegador.

Seguridad de Workspace Slack: Un administrador de Slack nota actividad sospechosa de API desde una integración de terceros. Revocan el token OAuth desde el panel de administración de Slack. La siguiente llamada a la API de la integración falla inmediatamente, y deben re-autorizarse para recuperar el acceso.

Analogy

La Desactivación de Llave Digital: Imagina que diste una llave digital de habitación a un visitante que es válida por 24 horas. Pero después de 2 horas, quieres que salga. La expiración del token es como esperar 22 horas más para que la llave deje de funcionar. La revocación del token es como desactivar inmediatamente esa llave específica en el sistema central del hotel - la tarjeta física todavía existe, pero ya no abrirá nada.

La Lista Negra de Pulseras de Concierto: Compraste una pulsera de concierto válida todo el fin de semana. Pero te echan por mal comportamiento. La pulsera todavía se ve válida y tiene la fecha de mañana, pero seguridad la agrega a una lista negra. La revocación de tokens es esa lista negra - la pulsera no expiró, pero está explícitamente marcada como “no permitir entrada”.

La Cancelación de Reserva de Restaurante: Hiciste una reserva para las 8 PM de esta noche, pero llamas a las 6 PM para cancelar. La reserva no expiró (todavía es válida por 2 horas más), pero la cancelaste explícitamente. Cuando apareces más tarde esa noche, el anfitrión dice “Lo siento, cancelaste esta reserva”. La revocación de tokens es esa cancelación explícita.

La Suspensión de Tarjeta de Biblioteca: Tu tarjeta de biblioteca dice que es válida hasta 2027, pero acumulas $100 en multas por retrasos. La biblioteca suspende tu tarjeta - no está expirada, pero ha sido revocada. Cuando intentas sacar libros, el escáner muestra “CUENTA SUSPENDIDA” aunque la tarjeta física dice que todavía es válida.

Code Example

# Revocación iniciada por cliente (cierre de sesión de usuario)
POST /oauth/revoke HTTP/1.1
Host: authorization-server.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

token=45ghiukldjahdnhzdauz4a5za4az5za&
token_type_hint=access_token

# Respuesta de revocación exitosa
HTTP/1.1 200 OK
Content-Type: application/json

{
  "revoked": true
}

# Revocando un refresh token (también invalida access tokens asociados)
POST /oauth/revoke HTTP/1.1
Host: authorization-server.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

token=fdb8fdbecf1d03855e3b8b55e3b8f&
token_type_hint=refresh_token

# Si el token ya es inválido/revocado o no existe
# Aún retorna 200 OK (especificación RFC 7009)
HTTP/1.1 200 OK

# Implementación del lado del cliente (React/JavaScript)
// Función de utilidad de revocación de tokens
async function revokeToken(token, tokenType = 'access_token') {
  const clientId = 'your-client-id';
  const clientSecret = 'your-client-secret';

  try {
    const response = await fetch('https://auth.example.com/oauth/revoke', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`
      },
      body: new URLSearchParams({
        token: token,
        token_type_hint: tokenType
      })
    });

    // RFC 7009: endpoint DEBE retornar 200 incluso si el token es inválido
    if (response.status === 200) {
      console.log(`Token revocado exitosamente`);
      return true;
    } else {
      console.error(`Revocación falló con estado: ${response.status}`);
      return false;
    }
  } catch (error) {
    console.error('Solicitud de revocación falló:', error);
    return false;
  }
}

// Flujo de cierre de sesión con limpieza apropiada de tokens
async function logout() {
  const accessToken = localStorage.getItem('access_token');
  const refreshToken = localStorage.getItem('refresh_token');

  // Revocar refresh token primero (puede hacer cascada a access tokens)
  if (refreshToken) {
    await revokeToken(refreshToken, 'refresh_token');
  }

  // Luego revocar explícitamente access token
  if (accessToken) {
    await revokeToken(accessToken, 'access_token');
  }

  // Limpiar almacenamiento local
  localStorage.removeItem('access_token');
  localStorage.removeItem('refresh_token');
  localStorage.removeItem('user_info');

  // Redirigir a login
  window.location.href = '/login';
}

// Aplicación del lado del servidor (Node.js/Express)
// Servidor de autorización mantiene lista de revocación
const revokedTokens = new Set(); // En producción: usar Redis o base de datos

app.post('/oauth/revoke', authenticate, (req, res) => {
  const { token, token_type_hint } = req.body;

  if (!token) {
    // RFC 7009: invalid_request si falta token
    return res.status(400).json({
      error: 'invalid_request',
      error_description: 'token parameter is required'
    });
  }

  // Validar que el solicitante posee el token o está autorizado
  // (verificar client_id coincide con client_id del token)
  const tokenInfo = getTokenInfo(token); // Tu lógica de búsqueda de token
  if (tokenInfo && tokenInfo.client_id !== req.client.id) {
    // RFC 7009: retornar 200 incluso para intentos no autorizados (no filtrar info)
    return res.status(200).send();
  }

  // Agregar a lista de revocación
  revokedTokens.add(token);

  // Si es un refresh token, también revocar access tokens asociados
  if (token_type_hint === 'refresh_token') {
    const associatedTokens = findAccessTokensByRefreshToken(token);
    associatedTokens.forEach(at => revokedTokens.add(at));
  }

  // Actualizar base de datos/caché
  db.tokens.update(
    { token: token },
    { $set: { revoked: true, revoked_at: new Date() } }
  );

  // RFC 7009: siempre retornar 200 OK
  res.status(200).send();
});

// Servidor de recursos verifica revocación antes de procesar solicitudes
app.use('/api/*', async (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return res.status(401).json({ error: 'Missing token' });
  }

  // Verificar si el token está en lista de revocación
  if (revokedTokens.has(token)) {
    return res.status(401).json({
      error: 'Token has been revoked'
    });
  }

  // Opcionalmente: introspeccionar token para estado de revocación en tiempo real
  const introspection = await introspectToken(token);
  if (!introspection.active) {
    return res.status(401).json({
      error: 'Token is not active'
    });
  }

  next();
});

Diagram

sequenceDiagram
    participant User
    participant Client
    participant AuthServer as Authorization Server
    participant ResourceServer as Resource Server
    participant RevocationDB as Revocation Database

    Note over User,Client: Usuario hace clic en "Cerrar sesión"

    User->>Client: Solicitud de cierre de sesión

    Client->>AuthServer: POST /revoke
token={refresh_token}
token_type_hint=refresh_token AuthServer->>RevocationDB: Marcar refresh token como revocado
Encontrar access tokens asociados RevocationDB-->>AuthServer: Tokens marcados como revocados AuthServer->>RevocationDB: Agregar todos los tokens a
lista negra de revocación AuthServer-->>Client: 200 OK Client->>Client: Limpiar tokens locales
del almacenamiento Client-->>User: Redirigir a página de login Note over User,ResourceServer: Más tarde: Atacante intenta usar token antiguo User->>ResourceServer: GET /api/data
Authorization: Bearer {old_token} ResourceServer->>AuthServer: Introspeccionar token
o verificar lista de revocación AuthServer->>RevocationDB: Verificar si token está revocado RevocationDB-->>AuthServer: Token encontrado en
lista de revocación AuthServer-->>ResourceServer: {"active": false} ResourceServer-->>User: 401 Unauthorized
Token ha sido revocado

Security Notes

SECURITY NOTES

CRÍTICO - …

Configuración y Validación:

  • El endpoint de revocación de tokens DEBE estar autenticado para prevenir que atacantes revoquen tokens arbitrarios.
  • Usar credenciales de cliente (client_id + client_secret) o TLS mutuo.
  • Cuando se revoca un refresh token, también DEBE revocar todos los access tokens asociados para prevenir acceso continuo.
  • Mantener una lista de revocación (lista negra) que los servidores de recursos puedan consultar, ya sea directamente o vía introspección de tokens.
  • Usar IDs de tokens (claim jti en JWT) para rastrear y revocar tokens eficientemente en lugar de almacenar cadenas de tokens completas.
  • Implementar token binding (RFC 8471) para prevenir robo de tokens y ataques de replay.

Monitoreo y Protección:

  • Establecer períodos de retención razonables para tokens revocados en la lista negra - mantener registros al menos tanto tiempo como la vida útil máxima posible del token.
  • Usar almacenamiento seguro y rápido (Redis, Memcached) para la lista de revocación para minimizar latencia en consultas de introspección.
  • Siempre retornar HTTP 200 OK incluso para tokens inválidos para evitar filtrar información sobre existencia de tokens.
  • Registrar todos los eventos de revocación con client_id, user_id y timestamp para auditoría de seguridad.
  • Implementar limitación de tasa en el endpoint de revocación para prevenir ataques DoS.
  • Considerar revocar tokens automáticamente en cambio de contraseña, eliminación de cuenta o eventos de seguridad.
  • Para tokens JWT, tiempos de expiración cortos reducen la ventana de riesgo después de la revocación antes de que se requiera introspección.

Best Practices

  1. Revocar Refresh Tokens Primero: Siempre revocar refresh tokens antes que access tokens, ya que tienen vidas útiles más largas
  2. Revocación en Cascada: Al revocar un refresh token, automáticamente revocar todos los access tokens asociados
  3. Usar IDs de Tokens: Almacenar y revocar por claim jti (JWT ID) en lugar de cadenas de tokens completas
  4. Implementar Lista Negra: Mantener una lista de revocación distribuida de acceso rápido (Redis, DynamoDB)
  5. Triggers de Revocación Automática: Revocar tokens en cambio de contraseña, eliminación de cuenta, actividad sospechosa
  6. Limpieza del Lado del Cliente: Después de la revocación, limpiar tokens de todo almacenamiento local (localStorage, sessionStorage, cookies)
  7. Revocación Idempotente: Revocar un token ya revocado debe tener éxito (retornar 200 OK)
  8. Proveer Control al Usuario: Dar a los usuarios UI para ver y revocar sesiones/tokens activos
  9. Registro de Auditoría: Registrar todos los eventos de revocación con contexto (quién, qué, cuándo, por qué)
  10. Combinar con Introspección: Usar introspección de tokens (RFC 7662) para verificar estado de revocación en tiempo real

Common Mistakes

Olvidar Tokens Asociados: Revocar solo el access token pero no el refresh token, permitiendo al atacante obtener nuevos access tokens. Solución: Revocar el refresh token para hacer revocación en cascada.

Sin Autenticación de Cliente: Exponer el endpoint de revocación sin autenticación, permitiendo a cualquiera revocar cualquier token. Solución: Requerir credenciales de cliente.

No Verificar Revocación: Servidores de recursos aceptan tokens sin verificar la lista de revocación, derrotando el propósito. Solución: Integrar verificaciones de revocación vía introspección o consultas directas a lista negra.

Filtración de Información: Retornar diferentes códigos de error para “token no existe” vs “token revocado” vs “no autorizado”. Solución: Siempre retornar 200 OK según RFC 7009.

Rendimiento Pobre: Almacenar la lista de revocación en una base de datos lenta, causando latencia en cada introspección. Solución: Usar caché en memoria (Redis) con respaldo de base de datos.

Sin TTL en Registros de Revocación: Mantener registros de tokens revocados para siempre, hinchando el almacenamiento. Solución: Establecer TTL para coincidir con la vida útil máxima del token.

Revocación Solo del Lado del Cliente: Solo borrar tokens de localStorage sin llamar al endpoint de revocación. Solución: Siempre llamar /revoke antes de limpiar estado local.

Ignorar Cierre de Sesión: No implementar flujos de cierre de sesión apropiados que revoquen tokens del lado del servidor. Solución: El cierre de sesión debe llamar al endpoint de revocación.

Standards & RFCs