Descripción
Renueva el token de acceso del usuario autenticado sin requerir credenciales nuevamente. El token actual se revoca y se genera uno nuevo válido por 8 horas adicionales.
Características
Renovación sin credenciales : No requiere username/password
Revocación atómica : Token antiguo se elimina al generar el nuevo
Misma duración : Nuevo token válido por 8 horas
Mismo usuario : Mantiene sesión del mismo usuario
Sin pérdida de datos : Permisos y empresas se mantienen
Usa este endpoint cuando el token esté próximo a expirar pero el usuario sigue activo en la aplicación.
Request
Bearer token actual (debe ser válido) Authorization: Bearer 1|abc123def456ghi789jklmnopqrstuvwxyz
Si el token ya expiró, recibirás 401 y el usuario deberá hacer login completo.
Body
Este endpoint no requiere body (vacío).
Response
Siempre true si se ejecutó correctamente
Nuevo token JWT válido por 8 horas. Reemplaza al token anterior. Formato: {id}|{hash_aleatorio}Ejemplo: 5|xyz789newtoken123abc456def789ghi012
Ejemplos
cURL
Fetch API
Axios Interceptor
React Hook
Python (requests)
curl -X POST "https://tu-dominio.com/api/refresh" \
-H "Authorization: Bearer 1|abc123def456ghi789jklmnopqrstuvwxyz"
Respuestas
200 OK - Refresh Exitoso
{
"success" : true ,
"token" : "5|xyz789newtoken123abc456def789ghi012"
}
401 Unauthorized - Token Expirado o Inválido
{
"message" : "Unauthenticated."
}
Cuando recibes 401, el usuario debe hacer login completo con /api/login.
Implementación Interna
Código del Controlador
app/Http/Controllers/Api/AuthController.php:150
public function refresh ( Request $request )
{
$user = $request -> user ();
// Revocar token actual
$request -> user () -> currentAccessToken () -> delete ();
// Crear nuevo token
$token = $user -> createToken ( 'auth_token' , [ '*' ], now () -> addHours ( 8 )) -> plainTextToken ;
return response () -> json ([
'success' => true ,
'token' => $token
]);
}
Pasos de Ejecución
Autenticación
El middleware auth:sanctum valida el token actual: $user = $request -> user ();
Si el token es inválido, retorna 401 antes de llegar al controlador.
Revocación del token antiguo
$request -> user () -> currentAccessToken () -> delete ();
Elimina el token de la tabla personal_access_tokens.
Generación del nuevo token
$token = $user -> createToken (
'auth_token' , // Nombre del token
[ '*' ], // Scopes (todos los permisos)
now () -> addHours ( 8 ) // Expiración: 8 horas
) -> plainTextToken ;
Crea un nuevo token Sanctum con la misma duración.
Respuesta
Retorna el nuevo token en formato JSON.
Estrategias de Renovación
1. Renovación Manual
El usuario hace clic en un botón “Renovar Sesión”:
const handleRefresh = async () => {
const response = await fetch ( '/api/refresh' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ localStorage . getItem ( 'auth_token' ) } `
}
});
const data = await response . json ();
if ( data . success ) {
localStorage . setItem ( 'auth_token' , data . token );
alert ( 'Sesión renovada por 8 horas más' );
}
};
2. Renovación Automática por Tiempo
Renueva automáticamente cada 7 horas (antes de expirar):
// En el componente raíz
useEffect (() => {
const SEVEN_HOURS = 7 * 60 * 60 * 1000 ;
const refreshInterval = setInterval ( async () => {
const token = localStorage . getItem ( 'auth_token' );
if ( token ) {
try {
const response = await fetch ( '/api/refresh' , {
method: 'POST' ,
headers: { 'Authorization' : `Bearer ${ token } ` }
});
const data = await response . json ();
if ( data . success ) {
localStorage . setItem ( 'auth_token' , data . token );
console . log ( 'Token renovado automáticamente' );
}
} catch ( error ) {
console . error ( 'Error al renovar token:' , error );
}
}
}, SEVEN_HOURS );
return () => clearInterval ( refreshInterval );
}, []);
3. Renovación por Interceptor
Renueva solo cuando una petición recibe 401:
axios . interceptors . response . use (
( response ) => response ,
async ( error ) => {
const originalRequest = error . config ;
if ( error . response ?. status === 401 && ! originalRequest . _retry ) {
originalRequest . _retry = true ;
// Intentar refresh
const { data } = await axios . post ( '/api/refresh' , {}, {
headers: {
'Authorization' : `Bearer ${ localStorage . getItem ( 'auth_token' ) } `
}
});
localStorage . setItem ( 'auth_token' , data . token );
originalRequest . headers . Authorization = `Bearer ${ data . token } ` ;
return axios ( originalRequest );
}
return Promise . reject ( error );
}
);
4. Renovación por Actividad del Usuario
Renueva cuando detecta actividad reciente:
let lastActivity = Date . now ();
const ACTIVITY_TIMEOUT = 30 * 60 * 1000 ; // 30 minutos
const TOKEN_EXPIRY = 8 * 60 * 60 * 1000 ; // 8 horas
// Detectar actividad
[ 'click' , 'keypress' , 'scroll' , 'mousemove' ]. forEach ( event => {
document . addEventListener ( event , () => {
const now = Date . now ();
// Si ha pasado más de 30 min desde última actividad
if ( now - lastActivity > ACTIVITY_TIMEOUT ) {
refreshToken ();
}
lastActivity = now ;
}, { passive: true });
});
Comparación: Refresh vs Login
Aspecto Refresh Login Requiere credenciales No Sí (user + password) Token actual Se revoca N/A Nuevo token Se genera Se genera Retorna permisos No Sí Retorna empresas No Sí Retorna usuario No Sí Uso Mantener sesión activa Iniciar sesión Requiere autenticación Sí (token válido) No (público)
Refresh es más ligero porque solo retorna el nuevo token, no recarga permisos ni empresas.
Gestión de Expiración
Almacenar Tiempo de Expiración
// Al hacer login o refresh
const saveToken = ( token ) => {
const expiresAt = Date . now () + ( 8 * 60 * 60 * 1000 ); // 8 horas
localStorage . setItem ( 'auth_token' , token );
localStorage . setItem ( 'token_expires_at' , expiresAt );
};
// Verificar si está expirando
const isTokenExpiringSoon = () => {
const expiresAt = parseInt ( localStorage . getItem ( 'token_expires_at' ));
const oneHourFromNow = Date . now () + ( 60 * 60 * 1000 );
return expiresAt < oneHourFromNow ;
};
Mostrar Advertencia al Usuario
const checkTokenExpiry = () => {
const expiresAt = parseInt ( localStorage . getItem ( 'token_expires_at' ));
const timeLeft = expiresAt - Date . now ();
const minutesLeft = Math . floor ( timeLeft / 60000 );
if ( minutesLeft <= 5 && minutesLeft > 0 ) {
showNotification (
`Tu sesión expirará en ${ minutesLeft } minutos. ` +
`<button onclick="refreshToken()">Renovar ahora</button>`
);
}
};
// Verificar cada minuto
setInterval ( checkTokenExpiry , 60000 );
Seguridad
Rotación de Tokens
Cada refresh revoca el token anterior y genera uno nuevo:
// Token antiguo se elimina
$request -> user () -> currentAccessToken () -> delete ();
// Token nuevo se crea
$token = $user -> createToken ( 'auth_token' , [ '*' ], now () -> addHours ( 8 ));
Esto previene:
Reutilización de tokens antiguos
Ataques de replay
Sesiones duplicadas
Límite de Renovaciones
Considera implementar un límite de renovaciones: // Máximo 10 renovaciones por día
$refreshCount = Cache :: get ( "refresh_count_{ $user -> id }" , 0 );
if ( $refreshCount >= 10 ) {
return response () -> json ([
'success' => false ,
'message' => 'Límite de renovaciones alcanzado. Por favor, inicia sesión nuevamente.'
], 429 );
}
Cache :: put ( "refresh_count_{ $user -> id }" , $refreshCount + 1 , now () -> endOfDay ());
Códigos de Estado
Código Descripción Causa 200 OK Refresh exitoso, nuevo token generado 401 Unauthorized Token expirado o inválido 429 Too Many Requests Límite de renovaciones alcanzado (si está implementado) 500 Internal Server Error Error de base de datos
Mejores Prácticas
Renovación Proactiva Renueva el token 1 hora antes de que expire (a las 7 horas) para evitar interrupciones.
Manejo de Fallos Si refresh falla, redirige inmediatamente al login en lugar de reintentar.
Actualización Silenciosa No muestres notificaciones al usuario durante refresh automático, solo en caso de error.
Sincronización Multi-Pestaña Usa localStorage events para sincronizar tokens entre pestañas: window . addEventListener ( 'storage' , ( e ) => {
if ( e . key === 'auth_token' ) {
// Token actualizado en otra pestaña
location . reload ();
}
});
Testing
tests/Feature/RefreshTest.php
class RefreshTest extends TestCase
{
use RefreshDatabase ;
public function test_can_refresh_valid_token ()
{
$user = User :: factory () -> create ();
$oldToken = $user -> createToken ( 'test' ) -> plainTextToken ;
$response = $this -> withHeaders ([
'Authorization' => 'Bearer ' . $oldToken
]) -> postJson ( '/api/refresh' );
$response -> assertStatus ( 200 )
-> assertJsonStructure ([
'success' ,
'token'
]);
$newToken = $response -> json ( 'token' );
$this -> assertNotEquals ( $oldToken , $newToken );
// Token antiguo debe estar revocado
$this -> withHeaders ([
'Authorization' => 'Bearer ' . $oldToken
]) -> getJson ( '/api/me' )
-> assertStatus ( 401 );
// Token nuevo debe funcionar
$this -> withHeaders ([
'Authorization' => 'Bearer ' . $newToken
]) -> getJson ( '/api/me' )
-> assertStatus ( 200 );
}
public function test_cannot_refresh_expired_token ()
{
$user = User :: factory () -> create ();
$token = $user -> createToken ( 'test' , [ '*' ], now () -> subHour ()) -> plainTextToken ;
$response = $this -> withHeaders ([
'Authorization' => 'Bearer ' . $token
]) -> postJson ( '/api/refresh' );
$response -> assertStatus ( 401 );
}
}