Passport

Examples & Use Cases

Practical examples and common use cases for Laravel Passport with Nuxt Umbu

Explore practical examples and common use cases for implementing Laravel Passport authentication with Nuxt Umbu.


Complete Authentication Flow

components/LoginForm.vue
<template>
  <div class="login-container">
    <form @submit.prevent="handleLogin" class="login-form">
      <h2>Sign In</h2>
      
      <!-- Error Display -->
      <div v-if="error" class="error-message">
        {{ error }}
        <button @click="error = ''" class="close-btn">×</button>
      </div>
      
      <!-- Regular Login Fields -->
      <div class="form-group">
        <label for="email">Email Address</label>
        <input
          id="email"
          v-model="credentials.email"
          type="email"
          :disabled="loading"
          placeholder="Enter your email"
          required
        />
      </div>
      
      <div class="form-group">
        <label for="password">Password</label>
        <input
          id="password"
          v-model="credentials.password"
          type="password"
          :disabled="loading"
          placeholder="Enter your password"
          required
        />
      </div>
      
      <div class="form-group checkbox">
        <label>
          <input v-model="credentials.remember" type="checkbox" />
          Remember me
        </label>
      </div>
      
      <button type="submit" :disabled="loading" class="submit-btn">
        <span v-if="loading">Signing in...</span>
        <span v-else>Sign In</span>
      </button>
      
      <div class="form-links">
        <NuxtLink to="/forgot-password">Forgot password?</NuxtLink>
        <NuxtLink to="/register">Create account</NuxtLink>
      </div>
    </form>
    
    <!-- 2FA Modal -->
    <Teleport to="body">
      <div v-if="show2FAModal" class="modal-overlay">
        <div class="modal">
          <h3>Two-Factor Authentication</h3>
          <p>Enter your 6-digit verification code:</p>
          
          <form @submit.prevent="handle2FA" class="2fa-form">
            <div class="code-inputs">
              <input
                v-for="(digit, index) in codeDigits"
                :key="index"
                v-model="codeDigits[index]"
                type="text"
                maxlength="1"
                @input="handleCodeInput(index, $event)"
                @keydown="handleKeydown(index, $event)"
                class="code-input"
                :disabled="twoFactorLoading"
              />
            </div>
            
            <div class="2fa-actions">
              <button type="submit" :disabled="twoFactorLoading" class="verify-btn">
                <span v-if="twoFactorLoading">Verifying...</span>
                <span v-else>Verify</span>
              </button>
              
              <button type="button" @click="cancel2FA" class="cancel-btn">
                Cancel
              </button>
            </div>
            
            <div class="backup-options">
              <button type="button" @click="useBackupCode" class="backup-btn">
                Use backup code
              </button>
            </div>
          </form>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<script setup>
const { loginWith, twoFactor, logout } = useAuth()

const credentials = ref({
  email: '',
  password: '',
  remember: false
})

const error = ref('')
const loading = ref(false)
const show2FAModal = ref(false)
const twoFactorLoading = ref(false)
const codeDigits = ref(['', '', '', '', '', ''])

const handleLogin = async () => {
  error.value = ''
  loading.value = true
  
  try {
    await loginWith('client', credentials.value)
    // Login successful, user will be redirected
  } catch (err) {
    if (err.statusCode === 423) {
      // 2FA required
      show2FAModal.value = true
    } else {
      error.value = getErrorMessage(err)
    }
  } finally {
    loading.value = false
  }
}

const handle2FA = async () => {
  const code = codeDigits.value.join('')
  if (code.length !== 6) {
    error.value = 'Please enter all 6 digits'
    return
  }
  
  twoFactorLoading.value = true
  
  try {
    await twoFactor('client', code)
    show2FAModal.value = false
    codeDigits.value = ['', '', '', '', '', '']
    // 2FA successful
  } catch (err) {
    error.value = 'Invalid verification code'
    codeDigits.value = ['', '', '', '', '', '']
  } finally {
    twoFactorLoading.value = false
  }
}

const handleCodeInput = (index, event) => {
  const value = event.target.value
  
  if (value && index < 5) {
    // Auto-focus next input
    const nextInput = event.target.parentElement.children[index + 1]
    nextInput?.focus()
  }
}

const handleKeydown = (index, event) => {
  if (event.key === 'Backspace' && !event.target.value && index > 0) {
    // Focus previous input on backspace
    const prevInput = event.target.parentElement.children[index - 1]
    prevInput?.focus()
  }
}

const cancel2FA = async () => {
  show2FAModal.value = false
  codeDigits.value = ['', '', '', '', '', '']
  await logout('client')
}

const useBackupCode = () => {
  // Implement backup code flow
  console.log('Use backup code')
}

const getErrorMessage = (err) => {
  switch (err.statusCode) {
    case 401: return 'Invalid email or password'
    case 422: return 'Please check your input'
    case 429: return 'Too many attempts. Try again later.'
    case 500: return 'Server error. Please try again.'
    default: return 'Login failed. Please try again.'
  }
}
</script>

<style scoped>
.login-container {
  max-width: 400px;
  margin: 0 auto;
  padding: 2rem;
}

.login-form {
  background: white;
  padding: 2rem;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.form-group {
  margin-bottom: 1rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
}

.form-group input {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

.checkbox label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.submit-btn {
  width: 100%;
  padding: 0.75rem;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 1rem;
  cursor: pointer;
}

.submit-btn:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.error-message {
  background: #f8d7da;
  color: #721c24;
  padding: 0.75rem;
  border-radius: 4px;
  margin-bottom: 1rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.close-btn {
  background: none;
  border: none;
  font-size: 1.2rem;
  cursor: pointer;
}

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal {
  background: white;
  padding: 2rem;
  border-radius: 8px;
  max-width: 400px;
  width: 90%;
}

.code-inputs {
  display: flex;
  gap: 0.5rem;
  justify-content: center;
  margin: 1.5rem 0;
}

.code-input {
  width: 3rem;
  height: 3rem;
  text-align: center;
  font-size: 1.5rem;
  border: 2px solid #ddd;
  border-radius: 4px;
}

.code-input:focus {
  border-color: #007bff;
  outline: none;
}

.2fa-actions {
  display: flex;
  gap: 1rem;
  margin-top: 1rem;
}

.verify-btn {
  flex: 1;
  padding: 0.75rem;
  background: #28a745;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.cancel-btn {
  padding: 0.75rem;
  background: #6c757d;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.backup-options {
  text-align: center;
  margin-top: 1rem;
}

.backup-btn {
  background: none;
  border: none;
  color: #007bff;
  text-decoration: underline;
  cursor: pointer;
}
</style>

Multi-Strategy Application

Admin and Customer Portals

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-umbu'],
  
  auth: {
    provider: 'passport',
    strategies: {
      admin: {
        endpoints: {
          login: { url: '/api/admin/login', method: 'POST' },
          user: { url: '/api/admin/profile', method: 'GET' },
          logout: { url: '/api/admin/logout', method: 'POST' },
          twoFactor: { url: '/api/admin/2fa', method: 'POST' }
        },
        redirect: {
          login: '/admin/dashboard',
          logout: '/admin/login'
        }
      },
      customer: {
        endpoints: {
          login: { url: '/api/customer/login', method: 'POST' },
          user: { url: '/api/customer/profile', method: 'GET' },
          logout: { url: '/api/customer/logout', method: 'POST' }
        },
        redirect: {
          login: '/customer/dashboard',
          logout: '/customer/login'
        }
      }
    }
  }
})
pages/admin/login.vue
<template>
  <div class="admin-login">
    <h1>Admin Portal</h1>
    <form @submit.prevent="handleAdminLogin">
      <input v-model="credentials.email" type="email" placeholder="Admin Email" />
      <input v-model="credentials.password" type="password" placeholder="Password" />
      <button type="submit">Login as Admin</button>
    </form>
  </div>
</template>

<script setup>
definePageMeta({
  layout: 'admin'
  // Note: Create custom middleware to redirect authenticated users from login pages
  // Example: middleware: (to) => { const { loggedIn } = useAuth(); if (loggedIn.value) return navigateTo('/admin/dashboard') }
})

const { loginWith } = useAuth()

const credentials = ref({
  email: '',
  password: ''
})

const handleAdminLogin = async () => {
  await loginWith('admin', credentials.value)
}
</script>
pages/customer/login.vue
<template>
  <div class="customer-login">
    <h1>Customer Portal</h1>
    <form @submit.prevent="handleCustomerLogin">
      <input v-model="credentials.email" type="email" placeholder="Email" />
      <input v-model="credentials.password" type="password" placeholder="Password" />
      <button type="submit">Login as Customer</button>
    </form>
  </div>
</template>

<script setup>
definePageMeta({
  layout: 'customer'
  // Note: Create custom middleware to redirect authenticated users from login pages
  // Example: middleware: (to) => { const { loggedIn } = useAuth(); if (loggedIn.value) return navigateTo('/customer/dashboard') }
})

const { loginWith } = useAuth()

const credentials = ref({
  email: '',
  password: ''
})

const handleCustomerLogin = async () => {
  await loginWith('customer', credentials.value)
}
</script>

API Integration

Protected API Calls

components/ProtectedData.vue
<template>
  <div class="protected-data">
    <h2>Protected Data</h2>
    
    <div v-if="loading" class="loading">
      Loading data...
    </div>
    
    <div v-else-if="error" class="error">
      {{ error }}
      <button @click="fetchData">Retry</button>
    </div>
    
    <div v-else class="data-content">
      <pre>{{ data }}</pre>
    </div>
  </div>
</template>

<script setup>
const { headers } = useAuthHeaders()

const data = ref(null)
const loading = ref(false)
const error = ref('')

const fetchData = async () => {
  loading.value = true
  error.value = ''
  
  try {
    const response = await $fetch('/api/protected/data', {
      headers: {
        // Auth headers are automatically included
        ...headers.value
      }
    })
    data.value = response
  } catch (err) {
    error.value = err.statusMessage || 'Failed to fetch data'
  } finally {
    loading.value = false
  }
}

onMounted(() => {
  fetchData()
})
</script>

Custom API Client

useApiClient.ts
export const useApiClient = () => {
  const { headers, fetchProfile } = useAuthHeaders()
  
  const apiCall = async (url, options = {}) => {
    try {
      const response = await $fetch(url, {
        ...options,
        headers: {
          ...headers.value,
          ...options.headers
        }
      })
      return response
    } catch (error) {
      if (error.statusCode === 401) {
        // Authentication failed, redirect to login
        await navigateTo('/login')
      }
      throw error
    }
  }
  
  return {
    apiCall
  }
}

Real-World Use Cases

E-commerce Application

pages/checkout.vue
<template>
  <div class="checkout">
    <h1>Checkout</h1>
    
    <!-- Guest Checkout -->
    <div v-if="!loggedIn" class="guest-checkout">
      <h2>Checkout as Guest</h2>
      <form @submit.prevent="handleGuestCheckout">
        <!-- Guest form fields -->
        <button type="submit">Continue as Guest</button>
      </form>
      
      <div class="login-prompt">
        <p>Or <NuxtLink to="/login">login</NuxtLink> for faster checkout</p>
      </div>
    </div>
    
    <!-- Authenticated Checkout -->
    <div v-else class="authenticated-checkout">
      <h2>Welcome back, {{ user?.name }}!</h2>
      
      <!-- Use saved addresses -->
      <div class="saved-addresses">
        <h3>Shipping Address</h3>
        <select v-model="selectedAddress">
          <option v-for="address in user?.addresses" :key="address.id" :value="address.id">
            {{ address.street }}, {{ address.city }}
          </option>
        </select>
      </div>
      
      <!-- Checkout form -->
      <form @submit.prevent="handleCheckout">
        <button type="submit">Place Order</button>
      </form>
    </div>
  </div>
</template>

<script setup>
definePageMeta({
  middleware: 'optional-auth' // Custom middleware
})

const { loggedIn, user, loginWith } = useAuth()

const selectedAddress = ref(null)

const handleGuestCheckout = () => {
  // Process guest checkout
  console.log('Guest checkout')
}

const handleCheckout = () => {
  // Process authenticated checkout
  console.log('Authenticated checkout', {
    user: user.value,
    address: selectedAddress.value
  })
}
</script>

SaaS Application

pages/billing/index.vue
<template>
  <div class="billing">
    <h1>Billing & Subscription</h1>
    
    <div v-if="!loggedIn" class="login-required">
      <p>Please <NuxtLink to="/login">login</NuxtLink> to manage your billing.</p>
    </div>
    
    <div v-else class="billing-content">
      <!-- Current Plan -->
      <div class="current-plan">
        <h2>Current Plan: {{ user?.subscription?.plan }}</h2>
        <p>Status: {{ user?.subscription?.status }}</p>
        <p>Next billing: {{ formatDate(user?.subscription?.next_billing) }}</p>
      </div>
      
      <!-- Usage Stats -->
      <div class="usage-stats">
        <h3>Usage This Month</h3>
        <div class="stat">
          <span>API Calls:</span>
          <span>{{ usage.api_calls }} / {{ limits.api_calls }}</span>
        </div>
        <div class="stat">
          <span>Storage:</span>
          <span>{{ formatBytes(usage.storage) }} / {{ formatBytes(limits.storage) }}</span>
        </div>
      </div>
      
      <!-- Upgrade Options -->
      <div class="upgrade-options">
        <h3>Upgrade Your Plan</h3>
        <div class="plans">
          <div v-for="plan in plans" :key="plan.id" class="plan-card">
            <h4>{{ plan.name }}</h4>
            <p>${{ plan.price }}/month</p>
            <ul>
              <li v-for="feature in plan.features" :key="feature">
                {{ feature }}
              </li>
            </ul>
            <button @click="upgradePlan(plan.id)" :disabled="plan.id === user?.subscription?.plan_id">
              {{ plan.id === user?.subscription?.plan_id ? 'Current Plan' : 'Upgrade' }}
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
definePageMeta({
  middleware: 'auth'
})

const { user, fetchProfile } = useAuth()

const usage = ref({
  api_calls: 850,
  storage: 1024 * 1024 * 500 // 500MB
})

const limits = computed(() => ({
  api_calls: user.value?.subscription?.limits?.api_calls || 1000,
  storage: user.value?.subscription?.limits?.storage || 1024 * 1024 * 1000 // 1GB
}))

const plans = ref([
  {
    id: 'basic',
    name: 'Basic',
    price: 9,
    features: ['1,000 API calls/month', '1GB storage', 'Email support']
  },
  {
    id: 'pro',
    name: 'Professional',
    price: 29,
    features: ['10,000 API calls/month', '10GB storage', 'Priority support', 'Advanced features']
  },
  {
    id: 'enterprise',
    name: 'Enterprise',
    price: 99,
    features: ['Unlimited API calls', '100GB storage', '24/7 support', 'Custom features']
  }
])

const formatDate = (date) => {
  return new Date(date).toLocaleDateString()
}

const formatBytes = (bytes) => {
  return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}

const upgradePlan = async (planId) => {
  try {
    await $fetch('/api/billing/upgrade', {
      method: 'POST',
      body: { plan_id: planId }
    })
    
    // Refresh user data
    await fetchProfile('client')
  } catch (error) {
    console.error('Upgrade failed:', error)
  }
}
</script>

Testing Examples

Authentication Testing

tests/auth.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import LoginForm from '~/components/LoginForm.vue'

describe('LoginForm', () => {
  let wrapper
  
  beforeEach(() => {
    wrapper = mount(LoginForm)
  })
  
  it('renders login form', () => {
    expect(wrapper.find('form').exists()).toBe(true)
    expect(wrapper.find('input[type="email"]').exists()).toBe(true)
    expect(wrapper.find('input[type="password"]').exists()).toBe(true)
  })
  
  it('validates required fields', async () => {
    const form = wrapper.find('form')
    await form.trigger('submit')
    
    expect(wrapper.find('.error-message').exists()).toBe(true)
    expect(wrapper.text()).toContain('required')
  })
  
  it('submits form with valid data', async () => {
    const emailInput = wrapper.find('input[type="email"]')
    const passwordInput = wrapper.find('input[type="password"]')
    
    await emailInput.setValue('test@example.com')
    await passwordInput.setValue('password123')
    
    const form = wrapper.find('form')
    await form.trigger('submit')
    
    // Assert login was called
    expect(wrapper.vm.loading).toBe(true)
  })
})

Integration Testing

tests/integration/auth-flow.test.ts
import { describe, it, expect } from 'vitest'
import { $fetch } from 'ofetch'

describe('Authentication Flow', () => {
  it('should authenticate user with valid credentials', async () => {
    const response = await $fetch('/api/login', {
      method: 'POST',
      body: {
        email: 'test@example.com',
        password: 'password123'
      }
    })
    
    expect(response.token).toBeDefined()
    expect(response.expires).toBeDefined()
  })
  
  it('should reject invalid credentials', async () => {
    try {
      await $fetch('/api/login', {
        method: 'POST',
        body: {
          email: 'invalid@example.com',
          password: 'wrongpassword'
        }
      })
    } catch (error) {
      expect(error.statusCode).toBe(401)
    }
  })
})

Congratulations! You now have a complete understanding of Laravel Passport integration with Nuxt Umbu. Check the main documentation for more information about other features and providers.