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 Part 1: Setup & JWT Basics Part 2: Authentication Flows Part 3: Protecting Routes & Security 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 { 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 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(); 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 = f

May 6, 2025 - 12:12
 0
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;

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 Uph2>

<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">Emaillabel>
    <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">Passwordlabel>
       <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 Ina>
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 Inh2>

<!-- 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">Emaillabel>
    <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 passworda>
    <label for="password">Passwordlabel>
    <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 Upa>
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