Skip to content

Authentication Methods ​

Overview ​

  1. Password Login: Standard email/password form submitting to the @nuxtjs/auth-next local strategy. Handles JWT issuance, current-workspace cookie creation, and Amplitude identification on success.

  2. Google OAuth: Redirects to the configured googleAuthUrl backend endpoint. The provider completes the OAuth handshake and redirects back to /social-login?access_token=<token>&provider=google.

  3. Microsoft OAuth: Same redirect pattern as Google; the provider is stored in sessionStorage before redirect and read back on the /social-login callback because the Microsoft redirect does not carry a provider query param.

  4. Passkey / WebAuthn: Browser-native biometric login via the @simplewebauthn/browser library. The backend issues a challenge, the browser completes the ceremony, and the resulting assertion is verified server-side. The token is then handed to /social-login using the same callback flow as OAuth.

  5. Branding & Domain Verification: Before the login form renders, pages/index.vue calls GET check-branding using the Origin header to verify the domain and load white-label configuration. A failed check triggers a Nuxt 404 error page rather than showing the login form.

  6. Logout: The $logout plugin helper tracks the event in Amplitude, flushes Session Replay, calls $auth.logout(), and clears auth cookies.

Architecture ​

The login system is built on @nuxtjs/auth-next with the local JWT strategy defined in nuxt.config.js. Two custom schemes in schemes/ extend the default behavior for external (guest) users. The $auth object is injected globally by the plugin and manages token storage, user fetching, and strategy switching.

All post-login routing is delegated to $nextStep(), defined in plugins/helper.js. It inspects $auth.user.workspace_id and the currentWorkspace cookie to redirect to the correct workspace dashboard or to the workspace-creation flow if none exists.

Social login and passkey flows both converge on pages/social-login.vue, which receives an access token via query string. The page sets the token on the auth strategy, calls $auth.fetchUser(), and then invokes $nextStep(). This keeps post-login setup — workspace cookie, Amplitude identification, branding reset — in a single code path regardless of the login method used.

The middleware/guestCheck.js guard runs on the login page and /social-login to redirect already-authenticated users away from these pages. Protected workspace routes use middleware/authCheck.js, which also handles external-user redirects and workspace-access validation. The simpler middleware/onlyAuth.js is used on specific pages (such as the asset download proxy) that need only a basic auth gate.

File Structure ​

JavaScript Files (.js) ​

  • plugins/helper.js — Auth helper functions injected globally

    • $nextStep() — post-login redirect logic based on workspace state and saved referring URL
    • $setAuthToken() — pushes the current JWT to Axios defaults via $axios.setToken()
    • $logout() — Amplitude tracking, Session Replay flush, $auth.logout(), cookie clear
  • middleware/guestCheck.js — Guest redirect middleware

    • Runs on pages/index.vue and pages/social-login.vue
    • Redirects authenticated users to their current workspace dashboard
    • Reads the currentWorkspace cookie for the target route params
  • middleware/onlyAuth.js — Basic auth gate

    • Used on asset download proxy and other simple protected pages
    • Saves referring URL to $auth.$storage before redirecting unauthenticated users to /
  • middleware/authCheck.js — Full workspace-aware auth gate

    • Redirects external users ($auth.user.is_external) to the external upload page
    • Validates workspace ID against accessibleWorkspaces on the user object
    • Falls back to the correct workspace dashboard if the workspace ID in the route is wrong
  • schemes/ — Custom @nuxtjs/auth-next schemes

    • External-user auth scheme for guest/OTP sessions
    • Extends the default local strategy with custom token handling for non-admin users

Vue Component Files (.vue) ​

  • pages/index.vue — Login page

    • Calls GET check-branding in beforeMount to verify the domain and load white-label config
    • Renders the email/password form with @submit.prevent="handleSubmit"
    • Conditionally shows Google, Microsoft, and Passkey tiles based on origin matching $config.baseUrl
    • Delegates all social and passkey flows to their respective handler methods
  • pages/social-login.vue — OAuth and passkey callback page

    • Receives access_token query param after OAuth or passkey verification
    • Sets the token on $auth.strategy.token, calls $auth.fetchUser()
    • Triggers $setAuthToken(), $setCurrentWorkspace(), Amplitude identification, then $nextStep()
    • Renders provider-specific loading UI for Google, Microsoft, and Passkey
  • pages/forgot-password.vue — Password reset request page

    • Sends a reset link to the provided email via backend API
  • pages/reset-password.vue — Password reset completion page

    • Accepts token from the email link and sets the new password

SVG Icon Components ​

  • components/svg/CollageGoogleIcon.vue — Google "G" logo for the social tile
  • components/svg/CollageMicrosoftIcon.vue — Microsoft logo for the social tile
  • components/svg/CollagePasskeyIcon.vue — Passkey fingerprint icon for the social tile
  • components/svg/CollageLogoIcon.vue — Collage wordmark shown on the default login page

Login Page Component ​

File: pages/index.vue

The entry point for all authentication. Handles domain verification, white-label branding, and all login method entry points from a single page component.

Features ​

  • Domain verification via check-branding API before the form renders
  • White-label logo and brand name support from the branding API response
  • Email/password form with client-side validation before submit
  • Social login tiles for Google, Microsoft, and Passkey — shown only when the current origin matches $config.baseUrl
  • Duplicate-session guard: checks the currentWorkspace cookie before any login attempt
  • Amplitude tracking on login success and failure with method label

Props ​

The login page is a Nuxt page component and accepts no props. Configuration comes from Nuxt runtime config ($config.googleAuthUrl, $config.microsoftAuthUrl, $config.baseUrl).

Events ​

javascript
{
  // beforeDestroy lifecycle hook dispatches analytics/setEmail
  // with the email address the user typed in the form
}

Methods ​

javascript
{
  handleSubmit()         // password login via $auth.loginWith('local', { data })
  googleSignIn()         // redirect to $config.googleAuthUrl
  microsoftSignIn()      // set sessionStorage login_provider, redirect to $config.microsoftAuthUrl
  passkeyLogin()         // WebAuthn ceremony via @simplewebauthn/browser, then /social-login
  checkIfAlreadyLogin()  // reads currentWorkspace cookie and $auth.loggedIn
  validateEmail()        // sets errors.email using validateEmailAddr utility
  validatePass()         // sets errors.password with minimum length check
}

Usage ​

vue
<template>
  <!-- pages/index.vue is the login route; Nuxt mounts it automatically at '/' -->
  <!-- No parent component imports it directly -->
</template>

<script>
export default {
  middleware: ['guestCheck'],
  // guestCheck redirects already-authenticated users before the page renders
}
</script>

Social Login Callback Component ​

File: pages/social-login.vue

Receives the access token from all non-password login methods and completes the session setup in a unified code path.

Features ​

  • Provider-aware loading UI: Google spinner in blue, Microsoft in gray, Passkey in brand indigo
  • Sets bearer token directly on $auth.strategy.token to avoid a second login round-trip
  • Calls $auth.fetchUser() to hydrate the full user object
  • Triggers Amplitude identification and login tracking with the resolved provider name
  • Redirects back to / with a snackbar error if the token is missing or user fetch fails

Props ​

Accepts no props. Reads access_token and provider from $route.query. Reads login_provider from sessionStorage as a fallback for Microsoft (where the backend redirect carries no provider param).

Methods ​

javascript
{
  // All logic is in the mounted() lifecycle hook
  // provider computed from $route.query.provider || sessionStorage.login_provider
}

Usage ​

vue
<!-- Nuxt redirects here from the backend OAuth callback: -->
<!-- /social-login?access_token=<jwt>&provider=google -->
<!-- Microsoft carries no provider param — sessionStorage.login_provider is the fallback -->
<!-- Passkey: $router.replace('/social-login', { query: { access_token, provider: 'passkey' } }) -->

Auth Middleware ​

guestCheck ​

File: middleware/guestCheck.js

Runs on pages/index.vue and pages/social-login.vue. If the user is already authenticated and has a currentWorkspace cookie, redirects to the route stored in that cookie's user.redirectPathName with the workspace ID — preventing double-login and the branding state from getting stuck on the login page version.

onlyAuth ​

File: middleware/onlyAuth.js

Minimal auth gate used on pages that simply require a logged-in user (such as pages/get_assets.vue). Saves the referring route name, params, and query to $auth.$storage before redirecting unauthenticated users to / so $nextStep() can restore navigation after login.

authCheck ​

File: middleware/authCheck.js

Full workspace-aware guard used on all DAM pages. Checks is_external to redirect guest users to the external upload flow. Validates the workspace ID in the route against $auth.user.accessibleWorkspaces and redirects to the correct workspace dashboard if there is a mismatch, rather than showing a 403 or blank page.

Workflows ​

Password Login ​

1. User navigates to /
   Middleware: guestCheck.js — redirects away if already logged in
   ↓
2. beforeMount: GET check-branding
   Domain verified via Origin header
   On success: commits to dam/brandingDetails, sets contentLoading = false
   On domain error (402/403/404): $nuxt.error({ statusCode: 404 })
   ↓
3. User fills email + password, clicks Login
   Method: handleSubmit()
   Guard: checkIfAlreadyLogin() prevents duplicate sessions
   Calls: $auth.loginWith('local', { data: { email, password } })
   On 401/422: $snackbar.error(), Amplitude 'user login-failed' with status
   ↓
4. On success:
   $auth.$storage.setUniversal('externalUser', false)
   $setAuthToken()                → pushes JWT to Axios default headers
   $setCurrentWorkspace(id)       → writes currentWorkspace cookie
   $amplitude.identifyFromAuthUser($auth.user)
   $amplitude.track('user logged-in', { method: 'local' })
   $store.commit('dam/brandingDetails', null)   → clears login-page branding
   $store.dispatch('dam/setBranding', { workspace_id, isSetFavicon: true })
   ↓
5. $nextStep()
   workspace_id === 0  → redirect('/create-workspace')
   workspace_id > 0, referring URL saved → restore referring URL route
   workspace_id > 0, no referring URL → redirect(user.redirectPathName, { workspace_id })

Google / Microsoft OAuth ​

1. User clicks Google or Microsoft tile
   Method: googleSignIn() or microsoftSignIn()
   Microsoft only: sessionStorage.setItem('login_provider', 'microsoft')
   window.location.href = $config.googleAuthUrl  OR  $config.microsoftAuthUrl
   ↓
2. Browser redirects to backend OAuth endpoint
   Backend handles consent screen and authorization code exchange
   ↓
3. Backend redirects browser to /social-login
   Google:    /social-login?access_token=<jwt>&provider=google
   Microsoft: /social-login?access_token=<jwt>  (no provider in URL)
   ↓
4. pages/social-login.vue created() hook runs
   Microsoft: storedProvider = sessionStorage.login_provider; removes it
   ↓
5. $auth.strategy.token.set('Bearer <access_token>')
   $auth.fetchUser()
   On error: $snackbar.error('Signin failed'), $logout(), redirect('/')
   ↓
6. $setAuthToken()
   $setCurrentWorkspace($auth.user.workspace_id)
   $amplitude.identifyFromAuthUser($auth.user)
   $amplitude.track('user logged-in', { method: 'google' | 'microsoft' })
   ↓
7. $nextStep() → workspace dashboard (same logic as password login step 5)

Passkey / WebAuthn Login ​

1. User clicks Passkey tile
   Method: passkeyLogin()
   Guard: passkeyLoading flag prevents concurrent triggers
   ↓
2. POST /passkeys/login/options
   No body required
   Returns: WebAuthn PublicKeyCredentialRequestOptionsJSON (challenge, rpId, etc.)
   ↓
3. startAuthentication({ optionsJSON })   — @simplewebauthn/browser
   Browser presents Face ID / fingerprint / security key UI
   NotAllowedError or AbortError → return silently (user cancelled — not an error)
   ↓
4. POST /passkeys/login  (body: assertion object from browser ceremony)
   Returns: { data: { access_token: '<jwt>' } }
   On missing token: $snackbar.error('Passkey sign-in failed')
   ↓
5. $router.replace({ path: '/social-login', query: { access_token, provider: 'passkey' } })
   Continues via the social-login callback flow (steps 4–7 of OAuth flow above)

Logout ​

1. $logout() called (from any page or component)
   Guard: $auth.loggedIn must be true or call is a no-op
   ↓
2. $amplitude.track('user logged-out', {}, { immediate: true })
   $amplitude.reset()   → flushes Session Replay, ends Amplitude session
   Must run before $auth.logout() which may redirect immediately
   ↓
3. $auth.logout()
   Calls backend logout endpoint to invalidate the server-side session
   May redirect to '/' immediately — code after this line may not run
   ↓
4. $clearAuthCookies()
   Removes currentWorkspace cookie and any other auth-related cookies
   ↓
5. User lands on /  (login page)
   guestCheck middleware does not redirect (user is now logged out)
   beforeMount runs check-branding to initialize domain branding again

API Integration ​

MethodEndpointPurpose
GETcheck-brandingDomain verification and white-label branding config via Origin header
POSTauth/loginPassword login handled by @nuxtjs/auth-next local strategy
GETuserHydrate $auth.user after token is set (called internally by $auth.fetchUser())
POST/passkeys/login/optionsGet WebAuthn challenge from the backend
POST/passkeys/loginVerify WebAuthn assertion, receive JWT
POSTauth/logoutServer-side session invalidation

check-branding Response ​

json
{
  "status": true,
  "data": {
    "verify": true,
    "logo": "https://cdn.example.com/logo.png",
    "brand_name": "Acme Corp",
    "is_branded": true
  }
}

Passkey Login Options Response ​

json
{
  "data": {
    "challenge": "base64url-encoded-challenge",
    "rpId": "app.collage.inc",
    "allowCredentials": [],
    "userVerification": "required",
    "timeout": 60000
  }
}

Passkey Verify Response ​

json
{
  "data": {
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
  }
}

Component Integration ​

vue
<template>
  <div>
    <!-- Sign-out button usable from any authenticated page -->
    <v-btn :ripple="false" class="btn-gray-invert" @click="handleLogout">
      Sign out
    </v-btn>
  </div>
</template>

<script>
export default {
  methods: {
    async handleLogout() {
      try {
        await this.$logout()
        // $auth.logout() redirects to '/' — code below may not execute
      } catch (e) {
        this.$snackbar.error(this.$getErrorMessage(e))
      }
    },
  },
}
</script>