Part 2: Authentication Flows

Mukesh JaiswalMukesh Jaiswal
8 min read

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;

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>

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);
}

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>

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]
}

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>

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

0
Subscribe to my newsletter

Read articles from Mukesh Jaiswal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mukesh Jaiswal
Mukesh Jaiswal

Driving business growth with AI Automation (as Business Automation Partner) | Helping startups build (as Software Developer)