Laravel 12 + Vue.js: JWT Token Refresh & Profile Page Magic

Author

Kritim Yantra

Jun 04, 2025

Laravel 12 + Vue.js: JWT Token Refresh & Profile Page Magic

πŸ”„ Why Refresh Tokens?

Imagine your JWT token is a concert ticket 🎫:

  • Access Token: Short-lived (1-2 hours), gets you in.
  • Refresh Token: Long-lived (7-30 days), gets you a new ticket when yours expires.

Without refresh logic, users get logged out constantly. Let’s fix that!


πŸ”§ Step 1: Update JWT Configuration

In .env, set token lifetimes:

JWT_TTL=60 # Access token expires in 60 minutes (for testing)  
JWT_REFRESH_TTL=10080 # Refresh token expires in 7 days (10080 minutes)

πŸ”„ Step 2: Create Refresh Token Endpoint

Add to routes/api.php:

Route::post('/refresh', [AuthController::class, 'refresh']);

Update AuthController.php:

public function refresh() {
    try {
        $newToken = auth()->refresh();
        return response()->json([
            'token' => $newToken,
            'refresh_token' => $newToken // Simplified for clarity
        ]);
    } catch (\Tymon\JWTAuth\Exceptions\TokenInvalidException $e) {
        return response()->json(['error' => 'Invalid token'], 401);
    }
}

⚑ Step 3: Auto-Refresh Tokens in Vue.js

Update your Axios setup (resources/js/app.js):

axios.interceptors.response.use(response => response, async error => {
  const originalRequest = error.config;
  
  // If token expired (401) and not already retrying
  if (error.response.status === 401 && !originalRequest._retry) {
    originalRequest._retry = true;
    
    try {
      // Attempt token refresh
      const refreshResponse = await axios.post('/api/refresh');
      const newToken = refreshResponse.data.token;
      
      // Update stored token
      localStorage.setItem('jwt_token', newToken);
      
      // Retry original request with new token
      originalRequest.headers.Authorization = `Bearer ${newToken}`;
      return axios(originalRequest);
      
    } catch (refreshError) {
      // Refresh failed - force logout
      localStorage.removeItem('jwt_token');
      window.location.href = '/login';
    }
  }
  return Promise.reject(error);
});

πŸ‘€ Step 4: Build the Profile Page

Create resources/js/components/Profile.vue:

<template>
  <div class="profile-card">
    <h1>πŸ‘‹ Hello, {{ user.name }}!</h1>
    <div class="details">
      <p><strong>πŸ“§ Email:</strong> {{ user.email }}</p>
      <p><strong>πŸ†” User ID:</strong> {{ user.id }}</p>
      <p><strong>πŸŽ‰ Member since:</strong> {{ formattedDate }}</p>
    </div>
    <button @click="logout" class="logout-btn">Log Out</button>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      user: {}
    };
  },
  computed: {
    formattedDate() {
      return new Date(this.user.created_at).toLocaleDateString();
    }
  },
  async mounted() {
    try {
      const response = await axios.get('/api/user');
      this.user = response.data;
    } catch (error) {
      console.error("Profile load error:", error);
    }
  },
  methods: {
    logout() {
      axios.post('/api/logout');
      localStorage.removeItem('jwt_token');
      this.$router.push('/login');
    }
  }
};
</script>

<style scoped>
.profile-card {
  max-width: 500px;
  margin: 2rem auto;
  padding: 2rem;
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.1);
  background: white;
}

.details {
  text-align: left;
  padding: 1.5rem;
  background: #f9fafb;
  border-radius: 8px;
  margin: 1.5rem 0;
}

.logout-btn {
  background: #ef4444;
  color: white;
  border: none;
  padding: 12px 24px;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
  transition: background 0.3s;
}

.logout-btn:hover {
  background: #dc2626;
}
</style>

πŸ›£οΈ Step 5: Add Vue Router (If Missing)

Install Vue Router:

npm install vue-router@4

Create resources/js/router.js:

import { createRouter, createWebHistory } from 'vue-router';
import Login from './components/Login.vue';
import Profile from './components/Profile.vue';

const routes = [
  { path: '/', redirect: '/profile' },
  { path: '/login', component: Login },
  { path: '/profile', component: Profile, meta: { requiresAuth: true } }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

// Auth protection
router.beforeEach((to, from, next) => {
  const hasToken = localStorage.getItem('jwt_token');
  if (to.meta.requiresAuth && !hasToken) {
    next('/login');
  } else {
    next();
  }
});

export default router;

Update app.js:

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

createApp(App).use(router).mount('#app');

πŸ”’ Step 6: Protect Laravel Profile Route

Ensure routes/api.php has:

Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

πŸš€ Step 7: Test Your Flow

  1. Log in β†’ Token saves to localStorage
  2. Visit /profile β†’ See your details!
  3. Wait 1 hour (or change JWT_TTL to 1 minute) β†’ Next API call auto-refreshes token
  4. Click "Log Out" β†’ Token destroyed

πŸŽ‰ Key Takeaways

  • Token Refresh: Silent authentication renewal
  • Profile Page: Personalized user data display
  • Security: Protected routes on frontend/backend
  • UX: Seamless session management

"Good authentication is like oxygen – users only notice it when it’s missing."


Tags

Comments

No comments yet. Be the first to comment!

Please log in to post a comment:

Sign in with Google

Related Posts