Skip to main content
POST
/
api
/
refresh
POST /api/refresh
curl --request POST \
  --url https://api.example.com/api/refresh \
  --header 'Authorization: <authorization>'
{
  "success": true,
  "token": "<string>"
}

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

Headers

Authorization
string
required
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

success
boolean
required
Siempre true si se ejecutó correctamente
token
string
required
Nuevo token JWT válido por 8 horas. Reemplaza al token anterior.Formato: {id}|{hash_aleatorio}Ejemplo: 5|xyz789newtoken123abc456def789ghi012

Ejemplos

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

1

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.
2

Revocación del token antiguo

$request->user()->currentAccessToken()->delete();
Elimina el token de la tabla personal_access_tokens.
3

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.
4

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

AspectoRefreshLogin
Requiere credencialesNoSí (user + password)
Token actualSe revocaN/A
Nuevo tokenSe generaSe genera
Retorna permisosNo
Retorna empresasNo
Retorna usuarioNo
UsoMantener sesión activaIniciar sesión
Requiere autenticaciónSí (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ódigoDescripciónCausa
200OKRefresh exitoso, nuevo token generado
401UnauthorizedToken expirado o inválido
429Too Many RequestsLímite de renovaciones alcanzado (si está implementado)
500Internal Server ErrorError 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);
    }
}

Build docs developers (and LLMs) love