Panduan Integrasi Vue.js
Panduan ini membahas integrasi autentikasi DUMEG Identity pada aplikasi frontend berbasis Vue 3 (Composition API), pengelolaan status dengan Pinia, perlindungan rute via Vue Router, dan pola komunikasi yang aman.
Status Kode Referensi (Starter Template)
Contoh kode di bawah ini adalah kode referensi arsitektural (starter kit) untuk membantu pengembang membangun integrasi frontend. Kode ini belum diuji dalam aplikasi integrasi nyata maupun server Keycloak nyata, dan BUKAN paket siap-production secara langsung. Setiap tim wajib menyesuaikan dengan kebutuhan manajemen state, library HTTP client, dan arsitektur backend spesifik sebelum diaplikasikan ke lingkungan production.
1. Rekomendasi Pola Arsitektur: Backend-for-Frontend (BFF)
Dalam pengembangan aplikasi web modern yang memiliki API backend (seperti Laravel), pola Backend-for-Frontend (BFF) adalah standar arsitektur yang sangat direkomendasikan untuk integrasi DUMEG Identity.
flowchart LR
subgraph Browser["Browser Pengguna"]
Vue["Vue 3 SPA\n(Pinia + Vue Router)"]
end
subgraph Backend["Server Backend (BFF)"]
Laravel["Laravel Backend / Sanctum\n(Confidential Client)"]
Session[(Encrypted Session\nHttpOnly Cookie)]
end
subgraph IdP["DUMEG Identity"]
Keycloak["Keycloak Server\nlogin.dumeg.com"]
end
Vue <-->|"1. Cookie Session (HttpOnly, Secure)"| Laravel
Laravel <-->|"2. OIDC PKCE + Client Secret"| Keycloak
Laravel --- SessionMengapa Memilih Pola BFF?
| Kriteria | Pola Backend-for-Frontend (BFF) | Token di Browser (localStorage) |
|---|---|---|
| Penyimpanan Token | Tersimpan aman di server | Di localStorage browser |
| Resiko Serangan XSS | Terlindungi dari Eksfiltrasi Token (Cookie HttpOnly tak terbaca JS) | Sangat Rentan (Token mudah dicuri script pihak ketiga) |
| Kerahasiaan Secret | client_secret aman di backend | Tidak bisa menggunakan client secret |
| Kompleksitas Frontend | Sederhana (hanya memanggil endpoint API /api/user) | Rumit (parsing JWT, cek expiry, refresh loop di JS) |
Jangan Gunakan `localStorage` untuk Token Sensitif
Menyimpan access_token atau id_token di window.localStorage membuka celah fatal bagi serangan Cross-Site Scripting (XSS). Seluruh data identitas dapat diekstraksi oleh skrip pihak ketiga.
2. Pengelolaan State Autentikasi dengan Pinia
Buat Pinia store stores/auth.ts untuk melacak status login, profil pengguna, dan memicu alur login/logout:
// src/stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import axios from 'axios'
export interface UserProfile {
id: number
keycloak_id: string
name: string
email: string
roles?: string[]
}
export const useAuthStore = defineStore('auth', () => {
const user = ref<UserProfile | null>(null)
const isLoading = ref<boolean>(false)
const isInitialized = ref<boolean>(false)
const isAuthenticated = computed(() => !!user.value)
/**
* Mengambil data profil user aktif dari backend Laravel (BFF)
*/
async function fetchUser(): Promise<UserProfile | null> {
isLoading.value = true
try {
const response = await axios.get<UserProfile>('/api/user', {
headers: { Accept: 'application/json' },
withCredentials: true, // Wajib menyertakan cookie sesi
})
user.value = response.data
return user.value
} catch (error) {
user.value = null
return null
} finally {
isLoading.value = false
isInitialized.value = true
}
}
/**
* Mengarahkan browser ke rute inisiasi login SSO Laravel
*/
function loginWithDumeg() {
// Redirect langsung ke route inisiasi SSO di Laravel
window.location.href = '/auth/dumeg/login'
}
/**
* Logout dari backend dan Keycloak SSO
*/
async function logout() {
try {
// POST ke endpoint logout Laravel
await axios.post('/logout', {}, { withCredentials: true })
} finally {
user.value = null
window.location.href = '/login'
}
}
return {
user,
isLoading,
isInitialized,
isAuthenticated,
fetchUser,
loginWithDumeg,
logout,
}
})3. Proteksi Rute dengan Vue Router
Terapkan navigasi terproteksi (Navigation Guards) pada router/index.ts untuk memastikan hanya pengguna yang telah terotentikasi yang dapat membuka halaman tertentu:
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: () => import('@/views/HomeView.vue'),
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/LoginView.vue'),
meta: { guestOnly: true },
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/DashboardView.vue'),
meta: { requiresAuth: true },
},
{
path: '/profile',
name: 'Profile',
component: () => import('@/views/ProfileView.vue'),
meta: { requiresAuth: true },
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
// Global Navigation Guard
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
// Inisialisasi status user jika aplikasi baru dibuka
if (!authStore.isInitialized) {
await authStore.fetchUser()
}
// Jika rute butuh autentikasi dan user belum login
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
return next({
name: 'Login',
query: { redirect: to.fullPath },
})
}
// Jika user sudah login mencoba akses halaman login
if (to.meta.guestOnly && authStore.isAuthenticated) {
return next({ name: 'Dashboard' })
}
next()
})
export default router4. Penanganan Sesi Kedaluwarsa (Axios Interceptors)
Jika sesi backend kedaluwarsa atau tidak valid, tangkap respons 401 Unauthorized secara terpusat:
// src/plugins/axios.ts
import axios from 'axios'
import { useAuthStore } from '@/stores/auth'
import router from '@/router'
axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL || ''
axios.defaults.withCredentials = true
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) {
const authStore = useAuthStore()
authStore.user = null
if (router.currentRoute.value.meta.requiresAuth) {
router.push({
name: 'Login',
query: { session_expired: '1' }
})
}
}
return Promise.reject(error)
}
)5. Contoh Komponen Tampilan (Vue 3)
Komponen Tombol Login SSO (DumegLoginButton.vue)
<!-- src/components/DumegLoginButton.vue -->
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
function handleLogin() {
authStore.loginWithDumeg()
}
</script>
<template>
<button @click="handleLogin" class="dumeg-sso-btn" type="button">
<span class="icon">🔐</span>
<span class="label">Masuk dengan DUMEG Identity (SSO)</span>
</button>
</template>
<style scoped>
.dumeg-sso-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
padding: 12px 18px;
background-color: #0875c9;
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.1s ease;
}
.dumeg-sso-btn:hover {
background-color: #0284c7;
transform: translateY(-1px);
}
.dumeg-sso-btn:active {
transform: translateY(0);
}
</style>6. Skenario Khusus: Standalone SPA (Tanpa Backend)
Apabila aplikasi frontend Vue.js Anda adalah Single Page Application murni yang tidak memiliki backend server sendiri:
- Daftarkan client di Keycloak sebagai Public Client.
- Wajib aktifkan PKCE S256 pada Keycloak Client Settings.
- Gunakan library OIDC tersertifikasi seperti
oidc-client-tsyang mengelola perputaran token di memori aplikasi. - Hindari menyimpan refresh token di
localStorage: Jika halaman di-refresh, manfaatkan silent SSO session check via hidden iframe atau redirect cepat ke Keycloak yang memanfaatkan session cookieKEYCLOAK_IDENTITYaktif pengguna.