Headshot: Keith Nickas

Keith Nickas

Full-Stack Software Engineer
Wristband, an Identity Access Management Platform

Vue.js Client Auth SDK

This project is in progress – An open source Vue.js SDK for integrating Wristband authentication and authorization features into Vue.js applications, e.g. useWristbandStore() composable.

Wristband Auth Flow

The Wristband Vue.js SDK provides authentication and authorization functionality for Vue.js applications, allowing developers to easily integrate Wristband's identity and access management features into their projects.

The SDK provides a set of components, composables, and services that can be used to handle user authentication, manage user sessions, and enforce access control based on user roles and permissions that are defined in the Wristband platform.

It simplifies the process of implementing secure authentication flows, such as login, logout, and token refresh, while also providing features like multi-factor authentication and social login options.

By using the Wristband Vue.js SDK, developers can ensure that their applications are secure and compliant with industry standards for identity and access management.

The SDK is designed to be easy to use and integrate, allowing developers to focus on building their applications while leveraging the robust authentication capabilities provided by Wristband.

The SDK is open source and available on GitHub, with comprehensive documentation and examples to help developers get started quickly.

Example App Configuration

Here is an example of the initialization code for the Wristband Vue.js SDK to configure authentication settings for a Vue.js application.

This code sets up the SDK with the necessary configuration options, such as the login URL, session URL, and token URL. It also initializes the Pinia state management library and mounts the Vue.js application to the DOM. The code ensures that the authentication state is properly managed and that the user session is fetched upon initialization.

Code:
import { createApp } from "vue"; import { createPinia } from "pinia"; import { WristbandAuthStore } from "@wristband/vue-client-auth" import { router } from "./router"; import App from "./App.vue"; import './css/style.css'; const init = async () => { // Create a Pinia instance const pinia = createPinia(); const app = createApp(App); // Install Pinia into the Vue application app.use(pinia); // Now we can set up the router and mount the app app.use(router); app.mount('#app'); // Access the WristbandAuthStore and attach it to Pinia const wristbandAuth = WristbandAuthStore(pinia); // Configure the WristbandAuthStore with your settings, e.g., API endpoints await wristbandAuth.setConfig({ loginUrl: '/api/auth/login', sessionUrl: '/api/v1/session', tokenUrl: '/api/v1/token', }); // Attempt to fetch the current session on app startup await wristbandAuth.fetchSession(); }; // Initialize the app init();

The Client Data Store

The SDK includes a client-side data store using Pinia for state management which manages the user session. It contains a baseline set of properties that are used to track the authentication state of the user, such as whether they are logged in or not, their access token, and their refresh token. The data store also includes methods for updating and retrieving these properties, as well as for handling token expiration and refresh.

Code:
import { defineStore } from 'pinia' import { ref, computed, nextTick } from 'vue' import { AuthStatus, TokenResponse, type SessionResponse, AuthConfig } from '../types/auth-store' import apiClient from '../api/api-client' import { delay, is4xxError, resolveAuthProviderLoginUrl, validateAuthProviderSessionUrl, validateAuthProviderTokenUrl, } from '../utils/auth-store-utils' import { isUnauthorizedError } from '../utils/auth-utils' import { WristbandError } from '../error' import { WristbandErrorCode } from '../types/errors' const TOKEN_EXPIRATION_BUFFER_TIME_MS = 30000 const MAX_API_ATTEMPTS = 3 const API_RETRY_DELAY_MS = 100 const WristbandAuthStore = defineStore('wristbandAuth', () => { // State const authError = ref<WristbandError | null>(null) const isAuthenticated = ref(false) const isLoading = ref(true) const userId = ref('') const tenantId = ref('') const metadata = ref<Record<string, unknown>>({} as Record<string, unknown>) const tokenUrl = ref('') const accessToken = ref('') const accessTokenExpiresAt = ref<number>(0) const tokenRequestRef = ref<Promise<string> | null>(null) // Config (set these before calling fetchSession) const config = ref({ csrfCookieName: 'CSRF-TOKEN', csrfHeaderName: 'X-CSRF-TOKEN', disableRedirectOnUnauthenticated: false, loginUrl: '', sessionUrl: '', tokenUrl: '', transformSessionMetadata: undefined, onSessionSuccess: undefined, } as AuthConfig) // Derived const authStatus = computed(() => isLoading.value ? AuthStatus.LOADING : isAuthenticated.value ? AuthStatus.AUTHENTICATED : AuthStatus.UNAUTHENTICATED, ) // Actions async function setConfig(newConfig: Partial<typeof config.value>) { config.value = { ...config.value, ...newConfig } config.value.loginUrl = resolveAuthProviderLoginUrl(config.value.loginUrl) validateAuthProviderSessionUrl(config.value.sessionUrl) if (config.value.tokenUrl && config.value.tokenUrl.length > 0) { validateAuthProviderTokenUrl(config.value.tokenUrl) } } function updateMetadata(newMetadata: Record<string, unknown>) { metadata.value = { ...metadata.value, ...newMetadata } } function clearToken() { accessToken.value = '' accessTokenExpiresAt.value = 0 tokenRequestRef.value = null } async function clearAuthData() { isAuthenticated.value = false isLoading.value = true userId.value = '' tenantId.value = '' metadata.value = {} // Reset token URL clearToken() await nextTick() } async function getToken(): Promise<string> { const validatedTokenUrl = config.value.tokenUrl if (!validatedTokenUrl || !validatedTokenUrl.trim()) { throw new WristbandError(WristbandErrorCode.INVALID_TOKEN_URL, 'Token URL not configured') } if (!isAuthenticated.value) { throw new WristbandError(WristbandErrorCode.UNAUTHENTICATED, 'User is not authenticated') } // Check if we have a valid cached token (with 30 second buffer) if ( accessToken.value && accessTokenExpiresAt.value > Date.now() + TOKEN_EXPIRATION_BUFFER_TIME_MS ) { return accessToken.value } // If there's already a token request in flight, return that promise if (tokenRequestRef.value) { return tokenRequestRef.value } const tokenRequest = (async () => { let lastError: unknown try { for (let attempt = 1; attempt <= MAX_API_ATTEMPTS; attempt++) { try { const response = await apiClient.get<TokenResponse>(validatedTokenUrl, { csrfCookieName: config.value.csrfCookieName, csrfHeaderName: config.value.csrfHeaderName, }) const { accessToken: newToken, expiresAt } = response.data accessToken.value = newToken accessTokenExpiresAt.value = expiresAt return newToken } catch (error: unknown) { lastError = error if (isUnauthorizedError(error)) { accessToken.value = '' accessTokenExpiresAt.value = 0 throw new WristbandError( WristbandErrorCode.UNAUTHENTICATED, 'Token request unauthorized', error, ) } if (is4xxError(error)) { throw new WristbandError( WristbandErrorCode.TOKEN_FETCH_FAILED, 'Failed to fetch token', error, ) } if (attempt === MAX_API_ATTEMPTS) { break } // Wait before retrying (only for 5xx errors and network issues) await delay(API_RETRY_DELAY_MS) } } // All attempts failed, throw the last error throw new WristbandError( WristbandErrorCode.TOKEN_FETCH_FAILED, 'Failed to fetch token after multiple attempts', lastError, ) } finally { // Clear the token request reference tokenRequestRef.value = null } })() // Store the promise to prevent duplicate requests tokenRequestRef.value = tokenRequest return tokenRequest } async function fetchSession() { const validatedSessionUrl = config.value.sessionUrl let lastError: WristbandError | null = null for (let attempt = 1; attempt <= MAX_API_ATTEMPTS; attempt++) { try { const response = await apiClient.get<SessionResponse>(validatedSessionUrl, { csrfCookieName: config.value.csrfCookieName, csrfHeaderName: config.value.csrfHeaderName, }) const { userId: uid, tenantId: tid, metadata: rawMetadata, tokenUrl: tUrl } = response.data if (config.value.onSessionSuccess) { await Promise.resolve(config.value.onSessionSuccess(response.data)) } if (rawMetadata) { metadata.value = config.value.transformSessionMetadata ? config.value.transformSessionMetadata(rawMetadata as Record<string, unknown>) : (rawMetadata as Record<string, unknown>) } tenantId.value = tid || '' userId.value = uid || '' isAuthenticated.value = true isLoading.value = false tokenUrl.value = tUrl || '' await nextTick() } catch (error: unknown) { // Always bubble up invalid response errors (represents a dev configuration error) if (error instanceof WristbandError) { throw error } if (isUnauthorizedError(error)) { lastError = new WristbandError( WristbandErrorCode.UNAUTHENTICATED, 'User is not authenticated', error, ) break } // If it's a non-401 4xx error, bail early (don't retry client errors) if (is4xxError(error)) { lastError = new WristbandError( WristbandErrorCode.SESSION_FETCH_FAILED, 'Failed to fetch session', error, ) break } // If this is the last attempt, don't delay if (attempt === MAX_API_ATTEMPTS) { lastError = new WristbandError( WristbandErrorCode.SESSION_FETCH_FAILED, 'Failed to fetch session', error, ) break } await delay(API_RETRY_DELAY_MS) } } if (config.value.disableRedirectOnUnauthenticated) { authError.value = lastError isAuthenticated.value = false isLoading.value = false } } return { // State isAuthenticated, isLoading, userId, tenantId, metadata, tokenUrl, // Derived authStatus, authError, // Actions clearAuthData, clearToken, getToken, fetchSession, setConfig, updateMetadata, // Config config, } })

Learn More

To learn more about the Wristband Vue.js SDK, including installation instructions, configuration options, and usage examples, please visit the official GitHub repository and the wristband platform documentation.

Loading Calendly...