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 2: Authentication Flows
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
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)