@sp-uvb/vue
v0.1.0
Published
Official Vue.js composables for Universal Verification Broker (UVB)
Maintainers
Readme
@sp-uvb/vue
Official Vue.js composables for Universal Verification Broker (UVB). Provides type-safe, reactive authentication for Vue 3, Nuxt 3, and Quasar applications.
Features
- 🎯 Type-Safe Composables - Full TypeScript support with Vue 3
- 🔄 Reactive State - Automatic UI updates on auth state changes
- 💾 Auto-Persistence - Session tokens stored in localStorage/sessionStorage
- ⏰ Session Management - Automatic expiration handling
- 🔐 WebAuthn Support - Built-in helpers for biometric authentication
- 🚀 SSR Compatible - Works with Nuxt 3 server-side rendering
- 📦 Zero Configuration - Sensible defaults, fully customizable
Installation
npm install @sp-uvb/vue
# or
yarn add @sp-uvb/vue
# or
pnpm add @sp-uvb/vueQuick Start
Vue 3
<script setup lang="ts">
import { useUVB } from '@sp-uvb/vue';
const { session, isAuthenticated, isLoading, user, verify, logout } = useUVB({
tenantId: 'your-tenant-id',
uvbUrl: 'http://localhost:8080',
});
const handleLogin = async () => {
await verify('totp', '123456');
};
</script>
<template>
<div>
<div v-if="isLoading">Loading...</div>
<div v-else-if="isAuthenticated">
<p>Welcome, {{ user?.userId }}!</p>
<button @click="logout">Logout</button>
</div>
<div v-else>
<button @click="handleLogin">Login</button>
</div>
</div>
</template>Nuxt 3
Create a plugin to provide UVB globally:
// plugins/uvb.ts
import { useUVB } from '@sp-uvb/vue';
export default defineNuxtPlugin(() => {
const uvb = useUVB({
tenantId: useRuntimeConfig().public.uvbTenantId,
uvbUrl: useRuntimeConfig().public.uvbUrl,
});
return {
provide: {
uvb,
},
};
});Use in components:
<script setup lang="ts">
const { $uvb } = useNuxtApp();
const handleLogin = async () => {
await $uvb.verify('totp', '123456');
};
</script>
<template>
<div v-if="$uvb.isAuthenticated.value">
<p>Welcome, {{ $uvb.user.value?.userId }}!</p>
</div>
</template>Quasar
Create a boot file:
// boot/uvb.ts
import { boot } from 'quasar/wrappers';
import { useUVB } from '@sp-uvb/vue';
export default boot(({ app }) => {
const uvb = useUVB({
tenantId: process.env.UVB_TENANT_ID || '',
uvbUrl: process.env.UVB_URL || 'http://localhost:8080',
});
app.config.globalProperties.$uvb = uvb;
});API Reference
useUVB(options)
Main composable for authentication and session management.
Options
interface UseUVBOptions {
tenantId: string; // Required: Your UVB tenant ID
uvbUrl?: string; // Optional: UVB server URL (default: 'http://localhost:8080')
apiKey?: string; // Optional: API key for server-to-server auth
storage?:
| 'localStorage' // Optional: Storage type (default: 'localStorage')
| 'sessionStorage'
| 'memory';
storageKey?: string; // Optional: Storage key name (default: 'uvb_session_token')
autoValidate?: boolean; // Optional: Auto-validate on mount (default: true)
}Returns
interface UseUVBReturn {
// State
session: Ref<UVBSession | null>;
isAuthenticated: Readonly<Ref<boolean>>;
isLoading: Ref<boolean>;
error: Ref<Error | null>;
// Computed
user: Readonly<
Ref<{
userId: string;
tenantId: string;
sessionId: string;
factorsVerified: string[];
expiresAt: Date;
} | null>
>;
// Methods
validateSession: (token?: string) => Promise<UVBSession | null>;
verify: (
factorType: FactorType,
factorValue?: string,
metadata?: Record<string, string>
) => Promise<any>;
logout: () => Promise<void>;
refreshSession: () => Promise<UVBSession | null>;
hasFactors: (...factors: FactorType[]) => boolean;
}Usage Examples
Basic Authentication
<script setup>
import { useUVB } from '@sp-uvb/vue';
const { verify, isLoading, error } = useUVB({
tenantId: 'my-tenant',
});
const totpCode = ref('');
const handleVerify = async () => {
try {
const result = await verify('totp', totpCode.value);
if (result.success) {
console.log('Authenticated!');
}
} catch (err) {
console.error('Verification failed:', err);
}
};
</script>Check Verified Factors
<script setup>
import { useUVB } from '@sp-uvb/vue';
const { hasFactors, user } = useUVB({ tenantId: 'my-tenant' });
// Check if user has verified both TOTP and WebAuthn
const hasStrongAuth = computed(() => hasFactors('totp', 'webauthn'));
</script>
<template>
<div v-if="hasStrongAuth">Access to sensitive data granted</div>
</template>Session Refresh
<script setup>
import { useUVB } from '@sp-uvb/vue';
import { onMounted } from 'vue';
const { refreshSession, session } = useUVB({ tenantId: 'my-tenant' });
// Refresh session periodically
onMounted(() => {
setInterval(
async () => {
await refreshSession();
},
5 * 60 * 1000
); // Every 5 minutes
});
</script>useUVBFactor(options)
Composable for factor enrollment and management.
Usage
<script setup>
import { useUVBFactor } from '@sp-uvb/vue'
const {
enroll,
verify,
listFactors,
removeFactor,
enrollmentData,
isLoading
} = useUVBFactor({
tenantId: 'my-tenant'
})
// Enroll TOTP
const enrollTOTP = async (userId: string) => {
const result = await enroll(userId, 'totp')
if (result.success) {
console.log('QR Code:', enrollmentData.value?.qrCode)
console.log('Secret:', enrollmentData.value?.secret)
}
}
// List user's enrolled factors
const getUserFactors = async (userId: string) => {
const factors = await listFactors(userId)
console.log('Enrolled factors:', factors)
}
// Remove a factor
const removeUserFactor = async (userId: string, factorId: string) => {
await removeFactor(userId, factorId)
}
</script>
<template>
<div v-if="enrollmentData?.qrCode">
<img :src="enrollmentData.qrCode" alt="TOTP QR Code" />
<p>Secret: {{ enrollmentData.secret }}</p>
</div>
</template>useWebAuthn()
Composable for WebAuthn/FIDO2 authentication.
Usage
<script setup>
import { useWebAuthn } from '@sp-uvb/vue'
const {
isSupported,
isLoading,
error,
register,
authenticate
} = useWebAuthn()
// Register a new WebAuthn credential
const registerWebAuthn = async (challenge: string, userName: string, userId: string) => {
if (!isSupported.value) {
alert('WebAuthn not supported in this browser')
return
}
try {
const credential = await register(challenge, userName, userId)
console.log('Registered credential:', credential)
// Send credential to your backend for verification
} catch (err) {
console.error('Registration failed:', err)
}
}
// Authenticate with WebAuthn
const authenticateWebAuthn = async (challenge: string) => {
try {
const assertion = await authenticate(challenge)
console.log('Authentication assertion:', assertion)
// Send assertion to your backend for verification
} catch (err) {
console.error('Authentication failed:', err)
}
}
</script>
<template>
<div>
<div v-if="!isSupported">WebAuthn is not supported in your browser</div>
<button
v-else
@click="registerWebAuthn(challenge, '[email protected]', 'user123')"
:disabled="isLoading"
>
Register Biometric
</button>
</div>
</template>Advanced Examples
Protected Routes (Vue Router)
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import { useUVB } from '@sp-uvb/vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true },
},
],
});
router.beforeEach((to, from, next) => {
const { isAuthenticated } = useUVB({ tenantId: 'my-tenant' });
if (to.meta.requiresAuth && !isAuthenticated.value) {
next('/login');
} else {
next();
}
});
export default router;Nuxt 3 Middleware
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { $uvb } = useNuxtApp();
if (!$uvb.isAuthenticated.value) {
return navigateTo('/login');
}
});Use in pages:
<script setup>
definePageMeta({
middleware: 'auth',
});
</script>Quasar Route Guards
// router/routes.ts
const routes = [
{
path: '/dashboard',
component: () => import('pages/Dashboard.vue'),
beforeEnter: (to, from, next) => {
const { isAuthenticated } = useUVB({ tenantId: 'my-tenant' });
if (!isAuthenticated.value) {
next('/login');
} else {
next();
}
},
},
];Multi-Factor Authentication Flow
<script setup>
import { ref, computed } from 'vue';
import { useUVB, useUVBFactor } from '@sp-uvb/vue';
const { session, verify, hasFactors } = useUVB({ tenantId: 'my-tenant' });
const { enroll, enrollmentData } = useUVBFactor({ tenantId: 'my-tenant' });
const step = (ref < 'login') | 'totp-setup' | 'totp-verify' | ('complete' > 'login');
const email = ref('');
const password = ref('');
const totpCode = ref('');
const userId = ref('');
// Step 1: Email login
const handleEmailLogin = async () => {
const result = await verify('email_otp', email.value);
if (result.success) {
userId.value = result.userId;
step.value = hasFactors('totp') ? 'totp-verify' : 'totp-setup';
}
};
// Step 2a: Set up TOTP (first time)
const handleTOTPSetup = async () => {
await enroll(userId.value, 'totp');
step.value = 'totp-verify';
};
// Step 2b: Verify TOTP
const handleTOTPVerify = async () => {
const result = await verify('totp', totpCode.value);
if (result.success) {
step.value = 'complete';
}
};
</script>
<template>
<div>
<div v-if="step === 'login'">
<input v-model="email" placeholder="Email" />
<button @click="handleEmailLogin">Login</button>
</div>
<div v-else-if="step === 'totp-setup'">
<h3>Set up Two-Factor Authentication</h3>
<img v-if="enrollmentData?.qrCode" :src="enrollmentData.qrCode" />
<button @click="handleTOTPSetup">Continue</button>
</div>
<div v-else-if="step === 'totp-verify'">
<input v-model="totpCode" placeholder="6-digit code" />
<button @click="handleTOTPVerify">Verify</button>
</div>
<div v-else-if="step === 'complete'">
<h3>Welcome! You're authenticated.</h3>
</div>
</div>
</template>TypeScript Support
All composables are fully typed. Import types for better DX:
import type { UVBSession, FactorType, UseUVBReturn } from '@sp-uvb/vue';
const uvb: UseUVBReturn = useUVB({ tenantId: 'my-tenant' });
const session: Ref<UVBSession | null> = uvb.session;SSR Considerations
When using with Nuxt or other SSR frameworks:
- Storage is automatically disabled during SSR (falls back to memory)
- Auto-validation only runs on client-side
- Use
onMountedfor client-only auth logic
<script setup>
import { onMounted } from 'vue';
import { useUVB } from '@sp-uvb/vue';
const { validateSession } = useUVB({
tenantId: 'my-tenant',
autoValidate: false, // Disable auto-validate for manual control
});
onMounted(async () => {
// Only validate on client-side
await validateSession();
});
</script>License
MIT
