Refresh tokens en PHP explicados fácil: cómo mantener sesiones seguras sin pedir login a cada rato
Introducción
Cuando empiezas a trabajar con JWT, al principio todo parece simple:
- el usuario hace login
- el servidor genera un token
- el cliente lo usa para acceder a rutas protegidas
Y listo.
Pero luego aparece un problema muy real:
👉 ¿qué pasa cuando el token expira?
Si el token dura poco, la seguridad mejora, pero el usuario tendría que iniciar sesión continuamente.
Si el token dura demasiado, la experiencia mejora, pero aumentas el riesgo si ese token se filtra o es robado.
Aquí es donde entran los refresh tokens.
Los refresh tokens sirven para renovar el acceso sin obligar al usuario a volver a hacer login constantemente. Son una pieza muy importante en autenticación moderna porque ayudan a equilibrar:
- seguridad
- experiencia de usuario
- control de sesiones
- revocación de acceso
En este post vamos a ver qué son, cómo funcionan, por qué no son lo mismo que un JWT de acceso y cómo implementarlos en PHP y MySQL de forma clara, práctica y razonable.
La idea no es solo copiar código, sino que entiendas la lógica correcta detrás del sistema.
Qué es un refresh token
Un refresh token es un token especial que se utiliza para conseguir un nuevo access token cuando el token principal ha expirado.
La idea general del flujo es esta:
- el usuario hace login
- el servidor devuelve dos cosas:
- un access token
- un refresh token
- el access token se usa para acceder a rutas protegidas
- cuando el access token expira, el cliente usa el refresh token
- el servidor valida el refresh token y, si sigue siendo válido, emite un nuevo access token
De esta forma, no necesitas pedir usuario y contraseña cada vez que vence el token principal.
Diferencia entre access token y refresh token
Esta diferencia es clave.
Access token
Es el JWT que usas para acceder a la API.
Suele:
- durar poco
- viajar en peticiones protegidas
- contener identidad del usuario
- expirar pronto
Ejemplo típico:
- 15 minutos
- 30 minutos
- 1 hora
Refresh token
Es el token que usas solo para renovar el access token.
Suele:
- durar más
- usarse menos veces
- guardarse con más cuidado
- no enviarse en cada petición normal
- servir para pedir nuevos access tokens
Ejemplo típico:
- varios días
- una semana
- varias semanas, según el sistema
La lógica correcta suele ser:
- token de acceso corto
- token de refresco más largo
Por qué no basta con un JWT largo
Mucha gente al principio piensa:
“mejor hago que el JWT dure 30 días y ya”.
Eso simplifica algunas cosas, sí. Pero también empeora la seguridad.
Si un access token dura demasiado y alguien lo roba, esa persona tiene acceso durante mucho tiempo.
En cambio, con un access token corto:
- reduces ventana de riesgo
- limitas el impacto de filtraciones
- obligas a renovar más a menudo
- mantienes mejor control sobre la sesión
El refresh token existe precisamente para no sacrificar experiencia de usuario mientras mejoras seguridad.
Cómo funciona el flujo completo con refresh tokens
La lógica completa suele verse así:
1. Login
El usuario manda email y contraseña.
2. El servidor valida credenciales
Si son correctas:
- genera un access token
- genera un refresh token
- guarda el refresh token en base de datos
- responde ambos tokens
3. El cliente usa el access token
Lo manda en las peticiones protegidas.
4. El access token expira
La API responde algo como:
- 401 Unauthorized
- token expirado
5. El cliente llama al endpoint de refresh
Envía el refresh token.
6. El servidor valida el refresh token
Si es correcto:
- genera un nuevo access token
- opcionalmente genera también un nuevo refresh token
- actualiza lo necesario
7. El cliente sigue trabajando sin login manual
Y la sesión continúa de forma más fluida.
Ventajas de usar refresh tokens
Implementarlos bien tiene varias ventajas:
- mejor experiencia de usuario
- access tokens más cortos y seguros
- posibilidad de controlar sesiones
- posibilidad de revocar refresh tokens
- mejor equilibrio entre comodidad y seguridad
En APIs modernas, especialmente para apps móviles o frontend SPA, esto es muy habitual.
Riesgos si los implementas mal
También es importante entender esto:
un refresh token mal planteado puede convertirse en un problema serio.
Errores típicos:
- darles demasiada duración sin control
- no guardarlos en base de datos
- no permitir revocarlos
- reutilizarlos sin estrategia clara
- guardarlos de forma insegura
- no invalidarlos al cerrar sesión
Por eso conviene montar el sistema con cierta lógica, no como simple “token extra”.
Cómo plantearlo bien en PHP y MySQL
Una estrategia bastante razonable para muchos proyectos es esta:
- usar JWT como access token
- usar un refresh token aleatorio
- guardar refresh token en base de datos
- asociarlo a un usuario
- darle fecha de expiración
- permitir invalidarlo
- usar endpoint específico para renovar tokens
Esto suele ser mejor que usar un segundo JWT también como refresh token sin demasiado control, aunque ambas estrategias existen.
Para muchos sistemas hechos con PHP y MySQL, el refresh token aleatorio guardado en base de datos da bastante control.
Estructura de base de datos recomendada
Te conviene una tabla específica para refresh tokens.
Ejemplo:
CREATE TABLE refresh_tokens ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, token VARCHAR(255) NOT NULL UNIQUE, expires_at DATETIME NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, revoked TINYINT(1) DEFAULT 0, FOREIGN KEY (user_id) REFERENCES usuarios(id) ON DELETE CASCADE );
Qué guarda esta tabla
- a qué usuario pertenece el token
- cuál es el token
- cuándo expira
- si fue revocado o no
- cuándo se creó
Esto te da mucho más control que simplemente confiar en algo “estateless” para todo.
Por qué conviene guardar refresh tokens en base de datos
Porque eso te permite:
- invalidarlos manualmente
- revocarlos en logout
- controlar expiración
- detectar reutilización
- eliminar tokens viejos
- gestionar sesiones por dispositivo si más adelante quieres crecer
En cambio, si no los guardas, pierdes bastante capacidad de control.
Qué duración poner al access token y al refresh token
No hay una única regla universal, pero una estrategia muy razonable puede ser:
Access token
- 15 minutos
- 30 minutos
- 1 hora
Refresh token
- 7 días
- 15 días
- 30 días
Depende del proyecto, el tipo de cliente y el nivel de seguridad que necesites.
Para un sistema moderado:
- access token de 15 o 30 minutos
- refresh token de 7 días
suele ser una base sensata.
Implementación paso a paso
Vamos a plantear una implementación clara.
Paso 1: instalar JWT si no lo tienes
composer require firebase/php-jwt
Paso 2: tabla para refresh tokens
Ya la vimos arriba, pero aquí la dejamos lista de nuevo:
CREATE TABLE refresh_tokens ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, token VARCHAR(255) NOT NULL UNIQUE, expires_at DATETIME NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, revoked TINYINT(1) DEFAULT 0, FOREIGN KEY (user_id) REFERENCES usuarios(id) ON DELETE CASCADE );
Paso 3: generar access token y refresh token en login
Aquí partimos de que ya validaste email y password correctamente.
Ejemplo:
<?php
use Firebase\JWT\JWT;
$key = "clave_super_segura";
$payload = [
"user_id" => $user['id'],
"email" => $user['email'],
"exp" => time() + (15 * 60) // 15 minutos
];
$accessToken = JWT::encode($payload, $key, 'HS256');
$refreshToken = bin2hex(random_bytes(64));
$expiresAt = date('Y-m-d H:i:s', strtotime('+7 days'));
$stmt = $pdo->prepare("
INSERT INTO refresh_tokens (user_id, token, expires_at)
VALUES (?, ?, ?)
");
$stmt->execute([
$user['id'],
$refreshToken,
$expiresAt
]);
echo json_encode([
"access_token" => $accessToken,
"refresh_token" => $refreshToken
]);
Qué está pasando aquí
- se crea un JWT corto
- se genera un refresh token aleatorio largo
- se guarda en base de datos
- se devuelven ambos al cliente
Por qué usar random_bytes para el refresh token
Porque necesitas un token difícil de predecir y suficientemente aleatorio.
Usar algo como:
- ids simples
- timestamps
- strings previsibles
sería mala idea.
random_bytes es una forma mucho más segura de generar tokens aleatorios en PHP.
Paso 4: crear endpoint para refrescar token
Ahora necesitas un endpoint específico, por ejemplo:
POST /refresh
El cliente enviará el refresh token y el servidor validará si:
- existe
- no está revocado
- no ha expirado
Si pasa la validación, devuelves un nuevo access token.
Ejemplo:
<?php
use Firebase\JWT\JWT;
$key = "clave_super_segura";
$data = json_decode(file_get_contents("php://input"), true);
if (!isset($data['refresh_token'])) {
http_response_code(400);
echo json_encode(["error" => "Refresh token requerido"]);
exit;
}
$refreshToken = $data['refresh_token'];
$stmt = $pdo->prepare("
SELECT * FROM refresh_tokens
WHERE token = ?
AND revoked = 0
AND expires_at > NOW()
LIMIT 1
");
$stmt->execute([$refreshToken]);
$storedToken = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$storedToken) {
http_response_code(401);
echo json_encode(["error" => "Refresh token inválido o expirado"]);
exit;
}
$stmt = $pdo->prepare("
SELECT id, email
FROM usuarios
WHERE id = ?
LIMIT 1
");
$stmt->execute([$storedToken['user_id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
http_response_code(401);
echo json_encode(["error" => "Usuario no válido"]);
exit;
}
$payload = [
"user_id" => $user['id'],
"email" => $user['email'],
"exp" => time() + (15 * 60)
];
$newAccessToken = JWT::encode($payload, $key, 'HS256');
echo json_encode([
"access_token" => $newAccessToken
]);
Qué hace este endpoint
- recibe el refresh token
- lo busca en base de datos
- comprueba que no esté revocado
- comprueba que no haya expirado
- busca el usuario asociado
- genera nuevo access token
- lo devuelve
Con eso, el cliente puede seguir usando la API sin volver a loguearse manualmente.
¿Conviene regenerar también el refresh token?
Aquí hay dos enfoques.
Enfoque simple
Mantienes el mismo refresh token hasta que expire.
Ventajas:
- más simple
- menos lógica adicional
Enfoque más seguro
Cada vez que usas un refresh token:
- lo invalidas
- generas uno nuevo
- guardas el nuevo
- devuelves también el nuevo al cliente
Esto se llama a veces refresh token rotation.
Ventajas:
- más seguridad
- mejor capacidad para detectar reutilización maliciosa
Para empezar, puedes montar el sistema simple.
Pero si quieres hacerlo más serio, la rotación es una mejora muy buena.
Ejemplo de refresh token rotation
En vez de devolver solo el access token, podrías:
- revocar el refresh token usado
- generar uno nuevo
- insertarlo en base de datos
- devolver ambos tokens
Ejemplo simplificado:
<?php
$newRefreshToken = bin2hex(random_bytes(64));
$newExpiresAt = date('Y-m-d H:i:s', strtotime('+7 days'));
$pdo->prepare("
UPDATE refresh_tokens
SET revoked = 1
WHERE id = ?
")->execute([$storedToken['id']]);
$pdo->prepare("
INSERT INTO refresh_tokens (user_id, token, expires_at)
VALUES (?, ?, ?)
")->execute([
$user['id'],
$newRefreshToken,
$newExpiresAt
]);
echo json_encode([
"access_token" => $newAccessToken,
"refresh_token" => $newRefreshToken
]);
Esto ya es un enfoque mucho más robusto.
Cómo hacer logout correctamente con refresh tokens
Si usas refresh tokens, el logout debería invalidarlos.
Por ejemplo:
- el cliente manda el refresh token actual
- el servidor lo marca como revocado
Ejemplo:
<?php
$data = json_decode(file_get_contents("php://input"), true);
if (!isset($data['refresh_token'])) {
http_response_code(400);
echo json_encode(["error" => "Refresh token requerido"]);
exit;
}
$stmt = $pdo->prepare("
UPDATE refresh_tokens
SET revoked = 1
WHERE token = ?
");
$stmt->execute([$data['refresh_token']]);
echo json_encode([
"message" => "Sesión cerrada correctamente"
]);
Esto ayuda mucho a mantener control real sobre sesiones.
Dónde guardar el refresh token en el cliente
Aquí depende del tipo de aplicación.
En frontend SPA
Mucha gente debate entre:
- localStorage
- cookies seguras HttpOnly
Desde el punto de vista de seguridad, muchas veces se considera más prudente usar cookies seguras y HttpOnly para refresh tokens, porque reduces exposición frente a ciertos ataques de JavaScript.
En apps móviles
El almacenamiento depende del entorno, pero conviene usar mecanismos seguros del sistema.
En proyectos simples
Aunque técnicamente podrías usar localStorage, conviene entender que no es siempre la opción más fuerte de seguridad.
No hay que convertir este punto en guerra absoluta, pero sí merece criterio.
Qué errores evitar con refresh tokens
1. Guardar refresh tokens sin expiración
Mala idea. Necesitan fecha de vencimiento.
2. No guardarlos en base de datos
Pierdes capacidad de revocación y control.
3. Darles demasiada duración
Aumenta el riesgo si se filtran.
4. No invalidarlos en logout
Entonces la “sesión” sigue viva.
5. No diferenciar access token y refresh token
Cada uno cumple un rol distinto.
6. Mandar refresh token en todas las peticiones normales
No hace falta. Solo debe usarse para refrescar.
7. No usar HTTPS
Esto es crítico. Sin HTTPS, cualquier sistema de tokens queda mucho más expuesto.
Flujo completo de autenticación con refresh tokens
Login
Devuelves:
- access token corto
- refresh token largo
Requests normales
Usan:
- access token
Access token expira
El cliente llama:
/refresh
Refresh correcto
El servidor devuelve:
- nuevo access token
- opcionalmente nuevo refresh token
Logout
El servidor:
- revoca refresh token
Ese es el ciclo principal.
Cómo queda la arquitectura de endpoints
Tu sistema podría tener algo así:
POST /register POST /login POST /refresh POST /logout GET /profile
Eso ya es una base bastante seria para un backend moderno con PHP y MySQL.
Buenas prácticas recomendadas
- usa access tokens cortos
- usa refresh tokens más largos pero controlados
- guárdalos en base de datos
- añade expiración
- revócalos en logout
- considera rotación si quieres más seguridad
- usa HTTPS siempre
- no metas datos sensibles en el payload JWT
- registra eventos relevantes de autenticación si tu sistema crece
Cuándo conviene usar refresh tokens
Son especialmente útiles cuando:
- tienes frontend SPA
- tienes apps móviles
- usas APIs con sesiones largas
- quieres buena UX sin sacrificar seguridad
- necesitas renovar tokens automáticamente
Cuándo quizá no haga falta tanta complejidad
Si tu proyecto es:
- muy pequeño
- administrativo interno
- con sesiones tradicionales
- sin necesidad de API stateless moderna
quizá unas sesiones clásicas te basten.
No todo proyecto necesita JWT + refresh tokens.
Pero si ya estás construyendo APIs modernas, aprender esto sí vale muchísimo la pena.
Conclusión:
Los refresh tokens son una pieza muy importante en autenticación moderna porque te permiten combinar dos cosas que normalmente chocan entre sí:
- seguridad
- experiencia de usuario
Gracias a ellos, puedes usar access tokens cortos y más seguros sin obligar al usuario a iniciar sesión constantemente.
La clave está en implementarlos con lógica:
- diferenciarlos bien del access token
- guardarlos en base de datos
- poner expiración
- permitir revocación
- considerar rotación si buscas más seguridad
Si ya tienes login con JWT en PHP, este es uno de los siguientes pasos más valiosos para llevar tu backend a un nivel mucho más profesional.
Siguiente lectura recomendada:
- Cómo crear una API REST desde cero con PHP y MySQL
- Autenticación con JWT en PHP explicada fácil
- Sistema de login completo con JWT + PHP + MySQL
- Cómo conectar MySQL con PHP usando PDO
- Seguridad básica para APIs