Dans le paysage en évolution du développement Web, les applications sans serveur ne cessent de se démarquer. Leurs avantages indéniables, tels qu’une évolutivité inégalée et des performances robustes, les distinguent. La beauté du sans serveur réside dans sa promesse : fournir des solutions puissantes qui non seulement évoluent sans effort, mais sont également économiques.

Avez-vous déjà rêvé de lancer une idée d’entreprise, mais d’être dissuadé par les détails techniques liés à la création d’un système d’enregistrement et de connexion des utilisateurs? Bien qu’il existe des plates-formes telles que Netlify et diverses bases de données basées sur le cloud, elles ne se comparent pas aux capacités offertes par Cloudflare. De nombreuses alternatives peuvent faire grimper les coûts lors de la mise à l’échelle, mais avec Cloudflare Pages, l’histoire est différente.

J’ai travaillé par intermittence sur ce projet et cette démo au cours des derniers mois, jonglant avec mes autres engagements. Mes excuses pour l’attente, en particulier à ceux qui attendaient avec impatience ce système.

Libérez la puissance des pages Cloudflare

  • Évolutivité transparente: gérez les inscriptions d’utilisateurs illimitées sans accroc.
  • Rentabilité: Dites adieu aux frais généraux inattendus; profitez de prix cohérents.
  • Vitesse fulgurante: découvrez des performances comme jamais auparavant.
  • Solide sécurité: les données de vos utilisateurs restent protégées et sécurisées.

Ce que nous utiliserons

  • Pages Cloudflare
  • Fonctions des pages Cloudflare
  • Travailleurs Cloudflare KV

Qu’est-ce que les pages Cloudflare?

Cloudflare Pages est une plate-forme moderne et conviviale permettant aux développeurs de créer, déployer et héberger leurs sites Web. Il offre une intégration transparente avec GitHub, ce qui signifie que vous pouvez simplement transférer votre code vers GitHub et Cloudflare Pages se chargera du reste : construction, déploiement et même mises à jour.

Voici comment cela fonctionne:

  1. Workflow intégré: Cloudflare Pages est construit autour du workflow git. Une fois que vous avez connecté votre référentiel GitHub à Cloudflare Pages, il commence à créer et à déployer votre site chaque fois que vous le poussez vers la branche sélectionnée.
  2. JAMstack optimisé: Cloudflare Pages prend en charge les principes JAMstack, ce qui signifie que vous pouvez créer votre site avec votre générateur de site statique préféré ou votre framework JavaScript, y compris, mais sans s’y limiter, Jekyll, Hugo, Next.js et React.
  3. Livraison rapide et sécurisée: Propulsé par le réseau Cloudflare distribué à l’échelle mondiale, Pages garantit que votre site est disponible et rapide, peu importe où se trouve votre public. De plus, les fonctionnalités de sécurité inhérentes à Cloudflare protègent votre site contre les menaces.
  4. Déploiement continu: Cloudflare Pages crée et déploie automatiquement votre site chaque fois que vous effectuez des mises à jour sur votre référentiel GitHub. Cela vous permet d’itérer rapidement et facilite le processus de déploiement.
  5. Domaine personnalisé et HTTPS: avec Pages, vous pouvez connecter un domaine personnalisé à votre site et il fournit un HTTPS gratuit et automatique sur tous les sites pour garantir que la connexion est toujours sécurisée.
  6. Déploiements d’aperçu: chaque fois que vous créez une nouvelle demande d’extraction dans votre référentiel GitHub lié, Cloudflare Pages génère automatiquement une URL d’aperçu unique, vous permettant de voir vos modifications avant de les mettre en ligne.

Que vous soyez un développeur solo ou que vous fassiez partie d’une grande équipe, Cloudflare Pages offre un moyen simple, rapide et sécurisé de mettre vos sites Web en ligne.

À la lumière de ce qui précède, pour ce système d’enregistrement des utilisateurs, j’ai opté pour des pages HTML pures et simples, en évitant tout cadre ou outil de construction supplémentaire. Cette approche garantit une simplicité inégalée et offre la flexibilité nécessaire pour atteindre n’importe quel résultat souhaité.

Qu’est-ce que Cloudflare Workers?

Cloudflare Workers est une plateforme informatique sans serveur innovante qui permet aux développeurs de déployer leur code directement sur le vaste réseau de Cloudflare, qui s’étend sur plus de 200 villes dans le monde. Essentiellement, cela permet aux applications de s’exécuter aussi près que possible des utilisateurs finaux, réduisant ainsi la latence et améliorant l’expérience utilisateur.

Voici un aperçu de ses fonctionnalités et avantages :

  1. Environnement d’exécution sans serveur: Cloudflare Workers fonctionne dans un environnement sans serveur, ce qui signifie que les développeurs n’ont pas à gérer ou entretenir de serveurs. Au lieu de cela, ils peuvent se concentrer sur l’écriture de leur code, tandis que la plateforme s’occupe du reste, de la distribution à la mise à l’échelle.
  2. Edge Computing: contrairement aux modèles traditionnels dans lesquels les applications s’exécutent sur un seul serveur ou centre de données, Cloudflare Workers amène votre code à la périphérie du réseau Cloudflare. Cela garantit que votre application fonctionne plus près de l’utilisateur, offrant ainsi des performances et une vitesse améliorées.
  3. Flexibilité du langage: Workers utilise le moteur JavaScript V8, le même moteur d’exécution utilisé par Chrome, qui permet aux développeurs d’écrire du code en JavaScript. De plus, grâce au support WebAssembly, d’autres langages comme Rust, C et C++ peuvent également être utilisés.
  4. Sécurité: en tirant parti de la sécurité inhérente au réseau Cloudflare, Workers contribue à protéger les applications contre diverses menaces telles que les attaques DDoS.

Cloudflare Workers propose une solution innovante et hautement évolutive pour les développeurs cherchant à améliorer les performances, la fiabilité et la sécurité de leurs applications.

Dans les pages Cloudflare, les Workers sont hébergés dans un répertoire nommé functions. J’ai positionné tout mon code JavaScript/TypeScript dans cet espace, en exploitant les fonctionnalités complètes offertes par Workers.

Qu’est-ce que Cloudflare Workers KV?

Cloudflare Workers KV (Key-Value) est un système de stockage clé-valeur distribué à l’échelle mondiale, finalement cohérent, qui vous permet pour stocker et accéder aux données depuis n’importe où dans vos scripts Cloudflare Workers. Il est conçu pour vous aider à faire évoluer et à simplifier la gestion de l’état dans les environnements sans serveur.

Voici ses principales caractéristiques et avantages :

  1. Distribution mondiale: Cloudflare Workers KV est construit sur le réseau Cloudflare, qui s’étend sur plus de 300 villes dans le monde. Cela garantit que vos données sont stockées et accessibles à proximité de vos utilisateurs, réduisant ainsi la latence et améliorant les performances globales de vos applications.
  2. Lectures et écritures rapides: Workers KV fournit un accès aux données à faible latence adapté à une variété d’applications. Même si les écritures mettent un peu plus de temps à se propager à l’échelle mondiale (généralement en quelques secondes), les opérations de lecture sont généralement rapides, ce qui les rend idéales pour les charges de travail lourdes en lecture.
  3. À grande échelle: vous pouvez stocker des milliards de clés dans un seul espace de noms Workers KV et chaque clé peut contenir une valeur allant jusqu’à 25Mo.
  4. Espaces de noms: les espaces de noms KV sont des conteneurs pour vos paires clé-valeur. Ils vous permettent de séparer différents ensembles de données au sein de votre magasin Workers KV, ce qui peut être particulièrement utile lors de la gestion de plusieurs applications ou environnements (comme la préparation et la production).
  5. Cohérence éventuelle: Workers KV utilise la cohérence éventuelle. Cela signifie que les mises à jour de vos données (écritures) se propageront à l’échelle mondiale et deviendront cohérentes au fil du temps, ce qui ne prend généralement que quelques secondes.

Cloudflare Workers KV présente une solution unique pour gérer l’état dans les environnements sans serveur, offrant aux développeurs un système de stockage de données fiable, rapide et distribué à l’échelle mondiale.

Lors du développement de ce système d’enregistrement des utilisateurs, j’ai stratégiquement conçu les espaces de noms Workers KV suivants:

  • USERS: cela sert de stockage principal pour tous les utilisateurs. Il est conçu pour gérer un nombre essentiellement infini d’enregistrements.
  • USERS_LOGIN_HISTORY: un espace dédié à la chronique des activités de connexion, permettant aux utilisateurs d’évaluer périodiquement l’empreinte de sécurité de leur compte.
  • USERS_SESSIONS: cet espace de noms capture des détails sur l’utilisateur actuellement connecté, notamment des identifiants uniques, des appareils, des emplacements et bien plus encore.
  • USERS_SESSIONS_MAPPING: En raison du modèle de cohérence éventuel de Workers KV, il peut y avoir des délais entre l’écriture dans USERS_SESSIONS et sa vérification. Cela est particulièrement probable si les opérations ont lieu dans différents emplacements périphériques. Pour contourner cela, après validation, j’ajoute directement le nouvel UID de session à USERS_SESSIONS_MAPPING, en m’assurant qu’il est inclus avant même d’être écrit dans USERS_SESSIONS.
  • USERS_TEMP: j’ai utilisé cet espace de noms comme référentiel pour les liens transitoires (temporaires) et autres contenus ayant une expiration prédéterminée.

Nous avons créé des bases de données avec une capacité illimitée, une mise à l’échelle automatique et une haute disponibilité, des fonctionnalités que l’on retrouve souvent dans les bases de données plus coûteuses.

Conception du projet

Mon objectif était de créer quelque chose de simple et efficace sans recourir à des bibliothèques tierces, et j’ai réussi. Voici comment se déroule l’ensemble de la structure du projet:

 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

Pour vous donner une idée claire de l’architecture ci-dessus, examinons en détail:

  • framework: ce répertoire héberge notre code TypeScript fondamental. Tout, des modèles de données aux modèles d’e-mails, se trouve ici, garantissant une approche cohérente dans l’ensemble de notre système.
  • functions: vous trouverez ici le code TypeScript spécialement conçu pour les fonctions Cloudflare Pages, rationalisant les opérations backend du site.
  • public: tous nos fichiers HTML statiques accessibles au public résident dans ce dossier, formant l’interface visible pour les utilisateurs.

En termes simples, lorsque vous accédez à la page login.html, Cloudflare Pages entre en action et exécute le code login.ts correspondant. Cette interaction dynamique se poursuit sur toutes les pages et leurs fonctions associées.

Avec cette configuration, nous pouvons aborder de manière transparente une gamme de tâches. Qu’il s’agisse de réécriture de contenu, de traitement de données ou de récupération de données via Cloudflare Workers KV, tout est géré efficacement.

Création du système d’enregistrement des utilisateurs

Pour commencer notre voyage, nous allons d’abord établir un système d’enregistrement des utilisateurs. Il s’agit de la fonctionnalité de base.

  1. Notre première étape consiste à concevoir un formulaire HTML simple et à le placer dans register.html, suivi de la création de la fonction responsable du traitement des données fournies:
 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>

Une fois notre formulaire configuré, l’étape suivante consiste à améliorer la soumission des données à l’aide de JavaScript. Bien que la soumission de formulaire conventionnelle soit parfaitement viable, j’ai incorporé la validation Bootstrap 5 dans mon exemple, m’orientant ainsi vers AJAX pour la publication des données.

Voici un exemple fonctionnel tiré de ma démo:

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

Une fois que vous avez cliqué sur Créer un compte, les données sont publiées dans /register via AJAX, déclenchant le code functions/register.ts. Ce mécanisme permet le traitement des données GET et POST. Bien que la gestion des OPTIONS soit possible, nous la mettrons de côté pour cette discussion.

  1. Pour les visites de pages standard ou les exécutions GET, il est possible d’introduire une multitude de fonctionnalités avancées, telles que la protection CSRF. Jetons un coup d’œil à l’exemple suivant:
 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};

Dans ce processus, nous utilisons la fonctionnalité Cloudflare Workers HTMLRewriter pour produire et intégrer un jeton CSRF unique dans le formulaire pour chaque demande de page.

J’approfondirai ma stratégie CSRF dans les sections suivantes. Pour le moment, observez comment j’ajoute dynamiquement un code CSRF unique et aléatoire pour renforcer la sécurité.

  1. Passons maintenant à la phase d’exécution POST. Ici, nous validons méticuleusement les données saisies, les stockons en toute sécurité, puis envoyons un e-mail à l’utilisateur pour confirmation ou instructions supplémentaires. Pour vous fournir un aperçu conceptuel, j’ai rédigé une représentation pseudo-code:
 1/**
 2 * POST /register
 3 */
 4export const onRequestPost: PagesFunction<{ USERS: KVNamespace; }> = async ({ request, env }) => 
 5{
 6	// Valider le type de contenu
 7	// Valider notre CSRF avant de faire quoi que ce soit
 8	// Vérifie l'e-mail de l'utilisateur existant
 9	// Génère un nouveau sel et hache le mot de passe d'origine
10	// Stocke l'utilisateur avec quelques métadonnées
11	// Etc
12}

Dans ma configuration, après une inscription réussie, les détails d’un utilisateur sont stockés dans Workers KV comme suit:

 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}

Vous pouvez ajuster ce format, en ajoutant ou en supprimant des champs en fonction de vos besoins. Je dois souligner que je ne fournirai pas de code complet et prêt à copier. Il est essentiel d’investir dans la compréhension et l’exploration pratique. Ce n’est qu’alors que l’on pourra véritablement évoluer en tant que développeur.

Création du système de connexion utilisateur

La transition vers le mécanisme de connexion pour votre entreprise Cloudflare Pages est un jeu d’enfant. Nous adopterons une approche similaire à celle que nous avons adoptée pour le processus d’inscription.

  1. Tout comme nous avons créé le formulaire d’inscription, créez un formulaire concis pour le processus de connexion et hébergez-le dans 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. Après avoir configuré le formulaire, il est temps de traiter son rendu. L’étape suivante consiste à créer le code nécessaire. Cela sera automatiquement déclenché depuis login.ts:
1/**
2 * GET /login
3 */
4export const onRequestGet: PagesFunction = async ({ next }) => 
5{
6	// Récupère le contenu de la page d'origine
7	// Préparer nos données CSRF
8	// Réécrivez le contenu et diffusez-le à l'utilisateur (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	// Valider le type de contenu
11	// Valider notre CSRF avant de faire quoi que ce soit
12	// Vérifie l'e-mail de l'utilisateur existant
13	// Génère un nouveau sel et hache le mot de passe d'origine
14	// Comparez les mots de passe
15	// Sauvegarder la session
16	// Mettre à jour l'historique
17	// Récupère le mappage actuel pour cet utilisateur (s'il existe)
18	// Ajoute le nouvel UID de session au mappage
19	// Stocke le mappage mis à jour
20	// Etc
21};

Grâce à ces 3 étapes rationalisées, nous avons sculpté de manière transparente le système de connexion. Indéniablement, le mécanisme de connexion est le segment le plus complexe, nécessitant plusieurs opérations comme annoté. Pourtant, ne considérez pas cela comme une complication mais comme un défi revigorant à relever !

Création du système de réinitialisation du mot de passe utilisateur

  1. En faisant des parallèles avec nos composants précédents, nous commençons par sculpter le formulaire dédié pour lancer une réinitialisation de mot de passe:
 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>

Le concept sous-jacent est simple: lors de la saisie de l’e-mail d’un utilisateur, s’il est reconnu dans notre système, un lien unique de réinitialisation du mot de passe est généré et envoyé. Pour plus de sécurité, assurez-vous que ce lien reste actif pendant une courte durée, idéalement pas plus de 2 heures.

  1. Lancez la création du code dédié à la gestion de l’exécution de GET, que vous souhaiterez intégrer dans forgot-password.ts:
1/**
2 * GET /forgot-password
3 */
4export const onRequestGet: PagesFunction = async ({ next }) => 
5{
6	// Récupère le contenu de la page d'origine
7	// Préparer nos données CSRF
8	// Réécrivez le contenu et diffusez-le à l'utilisateur (async)
9};
  1. Continuez en formulant le code responsable de l’exécution du POST, en vous assurant qu’il est hébergé dans le même fichier forgot-password.ts:
 1/**
 2 * POST /forgot-password
 3 */
 4export const onRequestPost: PagesFunction<{ 
 5	USERS: KVNamespace;
 6	USERS_TEMP: KVNamespace; 
 7	}> = async ({ request, env }) => 
 8{
 9	// Valider le type de contenu
10	// Valider notre CSRF avant de faire quoi que ce soit
11	// Vérifie l'e-mail de l'utilisateur existant
12	// Génère une chaîne unique à hacher
13	// Hachez la chaîne
14	// Enregistrer sur KV, expiration 2 heures
15	// Envoi d'un email à l'utilisateur
16	// Etc
17};

C’est à vous maintenant de terminer le puzzle de codage. En suivant les conseils contenus dans mes commentaires, vous devriez trouver le processus tout à fait gérable. Une fois terminé, vous serez équipé d’un système de réinitialisation de mot de passe entièrement opérationnel.

Remarque: évitez d’afficher des messages tels que Aucun e-mail de ce type n'existe. De tels indicateurs sont des mines d’or pour les pirates informatiques, ouvrant la voie à d’éventuelles attaques par force brute. Adoptez plutôt une approche plus ambiguë mais plus conviviale: Si l'e-mail est enregistré dans notre système, vous recevrez un lien de réinitialisation.

Démo d’inscription des utilisateurs

Explorez une démo en direct du processus d’enregistrement des utilisateurs que j’ai établi: https://members.mecanik.dev/

Les fonctionnalités incluent:

  • Capacités d’enregistrement, de connexion et de réinitialisation du mot de passe
  • Accès à votre profil
  • Gérer les paramètres de sécurité et sessions actives - Vous pouvez vous déconnecter par exemple si vous vous êtes connecté depuis votre mobile.
  • Prévisualisez les logiciels gratuits et recherchez les offres premium à venir.

Je jongle actuellement avec d’autres projets, mais je continuerai à enrichir cette plateforme. N’hésitez pas à le tester - assurez-vous simplement d’utiliser des e-mails authentiques pour éviter d’avoir un impact sur ma réputation SendGrid.

J’apprécie tous les commentaires ou problèmes signalés. N’hésitez pas à nous contacter si vous en avez !

Questions et réponses

Il ne fait aucun doute que la sécurité est une priorité. Vous pourriez remettre en question la robustesse du système face aux attaques potentielles et aux violations de données.

Considérez que Cloudflare Pages fonctionne sur la plate-forme Cloudflare, vous donnant accès à leur pare-feu d’application Web (WAF) avancé et à une suite d’outils de sécurité.

Comment fonctionne réellement le CSRF?

Vous avez peut-être observé la génération dynamique et l’injection d’un jeton CSRF dans le formulaire à chaque chargement. Voici comment ce mécanisme assure la sécurité des requêtes:

Composants du jeton CSRF:

  • IP: capture l’adresse IP actuelle de l’utilisateur.
  • Pays: identifie l’emplacement actuel de l’utilisateur.
  • UserAgent: enregistre les détails du navigateur de l’utilisateur.
  • expiration: définit une minuterie en ajoutant une minute à l’heure actuelle.

Ces données sont assemblées dans un format JSON : {i, c, u, e}, qui est ensuite chiffrée et transformée en Hex :

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

À propos de la fonction de cryptage:

  1. Les données sont cryptées à l’aide d’un mot de passe défini par l’utilisateur, générant une clé de mot de passe et en dérivant ensuite une clé AES.
  2. Cette clé AES crypte les données, avec le sel et le IV (vecteur d’initialisation) générés à la volée.
  3. La sortie chiffrée est un ArrayBuffer encapsulant le nombre d’itérations, le sel, l’IV et les données chiffrées.

En termes simples, ce processus de cryptage respecte les pratiques standard de l’industrie pour rendre les données cryptées à la fois indéchiffrables et protégées contre la falsification.

Validation du jeton CSRF:

 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: "Jeton CSRF invalide. Veuillez actualiser la page et réessayer." }
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: "Jeton CSRF invalide. Veuillez actualiser la page et réessayer." }
31		}),
32		{
33			status: 403,
34			headers: { 'content-type': 'application/json;charset=UTF-8'
35		}
36	}); 
37}

Le processus de validation vérifie rigoureusement le jeton CSRF, garantissant qu’il est:

  • Provenant de la même adresse IP.
  • Expédié depuis le même pays.
  • Envoyé depuis le même navigateur.
  • Soumis dans son délai actif.
  • Si l’une de ces vérifications échoue, le système identifie le jeton CSRF comme invalide, offrant ainsi une protection robuste contre les menaces potentielles.

Si l’une de ces vérifications échoue, le système identifie le jeton CSRF comme invalide, offrant ainsi une protection robuste contre les menaces potentielles.

Hachage/chiffrement du mot de passe

Pour le hachage/chiffrement des mots de passe, j’ai utilisé PBKDF2 (Password-Based Key Derivation Function 2) en tandem avec le SHA-256 algorithme de hachage.

La méthode utilise un sel pseudo-aléatoire unique et cryptographiquement sécurisé pour chaque mot de passe, garantissant ainsi une sécurité renforcée. Cette approche offre une protection contre les attaques par table arc-en-ciel et par force brute.

La version hachée du mot de passe est stockée, et non le texte en clair, ce qui renforce encore l’intégrité des données.

Pourquoi aucune utilisation du jeton JWT?

Les JWT (JSON Web Tokens) sont une méthode populaire pour gérer l’authentification des utilisateurs et transmettre des informations entre les parties de manière compacte et sécurisée pour les URL.

Bien qu’ils présentent de nombreux avantages, il existe des raisons pour lesquelles on pourrait choisir de ne pas les utiliser dans des contextes spécifiques, comme un système complet d’enregistrement des utilisateurs. Voici un aperçu plus approfondi:

  1. Apatridie et révocation: L’une des caractéristiques des JWT est leur apatridie. Cependant, cette nature même peut poser problème pour les systèmes de gestion des utilisateurs. Par exemple, si le jeton JWT d’un utilisateur est volé ou compromis, il n’existe aucun moyen simple de révoquer ce jeton à moins de maintenir une liste noire de jetons, ce qui va à l’encontre de l’objectif de l’apatridie.
  2. Taille: à mesure que vous ajoutez plus de données à un JWT, sa taille augmente. Si vous disposez d’un système complet d’enregistrement des utilisateurs dans lequel vous devrez peut-être stocker davantage de données relatives aux utilisateurs dans le jeton, cela peut entraîner des en-têtes HTTP plus volumineux et une latence accrue.
  3. Sécurité du stockage: pour le stockage côté client des JWT, les emplacements de stockage courants incluent le stockage local ou les cookies. Le stockage local est vulnérable aux attaques XSS et, même si les cookies peuvent être sécurisés dans une plus grande mesure, ils ouvrent la voie aux attaques CSRF s’ils ne sont pas gérés correctement.
  4. Gestion de l’expiration: la gestion de l’expiration des JWT peut être complexe. Bien que les jetons de courte durée réduisent les risques, ils nécessitent un mécanisme d’actualisation, ce qui introduit davantage de complexité dans le flux d’authentification.
  5. Aucune révocation intégrée: Comme mentionné précédemment, si un jeton est compromis, il n’existe aucun mécanisme inhérent pour le révoquer ou l’invalider jusqu’à son expiration. Cela peut constituer un risque de sécurité important si le jeton a une longue durée de vie.
  6. Complexité pour les développeurs: pour ceux qui ne sont pas familiers avec les JWT, il existe une courbe d’apprentissage pour comprendre comment les générer, les valider et les utiliser correctement. Cette complexité peut introduire des erreurs et des vulnérabilités si elle n’est pas bien comprise et mise en œuvre.

Lors de mon exploration de divers systèmes et de leur traitement des données utilisateur, j’ai découvert que certaines entreprises stockaient une abondance d’informations dans leurs jetons JWT, y compris des détails sensibles tels que les rôles des utilisateurs, entre autres.

Pour être clair, mon intention n’est pas de critiquer ou de calomnier d’autres approches. Cependant, compte tenu de ma position professionnelle en matière de sécurité, l’intégration de telles informations critiques directement dans les jetons suscite des inquiétudes. Il existe un risque inhérent associé à l’exposition de plus de données que nécessaire, en particulier lorsque ces données pourraient potentiellement être exploitées pour déduire des privilèges d’utilisateur ou d’autres fonctionnalités.

Compte tenu de ces réserves, j’ai pris la décision consciente de m’éloigner de cette méthode. Au lieu de cela, je me suis tourné vers l’authentification éprouvée côté serveur combinée à une gestion sécurisée des cookies. Cette approche, à mon avis, établit un équilibre entre l’expérience utilisateur et une sécurité robuste, garantissant que les données sensibles restent protégées et que le système reste résilient contre les vulnérabilités potentielles.

Envoi d’e-mails de manière fiable

J’ai opté pour SendGrid avec des modèles dynamiques compte tenu de son adoption généralisée et de ma profonde connaissance de ses nuances. Pour approfondir, reportez-vous à leur documentation API .

Même si son intégration peut poser certaines complexités, tout cela fait partie du défi gratifiant. Voici un exemple pour vous:

 1const SENDGRID_API_URL = 'https://api.sendgrid.com/v3/mail/send';
 2const SENDGRID_API_KEY = 'YOUR_SENDGRID_API_KEY'; // Remplacez par votre clé API réelle
 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('E-mail envoyé avec succès!');
40        } else {
41            const responseBody = await response.json();
42            console.error('Échec de envoi de e-mail:', responseBody.errors);
43        }
44    } catch (error) {
45        console.error('Erreur lors de envoi de e-mail:', 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});

Comment augmenter encore la sécurité ?

Même si Cloudflare Workers KV chiffre les données stockées, vous pouvez ajouter une couche supplémentaire de chiffrement pour les données utilisateur.

Ou souhaitez-vous ajouter une touche particulière aux attaquants potentiels? Pensez à implémenter des entrées de formulaire factices pour confondre les robots et les pirates:

 1return new HTMLRewriter()
 2	.on("form", 
 3	{
 4	  element(form) 
 5	  {
 6		// Entrées factices... juste pour donner encore plus de maux de tête au hacker et le confondre :)
 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);

Cet extrait génère dynamiquement entre 1 et 5 entrées nommées de manière aléatoire à chaque chargement de page.

Les capacités sont presque illimitées, il vous suffit d’utiliser votre imagination.

Emballer

Et voilà ! J’espère sincèrement que cet article a été à la fois perspicace et engageant pour vous. Que vous soyez un développeur chevronné ou que vous plongez simplement dans le monde des sites Web cloud évolutifs, le partage des connaissances est essentiel à la croissance de notre communauté.

Si vous avez trouvé cet article utile, pensez à le transmettre à d’autres développeurs ou à ceux intéressés par le sujet. Chaque partage élargit notre compréhension collective.

Vos commentaires et questions sont extrêmement appréciés. N’hésitez pas à laisser un commentaire ou à nous contacter – poursuivons la conversation. Codage sécurisé!