Skip to main content

Authentication Flow

Dokumentasi lengkap tentang alur autentikasi di MStore Dashboard, termasuk login, token refresh, dan logout.

Overview

MStore Dashboard menggunakan JWT authentication dengan strategi keamanan tinggi:
  • Access Token: Disimpan di memory (Pinia store)
  • Refresh Token: HttpOnly cookie (set oleh backend)

Login Flow

Login Implementation

// features/01_core/composables/useAuth.ts
export const useAuth = () => {
  const store = useAuthStore()
  const router = useRouter()
  const route = useRoute()

  const login = async (credentials: LoginCredentials) => {
    store.isLoading = true

    try {
      const response = await $fetch<LoginResponse>('/api/v1/auth/login', {
        method: 'POST',
        body: credentials,
        credentials: 'include', // Important for cookies
      })

      // Store access token in memory
      store.setToken(response.access_token)
      store.setUser(response.user)

      // Redirect to intended page or dashboard
      const redirectUrl = route.query.redirect as string
      router.push(redirectUrl || '/')

      return response
    } catch (error) {
      throw error
    } finally {
      store.isLoading = false
    }
  }

  return { login }
}

Token Refresh Flow

Token Refresh Implementation

// plugins/auth.client.ts
export default defineNuxtPlugin(() => {
  const authStore = useAuthStore()
  let refreshPromise: Promise<string> | null = null

  const apiFetch = $fetch.create({
    async onResponseError({ response, options }) {
      if (response.status !== 401) throw error

      // Prevent refresh loop
      if (options.url?.includes('/auth/refresh')) {
        authStore.logout()
        throw new Error('Session expired')
      }

      // Deduplicate refresh requests
      if (!refreshPromise) {
        refreshPromise = refreshToken()
      }

      try {
        const newToken = await refreshPromise
        // Retry original request
        return $fetch(response.url, {
          ...options,
          headers: {
            ...options.headers,
            Authorization: `Bearer ${newToken}`,
          },
        })
      } catch {
        authStore.logout()
        navigateTo('/login')
        throw error
      } finally {
        refreshPromise = null
      }
    },
  })

  const refreshToken = async (): Promise<string> => {
    const response = await $fetch<{ access_token: string }>(
      '/api/v1/auth/refresh-token',
      {
        method: 'POST',
        credentials: 'include',
      }
    )
    authStore.setToken(response.access_token)
    return response.access_token
  }

  return { provide: { api: apiFetch } }
})

Logout Flow

Logout Implementation

// features/01_core/store/auth.ts
export const useAuthStore = defineStore('auth', () => {
  const accessToken = ref<string | null>(null)
  const user = ref<User | null>(null)
  const isAuthenticated = ref(false)

  const logout = async () => {
    try {
      // Notify backend to invalidate refresh token
      await $fetch('/api/v1/auth/logout', {
        method: 'POST',
        credentials: 'include',
      })
    } catch {
      // Ignore errors - proceed with local logout
    } finally {
      // Clear local state
      accessToken.value = null
      user.value = null
      isAuthenticated.value = false

      // Redirect to login
      navigateTo('/login')
    }
  }

  return { accessToken, user, isAuthenticated, logout }
})

Route Protection Flow

Route Guard Implementation

// middleware/auth.global.ts
export default defineNuxtRouteMiddleware(async (to) => {
  const authStore = useAuthStore()

  // Public routes
  const publicRoutes = ['/login', '/register', '/forgot-password']
  if (publicRoutes.includes(to.path)) {
    // Redirect authenticated users away from auth pages
    if (authStore.isAuthenticated) {
      return navigateTo('/')
    }
    return
  }

  // Protected routes - require authentication
  if (!authStore.isAuthenticated) {
    return navigateTo({
      path: '/login',
      query: { redirect: to.fullPath },
    })
  }

  // Check token expiration
  if (authStore.isTokenExpired) {
    try {
      await refreshToken()
    } catch {
      return navigateTo('/login')
    }
  }
})

Session Recovery Flow

Session Recovery Implementation

// plugins/auth.client.ts
export default defineNuxtPlugin(async () => {
  const authStore = useAuthStore()

  // Try to recover session on app init
  if (authStore.isAuthenticated) {
    try {
      const response = await $fetch<{ access_token: string; user: User }>(
        '/api/v1/auth/refresh-token',
        {
          method: 'POST',
          credentials: 'include',
        }
      )

      authStore.setToken(response.access_token)
      if (response.user) {
        authStore.setUser(response.user)
      }
    } catch {
      // Session invalid - clear state
      authStore.logout()
    }
  }
})

Complete Auth State Machine

Security Considerations

  • Access Token: Memory only (not localStorage)
  • Refresh Token: HttpOnly cookie
  • Never expose tokens to JavaScript
  • Access Token: 15-30 minutes
  • Refresh Token: 7-30 days
  • Auto-refresh before expiration
  • All auth requests over HTTPS
  • Secure flag on cookies
  • SameSite=Strict atau Lax
  • Refresh token dalam HttpOnly cookie
  • Origin validation di backend
  • SameSite cookie attribute

Error Handling

ErrorUser ActionSystem Action
Invalid credentialsShow error messageClear form
Token expiredNone (auto-refresh)Refresh token
Refresh failedRedirect to loginClear all tokens
Network errorShow retry optionRetry with backoff
Account lockedShow support contactLog attempt

Next Steps