Learn how to implement user authentication in a Vue application using Nhost
This tutorial part builds upon the Protected Routes part by adding complete email/password authentication with email verification functionality. You’ll implement sign up, sign in, email verification, and sign out features to create a full authentication flow.
This is Part 3 in the Full-Stack Vue Development with Nhost series. This part creates a production-ready authentication system with secure email verification and proper error handling.
Build a comprehensive sign-in form with proper error handling and loading states. This page handles user authentication and includes special logic for post-verification sign-in.
src/views/SignIn.vue
Copy
Ask AI
<template> <div> <h1>Sign In</h1> <form @submit.prevent="handleSubmit" class="auth-form"> <div class="auth-form-field"> <label :for="emailId">Email</label> <input :id="emailId" type="email" v-model="email" required class="auth-input" /> </div> <div class="auth-form-field"> <label :for="passwordId">Password</label> <input :id="passwordId" type="password" v-model="password" required class="auth-input" /> </div> <div v-if="error" class="auth-error"> {{ error }} </div> <button type="submit" :disabled="isLoading" class="auth-button secondary" > {{ isLoading ? "Signing In..." : "Sign In" }} </button> </form> <div class="auth-links"> <p> Don't have an account? <router-link to="/signup">Sign Up</router-link> </p> </div> </div></template><script setup lang="ts">import { onMounted, ref, useId } from "vue";import { useRouter } from "vue-router";import { useAuth } from "../lib/nhost/auth";const { nhost, isAuthenticated } = useAuth();const router = useRouter();const email = ref("");const password = ref("");const isLoading = ref(false);const error = ref<string | null>(null);const emailId = useId();const passwordId = useId();// Use onMounted for navigation after authentication is confirmedonMounted(() => { if (isAuthenticated.value) { router.push("/profile"); }});const handleSubmit = async () => { isLoading.value = true; error.value = null; try { // Use the signIn function from auth context const response = await nhost.auth.signInEmailPassword({ email: email.value, password: password.value, }); // If we have a session, sign in was successful if (response.body?.session) { router.push("/profile"); } else { error.value = "Failed to sign in. Please check your credentials."; } } catch (err) { const message = (err as Error).message || "Unknown error"; error.value = `An error occurred during sign in: ${message}`; } finally { isLoading.value = false; }};</script>
Implement user registration with email verification flow. This page collects user information, creates accounts, and guides users through the email verification process.
src/views/SignUp.vue
Copy
Ask AI
<template> <div> <h1 v-if="!success">Sign Up</h1> <h1 v-else>Check Your Email</h1> <div v-if="success" class="success-message"> <p> We've sent a verification link to <strong>{{ email }}</strong> </p> <p> Please check your email and click the verification link to activate your account. </p> <p> <router-link to="/signin">Back to Sign In</router-link> </p> </div> <form v-else @submit.prevent="handleSubmit" class="auth-form"> <div class="auth-form-field"> <label :for="displayNameId">Display Name</label> <input :id="displayNameId" type="text" v-model="displayName" required class="auth-input" /> </div> <div class="auth-form-field"> <label :for="emailId">Email</label> <input :id="emailId" type="email" v-model="email" required class="auth-input" /> </div> <div class="auth-form-field"> <label :for="passwordId">Password</label> <input :id="passwordId" type="password" v-model="password" required minlength="8" class="auth-input" /> <small class="help-text">Minimum 8 characters</small> </div> <div v-if="error" class="auth-error"> {{ error }} </div> <button type="submit" :disabled="isLoading" class="auth-button primary" > {{ isLoading ? "Creating Account..." : "Sign Up" }} </button> </form> <div v-if="!success" class="auth-links"> <p> Already have an account? <router-link to="/signin">Sign In</router-link> </p> </div> </div></template><script setup lang="ts">import { onMounted, ref, useId } from "vue";import { useRouter } from "vue-router";import { useAuth } from "../lib/nhost/auth";const { nhost, isAuthenticated } = useAuth();const router = useRouter();const email = ref("");const password = ref("");const displayName = ref("");const isLoading = ref(false);const error = ref<string | null>(null);const success = ref(false);const displayNameId = useId();const emailId = useId();const passwordId = useId();// Redirect authenticated users to profileonMounted(() => { if (isAuthenticated.value) { router.push("/profile"); }});const handleSubmit = async () => { isLoading.value = true; error.value = null; success.value = false; try { const response = await nhost.auth.signUpEmailPassword({ email: email.value, password: password.value, options: { displayName: displayName.value, // Set the redirect URL for email verification redirectTo: `${window.location.origin}/verify`, }, }); if (response.body?.session) { // Successfully signed up and automatically signed in router.push("/profile"); } else { // Verification email sent success.value = true; } } catch (err) { const message = (err as Error).message || "Unknown error"; error.value = `An error occurred during sign up: ${message}`; } finally { isLoading.value = false; }};</script>
Build a dedicated verification page that processes email verification tokens. This page handles the verification flow when users click the email verification link.
src/views/Verify.vue
Copy
Ask AI
<template> <div> <h1>Email Verification</h1> <div class="page-center"> <div v-if="status === 'verifying'"> <p class="margin-bottom">Verifying your email...</p> <div class="spinner-verify" /> </div> <div v-else-if="status === 'success'"> <p class="verification-status"> ✓ Successfully verified! </p> <p>You'll be redirected to your profile page shortly...</p> </div> <div v-else-if="status === 'error'"> <p class="verification-status error"> Verification failed </p> <p class="margin-bottom">{{ error }}</p> <div v-if="Object.keys(urlParams).length > 0" class="debug-panel"> <p class="debug-title"> URL Parameters: </p> <div v-for="[key, value] in Object.entries(urlParams)" :key="key" class="debug-item" > <span class="debug-key"> {{ key }}: </span> <span class="debug-value">{{ value }}</span> </div> </div> <button type="button" @click="router.push('/signin')" class="auth-button secondary" > Back to Sign In </button> </div> </div> </div></template><script setup lang="ts">import { onMounted, onUnmounted, ref } from "vue";import { useRoute, useRouter } from "vue-router";import { useAuth } from "../lib/nhost/auth";const route = useRoute();const router = useRouter();const { nhost } = useAuth();const status = ref<"verifying" | "success" | "error">("verifying");const error = ref<string | null>(null);const urlParams = ref<Record<string, string>>({});// Flag to handle component unmounting during async operationslet isMounted = true;onMounted(() => { // Extract the refresh token from the URL const params = new URLSearchParams(route.fullPath.split("?")[1] || ""); const refreshToken = params.get("refreshToken"); if (!refreshToken) { // Collect all URL parameters to display for debugging const allParams: Record<string, string> = {}; params.forEach((value, key) => { allParams[key] = value; }); urlParams.value = allParams; status.value = "error"; error.value = "No refresh token found in URL"; return; } processToken(refreshToken, params);});onUnmounted(() => { isMounted = false;});async function processToken( refreshToken: string, params: URLSearchParams,): Promise<void> { try { // First display the verifying message for at least a moment await new Promise((resolve) => setTimeout(resolve, 500)); if (!isMounted) return; if (!refreshToken) { // Collect all URL parameters to display const allParams: Record<string, string> = {}; params.forEach((value, key) => { allParams[key] = value; }); urlParams.value = allParams; status.value = "error"; error.value = "No refresh token found in URL"; return; } // Process the token await nhost.auth.refreshToken({ refreshToken }); if (!isMounted) return; status.value = "success"; // Wait to show success message briefly, then redirect setTimeout(() => { if (isMounted) router.push("/profile"); }, 1500); } catch (err) { const message = (err as Error).message || "Unknown error"; if (!isMounted) return; status.value = "error"; error.value = `An error occurred during verification: ${message}`; }}</script>
Important Configuration Required: Before testing email verification, you must configure your Nhost project’s authentication settings:
Go to your Nhost project dashboard
Navigate to Settings → Authentication
Add your local development URL (e.g., http://localhost:5173) to the Allowed Redirect URLs field
Ensure your production domain is also added when deploying
Without this configuration, you’ll receive a redirectTo not allowed error when users attempt to sign up or verify their email addresses.
Start your development server and test the complete authentication flow to ensure everything works properly.
Copy
Ask AI
npm run dev
Things to try out:
Try signing up with a new email address. Check your email for the verification link and click it. See how you are sent to the verification page and then redirected to your profile.
Try signing out and then signing back in with the same credentials.
Notice how navigation links change based on authentication state showing “Sign In” and “Sign Up” when logged out, and “Profile” and “Sign Out” when logged in.
Check how the homepage also reflects the authentication state with appropriate messages.
Open multiple tabs and test signing out from one tab to see how other tabs respond. Now sign back in and see the changes propagate across tabs.