Authentication

Email/password and social login flows

Authentication

What's Included

  • Email/password registration and login
  • Google OAuth via Firebase
  • Forgot password / reset password flow
  • Email verification confirmation page
  • Auth middleware for protected routes
  • Guest middleware for auth-only pages
  • Automatic token refresh on 401 errors

Auth Composable (useAuth)

The useAuth composable handles all authentication flows:

const {
  register,     // (name, email, password) → registers user
  login,        // (email, password) → logs in user
  socialLogin,  // (idToken) → Firebase social auth
  logout,       // () → clears session
  forgotPassword, // (email) → sends reset email
  resetPassword,  // (token, password) → resets password
  refreshToken,   // () → refreshes JWT
  isLoading,    // Ref<boolean>
  error         // Ref<string | null>
} = useAuth()

All methods update the auth store on success and return readonly refs for reactivity.

Auth Store (useAuthStore)

Central state for authentication:

const authStore = useAuthStore()

authStore.token          // JWT access token
authStore.refreshToken   // Refresh token
authStore.user           // UserProfile | null
authStore.isAuthenticated // Computed boolean

Auth state is persisted to localStorage and rehydrated on mount via initAuth().

Pages

PageRouteMiddleware
Login/auth/loginguest
Register/auth/registerguest
Forgot Password/auth/forgot-passwordguest
Reset Password/auth/reset-passwordguest
Email Verified/auth/email-verified

Login Page

The login form accepts email and password. On success, it fetches the user profile and redirects to /profile. Includes a link to "Forgot password?" and a social login button.

Register Page

The registration form accepts name, email, and password. On success, the user is logged in immediately and redirected to /profile.

Social Login

The SocialLoginButton component handles the Firebase OAuth flow:

  1. User clicks "Sign in with Google"
  2. Firebase popup opens for Google OAuth
  3. On success, the Firebase ID token is sent to the backend
  4. Backend verifies the token and returns JWT credentials
  5. User is logged in and redirected

Firebase is optional. If no Firebase config is provided, the social login button is hidden.

Middleware

auth Middleware

Applied to protected pages (/profile, /settings, etc.). Checks authStore.isAuthenticated and redirects to /auth/login if not authenticated.

// In a page component:
definePageMeta({ middleware: 'auth' })

guest Middleware

Applied to auth pages (/auth/login, /auth/register, etc.). Redirects authenticated users to /profile.

Token Refresh

The useApi composable automatically handles token refresh:

  1. An API call returns 401 Unauthorized
  2. useApi attempts a single token refresh using the stored refresh token
  3. If refresh succeeds, the original request is retried with the new token
  4. If refresh fails, the user is logged out

End-to-End Flows

Registration Flow

1. User fills RegisterForm
   │
2. useAuth().register(name, email, password)
   │
3. POST /api/v1/auth/register
   │
4. Backend creates user + dispatches UserCreated event
   │
5. Welcome email sent (Mailpit in dev, Brevo in prod)
   │
6. Backend returns { token, refreshToken, userId }
   │
7. authStore.setAuth(token, refreshToken)
   │
8. useProfile().fetchProfile() → updates authStore.user
   │
9. Router navigates to /profile

Login Flow

1. User fills LoginForm
   │
2. useAuth().login(email, password)
   │
3. POST /api/v1/auth/login
   │
4. Backend verifies credentials
   │
5. Returns { token, refreshToken }
   │
6. authStore.setAuth(token, refreshToken)
   │
7. fetchProfile() → updates user state
   │
8. Router navigates to /profile

Social Login Flow (Firebase/Google)

1. User clicks SocialLoginButton
   │
2. Firebase popup opens (Google OAuth)
   │
3. User authenticates with Google
   │
4. Firebase returns idToken
   │
5. useAuth().socialLogin(idToken)
   │
6. POST /api/v1/auth/social { idToken, provider: 'google' }
   │
7. Backend verifies token with Firebase Admin SDK
   │   ├── New user → creates account (emailValidated: true)
   │   └── Existing user → issues new tokens
   │
8. Returns { token, refreshToken, isNewUser }
   │
9. authStore.setAuth() + fetchProfile()
   │
10. Router navigates to /profile

Token Refresh Flow

1. API call returns 401 Unauthorized
   │
2. useApi detects 401 response
   │
3. POST /api/v1/auth/token/refresh { refreshToken }
   │
4. Backend validates refresh token
   │   ├── Valid → returns new { token, refreshToken }
   │   └── Invalid/expired → returns 401
   │
5a. Success:
   │   ├── authStore.setAuth(newToken, newRefreshToken)
   │   └── Retry original request with new token
   │
5b. Failure:
       ├── authStore.clearAuth()
       └── Redirect to /auth/login

Password Reset Flow

1. User fills ForgotPasswordForm with email
   │
2. POST /api/v1/auth/forgot-password { email }
   │
3. Backend always returns 202 (prevents email enumeration)
   │
4. If email exists → sends reset email with tokenized link
   │
5. User clicks link in email
   │
6. GET /api/v1/auth/reset-password/{token}
   │
7. Backend validates token, redirects to frontend:
   │  {FRONTEND_RESET_PASSWORD_URL}?token={token}
   │
8. User fills ResetPasswordForm with new password
   │
9. POST /api/v1/auth/reset-password { token, password }
   │
10. Password is changed, PasswordChanged email sent

Email Verification Flow

1. User registers → welcome email sent
   │
2. Email contains validation link:
   │  /api/v1/auth/validate-email/{token}
   │
3. User clicks link
   │
4. Backend validates token, sets emailValidated: true
   │
5. Redirects to EMAIL_VALIDATION_REDIRECT_URL
   │  (typically /auth/email-verified)
   │
6. Frontend shows confirmation page

Session Persistence

Auth state is persisted to localStorage:

  • authStore.initAuth() runs on app mount (app.vue)
  • Reads token, refreshToken, and user from localStorage
  • If a token exists, sets isAuthenticated: true
  • Profile is fetched to validate the token is still valid

Firebase Setup

Firebase is used for Google OAuth (social login). It's optional — if no Firebase config is provided, the social login button is hidden automatically.

1. Get Firebase Configuration

  1. Go to Firebase Console and select your project
  2. Click the Gear iconProject settings
  3. Scroll to Your apps section (add a web app if you don't have one)
  4. Select the Config radio button and copy these values:
Firebase Config KeyEnvironment Variable
apiKeyNUXT_PUBLIC_FIREBASE_API_KEY
authDomainNUXT_PUBLIC_FIREBASE_AUTH_DOMAIN
projectIdNUXT_PUBLIC_FIREBASE_PROJECT_ID

These values are safe to expose client-side — they are restricted by your authorized domains.

2. Enable Google Sign-In

In Firebase Console → BuildAuthenticationSign-in method, ensure Google is enabled.

3. Add Authorized Domains (Critical)

Without this step, Firebase authentication will fail with auth/unauthorized-domain.

  1. In Firebase Console → BuildAuthenticationSettingsAuthorized domains
  2. Click Add domain
  3. Enter your production domain (e.g., yourdomain.com) — without https:// or trailing slashes
  4. Add any additional domains (staging, www subdomain, etc.)

localhost is included by default for local development.

Troubleshooting

auth/unauthorized-domain — Add your domain to Firebase authorized domains (see step 3 above).

auth/configuration-not-found — Verify all three NUXT_PUBLIC_FIREBASE_* environment variables are set and restart the application.

Google Sign-In not working — Ensure Google is enabled as a sign-in provider in Firebase Console → Build → Authentication → Sign-in method.