Appearance
Authentication Methods ​
Overview ​
Password Login: Standard email/password form submitting to the
@nuxtjs/auth-nextlocal strategy. Handles JWT issuance, current-workspace cookie creation, and Amplitude identification on success.Google OAuth: Redirects to the configured
googleAuthUrlbackend endpoint. The provider completes the OAuth handshake and redirects back to/social-login?access_token=<token>&provider=google.Microsoft OAuth: Same redirect pattern as Google; the provider is stored in
sessionStoragebefore redirect and read back on the/social-logincallback because the Microsoft redirect does not carry aproviderquery param.Passkey / WebAuthn: Browser-native biometric login via the
@simplewebauthn/browserlibrary. 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-loginusing the same callback flow as OAuth.Branding & Domain Verification: Before the login form renders,
pages/index.vuecallsGET check-brandingusing theOriginheader to verify the domain and load white-label configuration. A failed check triggers a Nuxt 404 error page rather than showing the login form.Logout: The
$logoutplugin 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.vueandpages/social-login.vue - Redirects authenticated users to their current workspace dashboard
- Reads the
currentWorkspacecookie for the target route params
- Runs on
middleware/onlyAuth.js— Basic auth gate- Used on asset download proxy and other simple protected pages
- Saves referring URL to
$auth.$storagebefore 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
accessibleWorkspaceson the user object - Falls back to the correct workspace dashboard if the workspace ID in the route is wrong
- Redirects external users (
schemes/— Custom@nuxtjs/auth-nextschemes- 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-brandinginbeforeMountto 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
- Calls
pages/social-login.vue— OAuth and passkey callback page- Receives
access_tokenquery 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
- Receives
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 tilecomponents/svg/CollageMicrosoftIcon.vue— Microsoft logo for the social tilecomponents/svg/CollagePasskeyIcon.vue— Passkey fingerprint icon for the social tilecomponents/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-brandingAPI 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
currentWorkspacecookie 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.tokento 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 againAPI Integration ​
| Method | Endpoint | Purpose |
|---|---|---|
GET | check-branding | Domain verification and white-label branding config via Origin header |
POST | auth/login | Password login handled by @nuxtjs/auth-next local strategy |
GET | user | Hydrate $auth.user after token is set (called internally by $auth.fetchUser()) |
POST | /passkeys/login/options | Get WebAuthn challenge from the backend |
POST | /passkeys/login | Verify WebAuthn assertion, receive JWT |
POST | auth/logout | Server-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>Related Documentation ​
- File Upload — Upload system that also uses
onlyAuthmiddleware - Download & Export — Download system that uses
onlyAuthon the proxy page - External Upload — Guest upload flow with OTP authentication and external scheme
- Plugins — Helper — Full source of
$nextStep,$setAuthToken,$logout