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

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