DEV Community

Mukesh
Mukesh

Posted on

Part 2: Authentication Flows

If you haven't already, I would recommend having a quick look at the Introduction & Sequence Diagram

Welcome to the 3-part series that helps you create a scalable production-ready authentication system using pure JWT & a middleware for your SvelteKit project


You are reading Part 2

Goal: Implement user authentication flows using JWT, covering sign-up, sign-in, and logout

Topics we'll cover

  • Sign-Up Flow: Server-side endpoint to register users and issue JWT, with a Svelte form.
  • Sign-In Flow: Server-side endpoint to authenticate users and issue JWT, with a Svelte form.
  • Logout Flow: Server-side endpoint to clear cookies, with a simple UI.

Note:

  • All form validations are happening server-side, as it should be.
  • The forms are pretty basic. Focus on the logic, understand & then enhance the design of the forms using AI.

Sign-Up Flow

Let's implement the sign-up endpoint:

// src/routes/auth/sign-up/+page.server.ts import { fail, redirect } from "@sveltejs/kit"; import { generateToken, setAuthCookie, logToken, } from "$lib/auth/jwt"; import { createUser, getUserByEmail } from "$lib/database/db"; import bcrypt from "bcrypt"; import type { Actions } from "./$types"; export const actions = { signup: async ({ cookies, request }) => { const data = await request.formData(); const email = data.get("email"); const password = data.get("password"); // Wrap all registration logic in a separate async function const registerUser = async () => { try { // Email validation if (typeof email !== "string" || !email) { return { success: false, error: "invalid-input", message: "Email is required", }; } // Email format validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return { success: false, error: "invalid-input", message: "Please enter a valid email address", }; } // Password validation if (typeof password !== "string" || password.length < 6) { return { success: false, error: "invalid-input", message: "Password must be at least 6 characters", }; } // Check if user already exists const existingUser = await getUserByEmail(email); if (existingUser) { return { success: false, error: "user-exists", message: "An account with this email already exists", }; } // Hash the password before storing it const saltRounds = 10; const hashedPassword = await bcrypt.hash( password, saltRounds ); // Create the user in the database const user = await createUser( email, hashedPassword, "user" // Default role ); console.log("User Created"); if (!user) { return { success: false, error: "database-error", message: "Failed to create account - database error", }; } // Create token for the new user const tokenPayload = { userId: user.USER_ID, email: user.EMAIL, role: user.ROLE, }; const accessToken = generateToken(tokenPayload); // Set JWT cookie setAuthCookie(cookies, accessToken); // Log token to database if (user.USER_ID) { // We use a non-awaited promise to avoid blocking logToken(accessToken, user.USER_ID).catch((err) => { console.error("Failed to log token:", err); }); } else { console.error( "Cannot log token: user.USER_ID is null or undefined" ); } return { success: true }; } catch (error) { console.error("Registration error:", error); return { success: false, error: "registration-failed", message: "Failed to create account", }; } }; // Execute the registration process const result = await registerUser(); if (!result.success) { // Map error types to appropriate HTTP status codes and response formats switch (result.error) { case "user-exists": return fail(400, { invalid: true, message: result.message, }); case "invalid-input": return fail(400, { invalid: true, message: result.message, }); case "connection-error": return fail(503, { error: true, message: result.message }); case "database-error": case "registration-failed": default: return fail(500, { error: true, message: result.message }); } } // Registration succeeded, perform redirect throw redirect(302, "/dashboards/analytics"); }, } satisfies Actions; 
Enter fullscreen mode Exit fullscreen mode

And the sign-up form:

// src/routes/auth/sign-up/+page.svelte <script lang="ts"> import AuthLayout from "$lib/layouts/AuthLayout.svelte"; import LogoBox from "$lib/components/LogoBox.svelte"; import SignWithOptions from "../components/SignWithOptions.svelte"; import {Button, Card, CardBody, Col, Input, Row} from "@sveltestrap/sveltestrap"; import type { ActionData } from './$types'; import { enhance } from '$app/forms'; import type { SubmitFunction } from '@sveltejs/kit'; import { goto } from '$app/navigation'; const signInImg = '/images/sign-in.svg' // Get form data for error display let { form } = $props<{ form?: ActionData }>(); let loading = $state(false); let showErrors = $state(true); // Controls visibility of error messages // Custom enhance function to track loading state const handleSubmit: SubmitFunction = () => { loading = true; showErrors = false; // Hide any previous errors on new submission return async ({ result, update }) => { if (result.type === 'redirect') { // Handle redirect by navigating to the specified location loading = false; // Make sure to reset loading before redirect goto(result.location); return; } // For other result types, update form with the result await update(); loading = false; showErrors = true; // Only show errors if we're not redirecting }; } </script> <h2>Sign Up</h2> <form method="POST" action="?/signup" use:enhance={handleSubmit}> <!-- Show loading spinner and form status --> {#if loading} <div>Loading...</div> <p>Creating your account...</p> {:else if showErrors} <!-- Display validation errors --> {#if form?.invalid} <div>{form.message || 'Please check your input.'}</div> {/if}  {#if form?.error} <div>{form.message || 'An error occurred.'}</div> {/if} {/if} <label class="form-label" for="email">Email</label> <Input type="email" id="email" name="email" class={showErrors && form?.invalid && form?.message?.includes('email') ? 'is-invalid' : ''} placeholder="Enter your email" disabled={loading} > <label class="form-label" for="password">Password</label> <Input type="password" id="password" name="password" class={showErrors && form?.invalid && form?.message?.includes('assword') ? 'is-invalid' : ''} placeholder="Enter your password" disabled={loading} /> <Button color="primary" type="submit" disabled={loading}> {loading ? 'Signing Up...' : 'Sign Up'} </Button> </form> <p > Already have an account? <a href="/auth/sign-in">Sign In</a> </p> 
Enter fullscreen mode Exit fullscreen mode

Sign-In Flow

Now for the sign-in endpoint:

// src/routes/auth/sign-in/+page.server.ts import { fail, redirect } from "@sveltejs/kit"; import { generateToken, logToken } from "$lib/auth/jwt"; import { setAuthCookie } from "$lib/auth/cookies"; import { validateUserCredentials } from "$lib/database/db"; import type { Actions } from "./$types"; // Error response types type AuthError = { success: false; error: | "invalid-input" | "invalid-credentials" | "connection-error" | "database-error" | "login-failed"; message: string; }; // Success response type type AuthSuccess = { success: true; }; // Combined result type type AuthResult = AuthError | AuthSuccess; export const actions = { login: async ({ cookies, request }) => { const data = await request.formData(); const email = data.get("email")?.toString() || ""; const password = data.get("password")?.toString() || ""; // Wrap all login logic in a separate async function const authenticateUser = async (): Promise<AuthResult> => { try { // Validate input fields if (!email || !password) { return { success: false, error: "invalid-input", message: "Email and password are required", }; } // Validate user credentials against database const user = await validateUserCredentials(email, password); // If authentication failed if (!user) { return { success: false, error: "invalid-credentials", message: "Invalid email or password", }; } // User authenticated - create JWT token const tokenPayload = { userId: user.USER_ID, email: user.EMAIL, role: user.ROLE, }; const accessToken = generateToken(tokenPayload); // Set JWT cookie setAuthCookie(cookies, accessToken); // Log token to database (non-blocking) if (user.USER_ID) { logToken(accessToken, user.USER_ID).catch((err) => { console.error("Failed to log token:", err); }); } return { success: true }; } catch (error) { console.error("Login error:", error); // Get error message from any type of error const errorMessage = error instanceof Error ? error.message : String(error); // Simple error classification based on key terms let errorType: AuthError["error"] = "login-failed"; let errorMsg = "An unexpected error occurred"; // Simple keyword-based error detection if ( errorMessage.includes("network") || errorMessage.includes("connect") ) { errorType = "connection-error"; errorMsg = "Unable to connect to the service. Please try again later."; } else if ( errorMessage.includes("database") || errorMessage.includes("query") ) { errorType = "database-error"; errorMsg = "Database error. Please try again later."; } return { success: false, error: errorType, message: errorMsg, }; } }; // Execute the authentication process const result = await authenticateUser(); if (!result.success) { return handleError(result); } // Login succeeded, perform redirect console.log("Login successful, redirecting to dashboard"); throw redirect(302, "/dashboard"); }, } satisfies Actions; // Helper function to handle errors - returns consistent error format function handleError(result: AuthError): ReturnType<typeof fail> { // Simple mapping of error types to status codes let statusCode = 500; // Define possible response shapes type ErrorResponse = { error: boolean; message: string }; type CredentialsResponse = { credentials: boolean; message: string; }; type InvalidResponse = { invalid: boolean; message: string }; // Start with default error response let responseData: | ErrorResponse | CredentialsResponse | InvalidResponse = { error: true, message: result.message }; if (result.error === "invalid-credentials") { statusCode = 400; responseData = { credentials: true, message: result.message }; } else if (result.error === "invalid-input") { statusCode = 400; responseData = { invalid: true, message: result.message }; } else if (result.error === "connection-error") { statusCode = 503; } return fail(statusCode, responseData); } 
Enter fullscreen mode Exit fullscreen mode

And the sign-in form:

// src/routes/auth/sign-in/+page.svelte <script lang="ts"> import AuthLayout from "$lib/layouts/AuthLayout.svelte"; import LogoBox from "$lib/components/LogoBox.svelte"; import {Button, Card, CardBody, Col, Input, Row} from "@sveltestrap/sveltestrap"; import SignWithOptions from "../components/SignWithOptions.svelte"; import type { ActionData } from './$types'; import { enhance } from '$app/forms'; import type { SubmitFunction } from '@sveltejs/kit'; import { goto } from '$app/navigation'; const signInImg = '/images/sign-in.svg' let { form } = $props<{ form?: ActionData }>(); let loading = $state(false); let showErrors = $state(true); // Controls visibility of error messages // Custom enhance function to track loading state const handleSubmit: SubmitFunction = () => { loading = true; showErrors = false; // Hide any previous errors on new submission return async ({ result, update }) => { if (result.type === 'redirect') { // Handle redirect by navigating to the specified location loading = false; // Make sure to reset loading before redirect goto(result.location); return; } // For other result types, update form with the result await update(); loading = false; showErrors = true; // Only show errors if we're not redirecting }; } </script> <h2>Sign In</h2> <!-- Using a native form with the enhance action --> <form method="POST" action="?/login" class="authentication-form" use:enhance={handleSubmit}> {#if loading} <span class="visually-hidden">Loading...</span> <p class="mt-2 text-muted">Signing in...</p> {:else if showErrors} {#if form?.invalid} <div>{form.message || 'Email and password are required.'}</div> {/if}  {#if form?.credentials} <div>{form.message || 'You have entered wrong credentials.'}</div> {/if} {#if form?.error} <div>{form.message || 'An unexpected error occurred.'}</div> {/if} {/if} <label class="form-label" for="email">Email</label> <Input type="email" id="email" name="email" class={showErrors && form?.invalid ? 'is-invalid' : ''} placeholder="Enter your email" value="user@demo.com" disabled={loading} /> <a href="/auth/reset-password"> Reset password</a> <label for="password">Password</label> <Input type="password" id="password" name="password" class={showErrors && (form?.invalid || form?.credentials) ? 'is-invalid' : ''} placeholder="Enter your password" value="123456" disabled={loading} /> <Button color="primary" type="submit" disabled={loading}> {loading ? 'Signing In...' : 'Sign In'} </Button> </form> <p> Don't have an account? <a href="/auth/sign-up" >Sign Up</a> </p> 
Enter fullscreen mode Exit fullscreen mode

Logout Flow

Finally, the logout endpoint:

// src/routes/auth/logout/+page.server.ts import { json, redirect } from '@sveltejs/kit'; export async function POST({ cookies }) { // [INSERT YOUR LOGOUT ENDPOINT CODE HERE] } 
Enter fullscreen mode Exit fullscreen mode

And the logout UI:

// src/routes/auth/logout/+page.svelte <svelte:head> <title>Logging out...</title> </svelte:head> <span >Loading...</span> <p>Logging you out...</p> 
Enter fullscreen mode Exit fullscreen mode

Next → Part 3: Protecting Routes & Security
Previous → Part 1: Setup & JWT Basics

Top comments (0)