In the evolving landscape of web development, serverless applications are steadily carving their mark. Their undeniable advantages, such as unmatched scalability and robust performance, make them stand out. The beauty of serverless lies in its promise: delivering powerful solutions that not only scale effortlessly but are also budget-friendly.

Ever dreamt of launching a business idea, only to be deterred by the technicalities of creating a user registration and login system? While platforms like Netlify and various cloud-based databases do exist, they don’t compare to the capabilities offered by Cloudflare. Many alternatives can escalate costs when scaling, but with Cloudflare Pages, the story is different.

I’ve been intermittently working on this project and demo over the past few months, juggling it alongside my other commitments. My apologies for the wait, especially to those who’ve been eagerly anticipating this system.

Unlock the Power of Cloudflare Pages

  • Seamless Scalability: Manage unlimited user registrations without a hitch.
  • Cost-Efficiency: Say goodbye to unexpected overheads; enjoy consistent pricing.
  • Blazing Speeds: Experience performance like never before.
  • Solid Security: Your users’ data remains shielded and safe.

What we will use

  • Cloudflare Pages
  • Cloudflare Pages Functions
  • Cloudflare Workers KV

What is Cloudflare Pages?

Cloudflare Pages is a modern, user-friendly platform for developers to build, deploy, and host their websites. It offers seamless integration with GitHub, meaning that you can simply push your code to GitHub and Cloudflare Pages will handle the rest – build, deployment, and even updates.

Here’s how it works:

  1. Integrated Workflow: Cloudflare Pages is built around the git workflow. Once you connect your GitHub repository to Cloudflare Pages, it starts building and deploying your site every time you push to your selected branch.
  2. JAMstack Optimized: Cloudflare Pages supports JAMstack principles, which means you can build your site with your preferred static site generator or JavaScript framework, including but not limited to Jekyll, Hugo, Next.js, and React.
  3. Fast & Secure Delivery: Powered by the globally distributed Cloudflare network, Pages ensures that your site is available and fast, no matter where your audience is. Also, Cloudflare’s inherent security features keep your site protected from threats.
  4. Continuous Deployment: Cloudflare Pages automatically builds and deploys your site every time you make updates on your GitHub repository. This allows you to iterate quickly and makes the deployment process a breeze.
  5. Custom Domain & HTTPS: With Pages, you can connect a custom domain to your site, and it provides free, automatic HTTPS on all sites to ensure that the connection is always secure.
  6. Preview Deployments: Whenever you create a new pull request in your linked GitHub repository, Cloudflare Pages automatically generates a unique preview URL, allowing you to see your changes before going live.

Whether you’re a solo developer or part of a large team, Cloudflare Pages provides an easy, fast, and secure way to get your websites online.

In light of the above, for this user registration system, I’ve opted for pure and straightforward HTML pages, eschewing any additional frameworks or build tools. This approach ensures unmatched simplicity and grants the flexibility to achieve any desired outcome.

What is Cloudflare Workers?

Cloudflare Workers is an innovative serverless computing platform that lets developers deploy their code directly to Cloudflare’s extensive network, which spans more than 200 cities worldwide. Essentially, it enables applications to run as close as possible to the end-users, thereby reducing latency and enhancing user experience.

Here’s an overview of its features and benefits:

  1. Serverless Execution Environment: Cloudflare Workers operates in a serverless environment, meaning that developers don’t have to manage or maintain any servers. Instead, they can focus on writing their code, while the platform takes care of the rest, from distribution to scaling.
  2. Edge Computing: Unlike traditional models where applications run on a single server or data center, Cloudflare Workers brings your code to the edge of the Cloudflare network. This ensures your application runs closer to the user, delivering improved performance and speed.
  3. Language Flexibility: Workers uses the V8 JavaScript engine, the same runtime used by Chrome, which allows developers to write code in JavaScript. Moreover, thanks to the WebAssembly support, other languages like Rust, C, and C++ can also be used.
  4. Security: By leveraging the inherent security of the Cloudflare network, Workers help to protect applications against a variety of threats such as DDoS attacks.

Cloudflare Workers provide an innovative and highly scalable solution for developers looking to enhance their applications’ performance, reliability, and security.

Within Cloudflare Pages, Workers are housed in a directory named functions. I’ve positioned all my JavaScript/TypeScript code within this space, tapping into the comprehensive capabilities that Workers offer.

What is Cloudflare Workers KV?

Cloudflare Workers KV (Key-Value) is a globally distributed, eventually consistent, key-value storage system that allows you to store and access data from anywhere within your Cloudflare Workers scripts. It’s designed to help you scale and simplify the management of state in serverless environments.

Here are its key features and benefits:

  1. Global Distribution: Cloudflare Workers KV is built on top of the Cloudflare network, which spans over 300 cities worldwide. This ensures your data is stored and accessed near your users, reducing latency and improving the overall performance of your applications.
  2. Fast Reads and Writes: Workers KV provides low-latency data access suitable for a variety of applications. While writes take a little longer to propagate globally (usually within a few seconds), read operations are typically fast, making it ideal for read-heavy workloads.
  3. Large Scale: You can store billions of keys in a single Workers KV namespace and each key can hold a value as large as 25MB.
  4. Namespaces: KV namespaces are containers for your key-value pairs. They allow you to segregate different sets of data within your Workers KV store, which can be particularly useful when managing multiple applications or environments (like staging and production).
  5. Eventual Consistency: Workers KV uses eventual consistency. This means that updates to your data (writes) will propagate globally and become consistent over time, which is usually a matter of a few seconds.

Cloudflare Workers KV presents a unique solution for managing state in serverless environments, providing developers with a reliable, fast, and globally distributed data storage system.

In the development of this user registration system, I’ve strategically devised the following Workers KV namespaces:

  • USERS: This serves as the primary storage for all users. It’s designed to handle an essentially infinite number of records.
  • USERS_LOGIN_HISTORY: A dedicated space to chronicle login activities, empowering users to periodically assess their account’s security footprint.
  • USERS_SESSIONS: This namespace captures details about the presently logged-in user—encompassing unique IDs, devices, locations, and more.
  • USERS_SESSIONS_MAPPING: Due to Workers KV’s eventual consistency model, there can be delays between writing to USERS_SESSIONS and checking it. This is particularly likely if operations occur in different edge locations. To circumvent this, after validation, I directly add the new session UID to USERS_SESSIONS_MAPPING, ensuring it’s included even before being written to USERS_SESSIONS.
  • USERS_TEMP: I employed this namespace as a repository for transient (temporary) links and other content that has a predetermined expiration.

We’ve created databases with unlimited capacity, auto-scaling, and high availability—features often found in pricier databases.

Designing the project

I aimed to craft something straightforward and effective without relying on third-party libraries, and I succeeded. Here’s how the entire project structure unfolds:

 1+---framework
 2|   +---models
 3|   |       password.ts
 4|   |       user.ts
 5|   +---templates
 6|   |   \---emails
 7|   |       password-reset.ts
 8|   |       password-updated.ts
 9|   \---utils
10|           encryptor.ts
11|           index.ts
12|           mailer.ts
13|           validators.ts
14+---functions
15|   |   forgot-password.ts
16|   |   login.ts
17|   |   logout.ts
18|   |   password-reset.ts
19|   |   register.ts
20|   |   _middleware.ts
21|   \---user
22|       dashboard.ts
23|       _middleware.ts
24\---public
25    |   forgot-password.html
26    |   index.html
27    |   login.html
28    |   logout.html
29    |   password-reset.html
30    |   register.html
31    \---user
32        dashboard.html

To give you a clear picture of the architecture above, let’s delve into a detailed breakdown:

  • framework: This directory houses our foundational TypeScript code. Everything from data models to email templates is located here, ensuring a consistent approach across our entire system.
  • functions: Here, you’ll find the TypeScript code tailored specifically for Cloudflare Pages functions, streamlining the site’s backend operations.
  • public: All our publicly accessible static HTML files reside in this folder, forming the visible interface for users.

Simply put, when you navigate to the login.html page, Cloudflare Pages springs into action, executing the corresponding login.ts code. This dynamic interplay continues across all pages and their associated functions.

With this setup, we can seamlessly tackle a range of tasks. Whether it’s content rewriting, data processing, or fetching data via Cloudflare Workers KV, everything is efficiently managed.

Building the User Registration System

Kickstarting our journey, we’ll first establish a user registration system. This is the core functionality.

  1. Our first step is to design a straightforward HTML form and place it inside register.html, followed by crafting the function responsible for processing the provided data:
 1<form class="row g-3 needs-validation" id="register" method="POST" novalidate>
 2	<div class="col-md-6 mb-3">
 3		<label for="firstName" class="form-label">First Name</label>
 4		<input type="text" class="form-control" name="firstName" id="firstName" autocomplete="given-name" placeholder="Required" required>
 5		<div class="invalid-feedback">You must enter your First Name.</div>
 6	</div>
 7	<div class="col-md-6 mb-3">
 8		<label for="lastName" class="form-label">Last Name</label>
 9		<input type="text" class="form-control" name="lastName" id="lastName" autocomplete="family-name" placeholder="Required" required>
10		<div class="invalid-feedback">You must enter your Last Name.</div>
11	</div>
12	<div class="col-md-12 mb-3">
13		<label for="email" class="form-label">Email</label>
14		<input type="email" class="form-control" name="email" id="email" autocomplete="email" placeholder="Required" required>
15		<div class="invalid-feedback">You must enter your Email.</div>
16	</div>
17	<div class="col-md-12 mb-3">
18		<label for="company" class="form-label">Company</label>
19		<input type="text" class="form-control" name="company" id="company" autocomplete="organization" placeholder="Optional">
20		<div class="invalid-feedback">You must enter your Company.</div>
21	</div>
22	<div class="col-md-6 mb-3">
23		<label for="password" class="form-label">Password</label>
24		<input type="password" class="form-control" name="password" id="password" autocomplete="new-password" placeholder="Required" required>
25		<div class="invalid-feedback">You must enter a Password.</div>
26	</div>
27	<div class="col-md-6 mb-3">
28		<label for="confirm_password" class="form-label">Confirm Password</label>
29		<input type="password" class="form-control" name="confirm_password" id="confirm_password" autocomplete="new-password" placeholder="Required" required>
30		<div class="invalid-feedback">Your Password does not match.</div>
31	</div>
32	<div class="col-md-12 mb-3">
33		<div class="form-check">
34			<input class="form-check-input" type="checkbox" name="terms" id="terms" required>
35			<label class="form-check-label" for="terms">
36				I have read and agree to the <a href="#" class="link-primary">Terms of Service</a>, <a href="#" class="link-primary">Privacy Policy</a> and <a href="#" class="link-primary">Cookies Policy</a>.
37			</label>
38			<div class="invalid-feedback">You must agree before registering.</div>
39		</div>
40	</div>
41	<div class="col-md-6 mx-auto mb-3">
42		<div class="d-grid gap-2">
43			<button class="btn btn-primary" type="submit">Create Account</button>
44		</div>
45	</div>
46</form>

With our form set up, the next step is to enhance data submission using JavaScript. While the conventional form submission is perfectly viable, I’ve incorporated Bootstrap 5 validation in my example, thus leaning towards AJAX for data posting.

Here is a working example taken from my demo:

  1(function () {
  2  'use strict'
  3
  4	// Check if passwords match
  5	document.querySelectorAll("#password, #confirm_password").forEach(function (input) {
  6		input.addEventListener("keyup", function () {
  7			const password = document.getElementById("password");
  8			const confirmPassword = document.getElementById("confirm_password");
  9			if (
 10				password.value !== "" &&
 11				confirmPassword.value !== "" &&
 12				confirmPassword.value === password.value
 13			) {
 14				// Do something when passwords match
 15			}
 16		});
 17	});
 18
 19	var form = document.querySelector("#register");
 20
 21	form.addEventListener('submit', function (event) 
 22	{
 23		const _alert = document.querySelector("div.alert.alert-danger");
 24		
 25		if(_alert)
 26			_alert.remove();
 27			
 28		if (!form.checkValidity()) 
 29		{
 30		  // Prevent default form submission
 31		  event.preventDefault();
 32		  event.stopPropagation();
 33		  form.classList.add('was-validated');
 34		}
 35		else
 36		{
 37			// Mark inputs as validated
 38			form.classList.add('was-validated');
 39			
 40			// Prevent default form submission again
 41			event.preventDefault();
 42			event.stopPropagation();
 43
 44			// Helper to get all inputs we want
 45			const getAllFormElements = element => Array.from(element.elements).filter(tag => ["input"].includes(tag.tagName.toLowerCase()));
 46				
 47			// Grab desired inputs
 48			const registerFormElements = getAllFormElements(event.currentTarget);
 49				
 50			// Loop over them and disable
 51			Array.prototype.slice.call(registerFormElements).forEach(function (element) {
 52				element.setAttribute("readonly", true);
 53				element.style = 'background-color: #e9ecef; opacity: 1;';		
 54			});
 55			 
 56			// Disable button and show loading spinner
 57			let _button = event.currentTarget.querySelector(".btn");
 58			_button.setAttribute("disabled", true);
 59			_button.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Please wait...';
 60			
 61			// A modern replacement for XMLHttpRequest.
 62			// https://caniuse.com/fetch
 63			if (window.fetch) 
 64			{
 65				(async() => {
 66					await fetch(window.location.href, {
 67						method: 'POST',
 68						// NOTE: FormData will only grab inputs with name attribute
 69						body: JSON.stringify(Object.fromEntries(new FormData(event.target))),
 70						headers: {
 71							'Accept': 'application/json',
 72							'Content-Type': 'application/json'
 73						}
 74					}).then((response) => {
 75						// Pass both the parsed JSON and the status code to the next .then()
 76						return response.json().then((data) => ({status: response.status, body: data}));
 77					}).then(({status, body}) => {
 78
 79						if (body.success === true && status === 201) {
 80							Array.prototype.slice.call(document.querySelectorAll('form *')).forEach(function (element) {
 81								element.style.display = "none";   
 82							});
 83							
 84							let _alert = document.createElement("div");
 85							_alert.classList.add('alert');
 86							_alert.classList.add('alert-success');
 87							_alert.setAttribute("role", 'alert');
 88							_alert.innerHTML = '<p>Thank you for joining! A confirmation email has been sent out. You need to verify your email before trying to log in.</p>';
 89							
 90							form.prepend(_alert);
 91						} else {
 92							let _alert = document.createElement("div");
 93							_alert.classList.add('alert');
 94							_alert.classList.add('alert-danger');
 95							_alert.setAttribute("role", 'alert');
 96							_alert.innerText = `Error #${data.error.code}: ${data.error.message}`;
 97							
 98							form.prepend(_alert);
 99				  
100							form.classList.remove('was-validated');
101				  
102							Array.prototype.slice.call(registerFormElements).forEach(function (element) {
103								element.removeAttribute("style");
104								element.removeAttribute("readonly");
105							});
106			
107							_button.removeAttribute("disabled");
108							_button.innerHTML = 'Create Account';
109						}
110					});
111				})();
112			}
113			else
114			{
115				alert("Your browser is too old!\nIt does not have the most recent features.\nPlease update your browser or use a different one.");
116			}
117		}
118	}, false);
119})();

Once the Create Account is clicked, data is posted to /register via AJAX, triggering the functions/register.ts code. This mechanism allows for both GET and POST data processing. While managing OPTIONS is possible, we’ll sideline it for this discussion.

  1. For standard page visits, or GET executions, there’s potential to introduce a plethora of advanced features, such as CSRF protection. Let’s take a look at the following example:
 1/**
 2 * GET /register
 3 */
 4export const onRequestGet: PagesFunction = async ({ next }) => 
 5{
 6	// Fetch the original page content
 7	const response = await next();
 8
 9	// Prepare our CSRF data
10	const IP = request.headers.get('CF-Connecting-IP');
11	const Country = request.headers.get('CF-IPCountry') || '';
12	const UserAgent = request.headers.get('User-Agent');
13	const expiry = Date.now() + 60000;
14	const CSRF_TOKEN = JSON.stringify({i: IP, c: Country, u: UserAgent, e: expiry});  
15	const encryptedData = await encryptData(new TextEncoder().encode(CSRF_TOKEN), env.CSRF_SECRET, 10000);	
16	const hex = Utils.BytesToHex(new Uint8Array(encryptedData));
17		
18	// Rewrite the content and stream it back to the user (async)
19	return new HTMLRewriter()
20    .on("form", {
21      element(form) {
22		// The CSRF input
23		form.append(
24		  `<input type="hidden" name="csrf" value="${hex}" />`,
25		  { html: true }
26		);
27      },
28    })
29    .transform(response);
30};

In this process, we use Cloudflare Workers HTMLRewriter feature to produce and embed a unique CSRF Token into the form for each page request.

I’ll delve deeper into my CSRF strategy in the subsequent sections. For the time being, observe how I dynamically append a unique, random CSRF code to fortify security.

  1. Now, let’s pivot to the POST execution phase. Here, we meticulously validate the input data, store it securely, and then dispatch an email to the user for confirmation or further instructions. To provide you with a conceptual overview, I’ve drafted a pseudo-code representation:
 1/**
 2 * POST /register
 3 */
 4export const onRequestPost: PagesFunction<{ USERS: KVNamespace; }> = async ({ request, env }) => 
 5{
 6	// Validate content type
 7	// Validate our CSRF before doing anything
 8	// Check for existing user email
 9	// Generate a new salt & hash the original password
10	// Store the user with some meta-data
11	// Etc
12}

In my configuration, after successful registration, a user’s details are stored in Workers KV as follows:

 1{
 2  "uid": "888e8801-3b35-4dd3-9ae2-7ff0f564bb3b",
 3  "email": "[email protected]",
 4  "firstname": "John",
 5  "lastname": "Smith",
 6  "company": "",
 7  "role": "User",
 8  "password": "....",
 9  "salt": "...",
10  "access_token": null,
11  "created_at": 123456789,
12  "confirmed_at": null,
13  "last_updated": null,
14  "last_sign_in_at": 123456789,
15  "recovery_sent_at": null,
16  "password_changed_at": null
17}

You can adjust this format, adding or removing fields based on your needs. I must highlight that I won’t provide a complete, ready-to-copy code. It’s essential to invest in understanding and hands-on exploration. Only then can one truly evolve as a developer.

Building the User Login system

Transitioning to the login mechanism for your Cloudflare Pages venture is a breeze. We’ll be adopting a similar approach as we did for the registration process.

  1. Just as we fashioned the registration form, craft a concise form for the login process and house it within login.html:
 1<form class="row g-3 needs-validation" id="login" method="POST" novalidate>
 2	<div class="col-sm-12">
 3		<label for="email" class="form-label">Email</label>
 4		<input type="email" class="form-control" name="email" id="email" autocomplete="email" placeholder="Required" required>
 5		<div class="invalid-feedback">You must enter your Email.</div>
 6	</div>
 7	<div class="col-sm-12">
 8		<label for="password" class="form-label">Password</label>
 9		<input type="password" class="form-control" name="password" id="password" autocomplete="password" placeholder="Required" required>
10		<div class="invalid-feedback">You must enter a Password.</div>
11	</div>
12	<div class="d-grid gap-2 col-sm-6 mx-auto">
13		<button class="btn btn-primary" type="submit">Login</button>
14	</div>
15</form>
  1. After setting up the form, it’s time to process its rendering. The subsequent step involves creating the necessary code. This will automatically be triggered from login.ts:
1/**
2 * GET /login
3 */
4export const onRequestGet: PagesFunction = async ({ next }) => 
5{
6	// Fetch the original page content
7	// Prepare our CSRF data	
8	// Rewrite the content and stream it back to the user (async)
9};
  1. The concluding stride is formulating the code to manage the POST execution, situated within the login.ts:
 1/**
 2 * POST /login
 3 */
 4export const onRequestPost: PagesFunction<{ 
 5	USERS: KVNamespace;
 6	USERS_SESSIONS: KVNamespace;
 7	USERS_SESSIONS_MAPPING: KVNamespace;
 8	USERS_LOGIN_HISTORY: KVNamespace; }> = async ({ request, env }) => 
 9{
10	// Validate content type
11	// Validate our CSRF before doing anything
12	// Check for existing user email
13	// Generate a new salt & hash the original password
14	// Compare the passwords
15	// Save session
16	// Update history
17	// Retrieve the current mapping for this user (if it exists)
18	// Add the new session UID to the mapping
19	// Store the updated mapping
20	// Etc
21};

Through these 3 streamlined steps, we’ve seamlessly sculpted the login system. Undeniably, the login mechanism is the most intricate segment, necessitating several operations as annotated. Yet, view this not as a complication but as an invigorating challenge to overcome!

Building the User Password Reset system

  1. Drawing parallels to our prior components, we commence by sculpting the dedicated form for initiating a password reset:
 1<form class="row g-3 needs-validation" id="forgot" novalidate>
 2	<div class="col-sm-12">
 3		<label for="email" class="form-label">Email</label>
 4		<input type="email" class="form-control" name="email" id="email" autocomplete="email" placeholder="Required" required />
 5		<div class="invalid-feedback">You must enter your Email.</div>
 6	</div>
 7
 8	<div class="d-grid gap-2 col-sm-6 mx-auto">
 9		<button class="btn btn-primary" type="submit">Request</button>
10	</div>
11</form>

The underlying concept is straightforward: upon entry of a user’s email, if it’s recognized within our system, a singular password reset link is generated and dispatched. For enhanced security, ensure this link remains active for a short duration, ideally no more than 2 hours.

  1. Initiate the crafting of code dedicated to managing GET execution, which you’ll want to embed within forgot-password.ts:
1/**
2 * GET /forgot-password
3 */
4export const onRequestGet: PagesFunction = async ({ next }) => 
5{
6	// Fetch the original page content
7	// Prepare our CSRF data	
8	// Rewrite the content and stream it back to the user (async)
9};
  1. Continue by formulating the code responsible for POST execution, ensuring it’s housed within the same forgot-password.ts file:
 1/**
 2 * POST /forgot-password
 3 */
 4export const onRequestPost: PagesFunction<{ 
 5	USERS: KVNamespace;
 6	USERS_TEMP: KVNamespace; 
 7	}> = async ({ request, env }) => 
 8{
 9	// Validate content type
10	// Validate our CSRF before doing anything
11	// Check for existing user email
12	// Generate a unique string to hash
13	// Hash the string
14	// Save to KV, expiration 2 hours
15	// Send email to user
16	// Etc
17};

It’s up to you now to complete the coding puzzle. By following the guidance within my comments, you should find the process quite manageable. Once finished, you’ll be equipped with a fully operational password reset system.

Cautionary Note: Refrain from displaying messages such as “No such email exists.” Such indicators are gold mines for hackers, paving the way for potential brute force assaults. Instead, adopt a more ambiguous yet user-friendly approach: “Should the email be registered in our system, you will receive a reset link.”

User Registration Demo

Explore a live demo of the user registration process I’ve established: https://members.mecanik.dev/

Features include:

  • Registration, login, and password reset capabilities
  • Access to your profile
  • Manage security settings and active sessions - You can disconnect for example if you logged in from your mobile.
  • Preview free software and look out for upcoming premium offerings.

I’m currently juggling other projects, but I’ll continue to enhance this platform. Feel free to test it out - just ensure you use genuine emails to avoid impacting my SendGrid reputation.

I welcome any feedback or reported issues. Please reach out if you have any!

Questions and Answers

Undoubtedly, security is top of mind. You might question the system’s robustness against potential attacks and data breaches.

Consider that Cloudflare Pages operates on the Cloudflare platform, granting you access to their advanced Web Application Firewall (WAF) and a suite of security tools.

How CSRF actually works?

You might have observed the dynamic generation and injection of a CSRF token into the form on every load. Here’s how this mechanism ensures the security of requests:

CSRF Token Components:

  • IP: Captures the user’s current IP Address.
  • Country: Identifies the user’s current location.
  • UserAgent: Records the user’s browser details.
  • expiry: Sets a timer by adding one minute to the current time.

This data is assembled in a JSON format: {i, c, u, e}, which is subsequently encrypted and transformed to Hex:

1const encryptedData = await encryptData(new TextEncoder().encode(CSRF_TOKEN), env.CSRF_SECRET, 10000);	
2const hex = Utils.BytesToHex(new Uint8Array(encryptedData));

About the Encryption Function:

  1. The data undergoes encryption utilizing a user-defined password, generating a password key and subsequently deriving an AES key from it.
  2. This AES key encrypts the data, with the salt and IV (Initialization Vector) generated on-the-fly.
  3. The encrypted output is an ArrayBuffer encapsulating the iterations count, salt, IV, and encrypted data.

Simply put, this encryption process upholds industry-standard practices to render the encrypted data both undecipherable and safeguarded against tampering.

Validating the CSRF Token:

 1const unhex = Utils.HexToBytes(formData.csrf);
 2const decrypted = new TextDecoder().decode(await decryptData(unhex, env.CSRF_SECRET));
 3const parsed = JSON.parse(decrypted);
 4
 5if (!Utils.isCRSFData(parsed))
 6{
 7	return new Response(JSON.stringify({
 8			result: null,
 9			success: false,
10			// An ambigous message; don't tell the hacker what's missing.
11			error: { code: 1001, message: "Invalid CSRF Token. Please refresh the page and try again." }
12		}),
13		{
14			status: 403,
15			headers: { 'content-type': 'application/json;charset=UTF-8'
16		}
17	}); 
18}
19
20const IP = request.headers.get('CF-Connecting-IP');
21const Country = request.headers.get('CF-IPCountry') || '';
22const UserAgent = request.headers.get('User-Agent');
23
24if(IP !== parsed.i || Country !== parsed.c || UserAgent !== parsed.u || Date.now() > parsed.e)
25{
26	return new Response(JSON.stringify({
27			result: null,
28			success: false,
29			// An ambigous message; don't tell the hacker what's missing.
30			error: { code: 1002, message: "Invalid CSRF Token. Please refresh the page and try again." }
31		}),
32		{
33			status: 403,
34			headers: { 'content-type': 'application/json;charset=UTF-8'
35		}
36	}); 
37}

The validation process rigorously checks the CSRF token, ensuring it’s:

  • Originating from the same IP Address.
  • Dispatched from the same country.
  • Sent from the same browser.
  • Submitted within its active timeframe.
  • Should any of these checks fail, the system identifies the CSRF token as invalid, offering robust protection against potential threats.

Should any of these checks fail, the system identifies the CSRF token as invalid, offering robust protection against potential threats.

Password hashing/encryption

For password hashing/encryption, I employed PBKDF2 (Password-Based Key Derivation Function 2) in tandem with the SHA-256 hashing algorithm.

The method uses a unique, cryptographically secure pseudorandom “salt” for each password, ensuring enhanced security. This approach offers protection against rainbow table and brute force attacks.

The hashed version of the password is stored, not the plaintext, further bolstering data integrity.

Why no JWT Token usage?

JWT (JSON Web Tokens) are a popular method for handling user authentication and transmitting information between parties in a compact, URL-safe manner.

While they have many advantages, there are reasons one might choose not to use them in specific contexts, such as a full user registration system. Here’s a deeper look:

  1. Statelessness and Revocation: One of the hallmarks of JWTs is their statelessness. However, this very nature can be problematic for user management systems. For example, if a user’s JWT token is stolen or compromised, there’s no straightforward way to revoke that token unless you maintain a blacklist of tokens, which defeats the purpose of statelessness.
  2. Size: As you add more data to a JWT, it grows in size. If you have a comprehensive user registration system where you might need to store more user-related data in the token, this can lead to larger HTTP headers and increased latency.
  3. Storage Security: For client-side storage of JWTs, common storage locations include local storage or cookies. Local storage is vulnerable to XSS attacks, and while cookies can be secured to a greater extent, they open up avenues for CSRF attacks if not handled properly.
  4. Expiration Handling: Managing the expiration of JWTs can be complex. While short-lived tokens reduce risk, they require a refresh mechanism, which introduces more complexity into the authentication flow.
  5. No Built-in Revocation: As previously mentioned, if a token is compromised, there’s no inherent mechanism to revoke or invalidate it until it expires. This can be a significant security risk if the token has a long lifespan.
  6. Complexity for Developers: For those unfamiliar with JWTs, there’s a learning curve involved in understanding how to properly generate, validate, and use them. This complexity can introduce mistakes and vulnerabilities if not thoroughly understood and implemented.

In my exploration of various systems and their handling of user data, I found that some companies store an abundance of information within their JWT tokens, including sensitive details like user roles, among others.

To be clear, my intent isn’t to critique or malign other approaches. However, based on my professional stance on security, embedding such critical information directly within tokens raises concerns. There’s an inherent risk associated with exposing more data than necessary, especially when that data could potentially be exploited to infer user privileges or other functionalities.

Given these reservations, I made a conscious decision to veer away from this method. Instead, I gravitated towards the tried-and-true server-side authentication combined with secure cookie handling. This approach, in my opinion, strikes a balance between user experience and robust security, ensuring that sensitive data remains protected and the system remains resilient against potential vulnerabilities.

Sending emails reliably

I opted for SendGrid with dynamic templates given its widespread adoption and my deep familiarity with its nuances. To dive deeper, refer to their API documentation .

While its integration might pose some complexities, it’s all part of the rewarding challenge. Here is an example for you:

 1const SENDGRID_API_URL = 'https://api.sendgrid.com/v3/mail/send';
 2const SENDGRID_API_KEY = 'YOUR_SENDGRID_API_KEY'; // Replace with your actual API key
 3
 4interface EmailDetails {
 5    to: string;
 6    from: string;
 7    subject: string;
 8    content: string;
 9}
10
11async function sendEmail(details: EmailDetails): Promise<void> {
12    const data = {
13        personalizations: [{
14            to: [{
15                email: details.to
16            }]
17        }],
18        from: {
19            email: details.from
20        },
21        subject: details.subject,
22        content: [{
23            type: 'text/plain',
24            value: details.content
25        }]
26    };
27
28    try {
29        const response = await fetch(SENDGRID_API_URL, {
30            method: 'POST',
31            headers: {
32                'Authorization': `Bearer ${SENDGRID_API_KEY}`,
33                'Content-Type': 'application/json'
34            },
35            body: JSON.stringify(data)
36        });
37
38        if (response.status === 202) {
39            console.log('Email sent successfully!');
40        } else {
41            const responseBody = await response.json();
42            console.error('Failed to send email:', responseBody.errors);
43        }
44    } catch (error) {
45        console.error('Error sending email:', error);
46    }
47}
48
49// Usage example:
50sendEmail({
51    to: '[email protected]',
52    from: '[email protected]',
53    subject: 'Hello from SendGrid!',
54    content: 'This is a test email sent using the SendGrid API.'
55});

How to further increase security?

Even though Cloudflare Workers KV encrypts stored data, you can add an extra layer of encryption for user data.

Or do want to add a twist for would-be attackers? Consider implementing dummy form inputs to confound bots and hackers alike:

 1return new HTMLRewriter()
 2	.on("form", 
 3	{
 4	  element(form) 
 5	  {
 6		// Dummy inputs... just to give the hacker more headache and confuse him :)
 7		const randomNumber = Utils.RandomNumber(1, 5);
 8
 9		for (let i = 0; i < randomNumber; i++) 
10		{
11			form.append(
12				`<input type="hidden" name="${Utils.RandomString(Utils.RandomNumber(i + 5, i + 25))}" value="${Utils.RandomString(Utils.RandomNumber(i + 25, i + 256))}" />`,
13				{ html: true }
14			);
15		}
16	  },
17	})
18.transform(response);

This snippet dynamically generates between 1 to 5 randomly named inputs with every page load.

The capabillities are almost unlimited, you just need to use your imagination.

Wrapping Up

And there we have it! I sincerely hope this piece was both insightful and engaging for you. Whether you’re a seasoned developer or just diving into the world of scalable cloud websites, sharing knowledge is vital to our community’s growth.

If you found this article valuable, please consider passing it along to fellow developers or those interested in the topic. Every share broadens our collective understanding.

Your feedback and questions are immensely appreciated. Don’t hesitate to drop a comment or reach out – let’s keep the conversation going. Safe coding!