Nel panorama in evoluzione dello sviluppo web, le applicazioni serverless stanno progressivamente lasciando il segno. I loro innegabili vantaggi, come la scalabilità senza pari e le prestazioni robuste, le rendono eccezionali. La bellezza del serverless risiede nella sua promessa: offrire soluzioni potenti che non solo scalano senza sforzo, ma sono anche economiche.

Hai mai sognato di lanciare un’idea imprenditoriale, solo per essere scoraggiato dalle complessità tecniche della creazione di un sistema di registrazione e login utente? Sebbene piattaforme come Netlify e vari database cloud-based esistano, non si comparano alle capacità offerte da Cloudflare. Molte alternative possono far lievitare i costi durante il ridimensionamento, ma con Cloudflare Pages la storia è diversa.

Ho lavorato in modo intermittente a questo progetto e demo negli ultimi mesi, gestendolo insieme ai miei altri impegni. Mi scuso per l’attesa, specialmente con coloro che hanno atteso con impazienza questo sistema.

Sblocca la potenza di Cloudflare Pages

  • Scalabilità senza interruzioni: Gestisci registrazioni utente illimitate senza intoppi.
  • Efficienza dei costi: Dì addio ai costi imprevisti; goditi una tariffazione costante.
  • Velocità fulminee: Sperimenta prestazioni mai viste prima.
  • Sicurezza solida: I dati dei tuoi utenti rimangono protetti e al sicuro.

Cosa utilizzeremo

  • Cloudflare Pages
  • Cloudflare Pages Functions
  • Cloudflare Workers KV

Cos’è Cloudflare Pages?

Cloudflare Pages è una piattaforma moderna e intuitiva per gli sviluppatori per costruire, distribuire e ospitare i propri siti web. Offre un’integrazione perfetta con GitHub, il che significa che puoi semplicemente fare push del tuo codice su GitHub e Cloudflare Pages si occuperà del resto: build, distribuzione e persino aggiornamenti.

Ecco come funziona:

  1. Workflow integrato: Cloudflare Pages è costruito attorno al workflow git. Una volta collegato il tuo repository GitHub a Cloudflare Pages, inizia a costruire e distribuire il tuo sito ogni volta che fai push sul branch selezionato.
  2. Ottimizzato per JAMstack: Cloudflare Pages supporta i principi JAMstack, il che significa che puoi costruire il tuo sito con il tuo generatore di siti statici o framework JavaScript preferito, inclusi ma non limitati a Jekyll, Hugo, Next.js e React.
  3. Distribuzione veloce e sicura: Alimentato dalla rete Cloudflare distribuita globalmente, Pages garantisce che il tuo sito sia disponibile e veloce, indipendentemente da dove si trovi il tuo pubblico. Inoltre, le funzionalità di sicurezza intrinseche di Cloudflare proteggono il tuo sito dalle minacce.
  4. Distribuzione continua: Cloudflare Pages costruisce e distribuisce automaticamente il tuo sito ogni volta che effettui aggiornamenti sul tuo repository GitHub. Questo ti permette di iterare rapidamente e rende il processo di distribuzione un gioco da ragazzi.
  5. Dominio personalizzato e HTTPS: Con Pages, puoi collegare un dominio personalizzato al tuo sito, e fornisce HTTPS gratuito e automatico su tutti i siti per garantire che la connessione sia sempre sicura.
  6. Distribuzioni di anteprima: Ogni volta che crei un nuovo pull request nel tuo repository GitHub collegato, Cloudflare Pages genera automaticamente un URL di anteprima unico, permettendoti di vedere le modifiche prima di andare in produzione.

Che tu sia uno sviluppatore indipendente o parte di un grande team, Cloudflare Pages offre un modo semplice, veloce e sicuro per mettere online i tuoi siti web.

Alla luce di quanto sopra, per questo sistema di registrazione utente, ho optato per pagine HTML pure e dirette, evitando framework aggiuntivi o strumenti di build. Questo approccio garantisce una semplicità senza pari e concede la flessibilità di ottenere qualsiasi risultato desiderato.

Cos’è Cloudflare Workers?

Cloudflare Workers è una piattaforma innovativa di calcolo serverless che permette agli sviluppatori di distribuire il proprio codice direttamente sulla vasta rete di Cloudflare, che si estende in più di 200 città nel mondo. Essenzialmente, permette alle applicazioni di funzionare il più vicino possibile agli utenti finali, riducendo così la latenza e migliorando l’esperienza utente.

Ecco una panoramica delle sue funzionalità e vantaggi:

  1. Ambiente di esecuzione serverless: Cloudflare Workers opera in un ambiente serverless, il che significa che gli sviluppatori non devono gestire o mantenere alcun server. Invece, possono concentrarsi sulla scrittura del codice, mentre la piattaforma si occupa del resto, dalla distribuzione alla scalabilità.
  2. Edge Computing: A differenza dei modelli tradizionali dove le applicazioni funzionano su un singolo server o data center, Cloudflare Workers porta il tuo codice al margine della rete Cloudflare. Questo garantisce che la tua applicazione funzioni più vicino all’utente, offrendo prestazioni e velocità migliorate.
  3. Flessibilità linguistica: Workers utilizza il motore JavaScript V8, lo stesso runtime utilizzato da Chrome, che permette agli sviluppatori di scrivere codice in JavaScript. Inoltre, grazie al supporto WebAssembly, possono essere utilizzati anche altri linguaggi come Rust, C e C++.
  4. Sicurezza: Sfruttando la sicurezza intrinseca della rete Cloudflare, Workers aiuta a proteggere le applicazioni da una varietà di minacce come gli attacchi DDoS.

Cloudflare Workers fornisce una soluzione innovativa e altamente scalabile per gli sviluppatori che cercano di migliorare le prestazioni, l’affidabilità e la sicurezza delle proprie applicazioni.

All’interno di Cloudflare Pages, i Workers sono ospitati in una directory chiamata functions. Ho posizionato tutto il mio codice JavaScript/TypeScript in questo spazio, sfruttando le capacità complete che Workers offre.

Cos’è Cloudflare Workers KV?

Cloudflare Workers KV (Key-Value) è un sistema di archiviazione chiave-valore distribuito globalmente e eventualmente consistente che ti permette di archiviare e accedere ai dati ovunque nei tuoi script Cloudflare Workers. È progettato per aiutarti a scalare e semplificare la gestione dello stato in ambienti serverless.

Ecco le sue caratteristiche e vantaggi principali:

  1. Distribuzione globale: Cloudflare Workers KV è costruito sulla rete Cloudflare, che si estende in oltre 300 città nel mondo. Questo garantisce che i tuoi dati siano archiviati e accessibili vicino ai tuoi utenti, riducendo la latenza e migliorando le prestazioni complessive delle tue applicazioni.
  2. Letture e scritture veloci: Workers KV fornisce accesso ai dati a bassa latenza adatto a una varietà di applicazioni. Mentre le scritture impiegano un po’ più di tempo per propagarsi globalmente (di solito entro pochi secondi), le operazioni di lettura sono tipicamente veloci, rendendolo ideale per carichi di lavoro a predominanza di lettura.
  3. Grande scalabilità: Puoi archiviare miliardi di chiavi in un singolo namespace Workers KV e ogni chiave può contenere un valore fino a 25MB.
  4. Namespace: I namespace KV sono contenitori per le tue coppie chiave-valore. Ti permettono di segregare diversi set di dati all’interno del tuo archivio Workers KV, il che può essere particolarmente utile quando gestisci più applicazioni o ambienti (come staging e produzione).
  5. Consistenza eventuale: Workers KV utilizza la consistenza eventuale. Ciò significa che gli aggiornamenti ai tuoi dati (scritture) si propagheranno globalmente e diventeranno consistenti nel tempo, che di solito è una questione di pochi secondi.

Cloudflare Workers KV presenta una soluzione unica per la gestione dello stato in ambienti serverless, fornendo agli sviluppatori un sistema di archiviazione dati affidabile, veloce e distribuito globalmente.

Nello sviluppo di questo sistema di registrazione utenti, ho progettato strategicamente i seguenti namespace Workers KV:

  • USERS: Questo serve come archivio primario per tutti gli utenti. È progettato per gestire un numero essenzialmente infinito di record.
  • USERS_LOGIN_HISTORY: Uno spazio dedicato per registrare le attività di login, consentendo agli utenti di valutare periodicamente l’impronta di sicurezza del proprio account.
  • USERS_SESSIONS: Questo namespace cattura i dettagli dell’utente attualmente connesso, inclusi ID univoci, dispositivi, posizioni e altro.
  • USERS_SESSIONS_MAPPING: A causa del modello di consistenza eventuale di Workers KV, possono esserci ritardi tra la scrittura in USERS_SESSIONS e il suo controllo. Questo è particolarmente probabile se le operazioni avvengono in posizioni edge diverse. Per aggirare questo problema, dopo la validazione, aggiungo direttamente il nuovo UID della sessione a USERS_SESSIONS_MAPPING, assicurandone l’inclusione anche prima della scrittura in USERS_SESSIONS.
  • USERS_TEMP: Ho utilizzato questo namespace come repository per link e altri contenuti transitori (temporanei) con una scadenza predeterminata.

Abbiamo creato database con capacità illimitata, auto-scaling e alta disponibilità, funzionalità spesso presenti in database più costosi.

Progettazione del progetto

Il mio obiettivo era creare qualcosa di semplice ed efficace senza affidarsi a librerie di terze parti, e ci sono riuscito. Ecco come si sviluppa l’intera struttura del progetto:

 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
32dashboard.html

Per darvi un quadro chiaro dell’architettura sopra descritta, approfondiamo con un’analisi dettagliata:

  • framework: Questa directory ospita il nostro codice TypeScript fondamentale. Tutto, dai modelli dati ai template email, si trova qui, garantendo un approccio coerente in tutto il nostro sistema.
  • functions: Qui troverai il codice TypeScript specificamente progettato per le funzioni di Cloudflare Pages, ottimizzando le operazioni backend del sito.
  • public: Tutti i nostri file HTML statici accessibili pubblicamente risiedono in questa cartella, formando l’interfaccia visibile per gli utenti.

In parole semplici, quando navighi alla pagina login.html, Cloudflare Pages entra in azione, eseguendo il corrispondente codice login.ts. Questa interazione dinamica continua attraverso tutte le pagine e le loro funzioni associate.

Con questa configurazione, possiamo gestire senza problemi una serie di compiti. Che si tratti di riscrittura di contenuti, elaborazione dati o recupero dati tramite Cloudflare Workers KV, tutto è gestito in modo efficiente.

Costruire il sistema di registrazione utente

Per iniziare il nostro percorso, stabiliremo prima un sistema di registrazione utente. Questa è la funzionalità principale.

  1. Il nostro primo passo è progettare un form HTML semplice e posizionarlo all’interno di register.html, seguito dalla creazione della funzione responsabile dell’elaborazione dei dati forniti:
 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>

Con il form configurato, il passo successivo è migliorare l’invio dei dati utilizzando JavaScript. Sebbene l’invio convenzionale del form sia perfettamente valido, nel mio esempio ho incorporato la validazione Bootstrap 5, orientandomi quindi verso AJAX per l’invio dei dati.

Ecco un esempio funzionante preso dalla mia 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
120})();
Una volta cliccato Create Account, i dati vengono inviati a /register tramite AJAX, attivando il codice functions/register.ts. Questo meccanismo permette l’elaborazione sia di dati GET che POST. La gestione di OPTIONS è possibile, ma la tralasceremo per questa discussione.

  1. Per le visite standard alla pagina, o esecuzioni GET, c’è il potenziale per introdurre una pletora di funzionalità avanzate, come la protezione CSRF. Diamo un’occhiata al seguente esempio:
 1/\*\*
 2
 3- GET /register
 4  \*/
 5  export const onRequestGet: PagesFunction = async ({ next }) =>
 6  {
 7  // Fetch the original page content
 8  const response = await next();
 9
10      // Prepare our CSRF data
11      const IP = request.headers.get('CF-Connecting-IP');
12      const Country = request.headers.get('CF-IPCountry') || '';
13      const UserAgent = request.headers.get('User-Agent');
14      const expiry = Date.now() + 60000;
15      const CSRF_TOKEN = JSON.stringify({i: IP, c: Country, u: UserAgent, e: expiry});
16      const encryptedData = await encryptData(new TextEncoder().encode(CSRF_TOKEN), env.CSRF_SECRET, 10000);
17      const hex = Utils.BytesToHex(new Uint8Array(encryptedData));
18
19      // Rewrite the content and stream it back to the user (async)
20      return new HTMLRewriter()
21      .on("form", {
22        element(form) {
23      	// The CSRF input
24      	form.append(
25      	  `<input type="hidden" name="csrf" value="${hex}" />`,
26      	  { html: true }
27      	);
28        },
29      })
30      .transform(response);
31
32  };
33  

In questo processo, utilizziamo la funzionalità HTMLRewriter di Cloudflare Workers per produrre e incorporare un token CSRF unico nel form per ogni richiesta di pagina.

Approfondirò la mia strategia CSRF nelle sezioni successive. Per il momento, osserva come aggiungo dinamicamente un codice CSRF unico e casuale per rafforzare la sicurezza.

  1. Ora, passiamo alla fase di esecuzione POST. Qui, validiamo meticolosamente i dati di input, li archiviamo in modo sicuro e poi inviamo un’email all’utente per conferma o ulteriori istruzioni. Per fornirvi una panoramica concettuale, ho redatto una rappresentazione in pseudo-codice:
 1/\*\*
 2
 3- POST /register
 4  \*/
 5  export const onRequestPost: PagesFunction<{ USERS: KVNamespace; }> = async ({ request, env }) =>
 6  {
 7  // Validate content type
 8  // Validate our CSRF before doing anything
 9  // Check for existing user email
10  // Generate a new salt & hash the original password
11  // Store the user with some meta-data
12  // Etc
13  }
14  

Nella mia configurazione, dopo una registrazione riuscita, i dettagli dell’utente vengono archiviati in Workers KV come segue:

 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}

Puoi regolare questo formato, aggiungendo o rimuovendo campi in base alle tue esigenze. Devo sottolineare che non fornirò un codice completo e pronto da copiare. È essenziale investire nella comprensione e nell’esplorazione pratica. Solo così si può veramente evolversi come sviluppatore.

Costruire il sistema di login utente

Passare al meccanismo di login per il tuo progetto Cloudflare Pages è un gioco da ragazzi. Adotteremo un approccio simile a quello usato per il processo di registrazione.

  1. Proprio come abbiamo creato il form di registrazione, crea un form conciso per il processo di login e posizionalo all’interno di 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. Dopo aver configurato il form, è il momento di elaborarne il rendering. Il passaggio successivo prevede la creazione del codice necessario. Questo verrà attivato automaticamente da login.ts:
 1/\*\*
 2
 3- GET /login
 4  \*/
 5  export const onRequestGet: PagesFunction = async ({ next }) =>
 6  {
 7  // Fetch the original page content
 8  // Prepare our CSRF data
 9  // Rewrite the content and stream it back to the user (async)
10  };
11  
  1. L’ultimo passo è formulare il codice per gestire l’esecuzione POST, situato all’interno di login.ts:
 1/\*\*
 2
 3- POST /login
 4  \*/
 5  export const onRequestPost: PagesFunction<{
 6  USERS: KVNamespace;
 7  USERS_SESSIONS: KVNamespace;
 8  USERS_SESSIONS_MAPPING: KVNamespace;
 9  USERS_LOGIN_HISTORY: KVNamespace; }> = async ({ request, env }) =>
10  {
11  // Validate content type
12  // Validate our CSRF before doing anything
13  // Check for existing user email
14  // Generate a new salt & hash the original password
15  // Compare the passwords
16  // Save session
17  // Update history
18  // Retrieve the current mapping for this user (if it exists)
19  // Add the new session UID to the mapping
20  // Store the updated mapping
21  // Etc
22  };
23  

Attraverso questi 3 passaggi semplificati, abbiamo modellato senza problemi il sistema di login. Indubbiamente, il meccanismo di login è il segmento più intricato, che necessita di diverse operazioni come annotato. Tuttavia, considera questo non come una complicazione ma come una sfida stimolante da superare!

Costruire il sistema di reset password

  1. Tracciando paralleli con i nostri componenti precedenti, iniziamo modellando il form dedicato per avviare il reset della password:
 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
12</form>

Il concetto sottostante è semplice: all’inserimento dell’email di un utente, se riconosciuta nel nostro sistema, viene generato e inviato un link unico per il reset della password. Per una maggiore sicurezza, assicurati che questo link rimanga attivo per un breve periodo, idealmente non più di 2 ore.

  1. Inizia la creazione del codice dedicato alla gestione dell’esecuzione GET, che vorrai incorporare in forgot-password.ts:
 1/\*\*
 2
 3- GET /forgot-password
 4  \*/
 5  export const onRequestGet: PagesFunction = async ({ next }) =>
 6  {
 7  // Fetch the original page content
 8  // Prepare our CSRF data
 9  // Rewrite the content and stream it back to the user (async)
10  };
11  
  1. Continua formulando il codice responsabile dell’esecuzione POST, assicurandoti che sia ospitato nello stesso file forgot-password.ts:
 1/\*\*
 2
 3- POST /forgot-password
 4  \*/
 5  export const onRequestPost: PagesFunction<{
 6  USERS: KVNamespace;
 7  USERS_TEMP: KVNamespace;
 8  }> = async ({ request, env }) =>
 9  {
10  // Validate content type
11  // Validate our CSRF before doing anything
12  // Check for existing user email
13  // Generate a unique string to hash
14  // Hash the string
15  // Save to KV, expiration 2 hours
16  // Send email to user
17  // Etc
18  };
19  

Sta a te ora completare il puzzle del codice. Seguendo le indicazioni nei miei commenti, dovresti trovare il processo abbastanza gestibile. Una volta terminato, sarai equipaggiato con un sistema di reset password completamente operativo.

Nota di cautela: Evita di mostrare messaggi come “Non esiste tale email.” Tali indicatori sono miniere d’oro per gli hacker, aprendo la strada a potenziali attacchi brute force. Invece, adotta un approccio più ambiguo ma user-friendly: “Se l’email è registrata nel nostro sistema, riceverai un link di reset.”

Demo della registrazione utente

Esplora una demo dal vivo del processo di registrazione utente che ho creato: https://members.mecanik.dev/

Le funzionalità includono:

  • Registrazione, login e reset della password
  • Accesso al tuo profilo
  • Gestione delle impostazioni di sicurezza e delle sessioni attive - Puoi disconnetterti ad esempio se hai effettuato l’accesso dal tuo cellulare.
  • Anteprima di software gratuito e occhio alle prossime offerte premium.

Attualmente sto gestendo altri progetti, ma continuerò a migliorare questa piattaforma. Sentiti libero di testarla - assicurati solo di usare email genuine per non impattare la mia reputazione SendGrid.

Accolgo qualsiasi feedback o segnalazione di problemi. Non esitare a contattarmi!

Domande e risposte

Senza dubbio, la sicurezza è la priorità. Potresti mettere in discussione la robustezza del sistema contro potenziali attacchi e violazioni dei dati.

Considera che Cloudflare Pages opera sulla piattaforma Cloudflare, garantendoti l’accesso al loro avanzato Web Application Firewall (WAF) e a una suite di strumenti di sicurezza.

Come funziona effettivamente il CSRF?

Potresti aver osservato la generazione dinamica e l’iniezione di un token CSRF nel form ad ogni caricamento. Ecco come questo meccanismo garantisce la sicurezza delle richieste:

Componenti del token CSRF:

  • IP: Cattura l’indirizzo IP corrente dell’utente.
  • Country: Identifica la posizione corrente dell’utente.
  • UserAgent: Registra i dettagli del browser dell’utente.
  • expiry: Imposta un timer aggiungendo un minuto all’ora corrente.

Questi dati sono assemblati in formato JSON: {i, c, u, e}, che viene successivamente crittografato e trasformato in Hex:

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

Sulla funzione di crittografia:

  1. I dati vengono crittografati utilizzando una password definita dall’utente, generando una chiave password e successivamente derivando una chiave AES da essa.
  2. Questa chiave AES crittografa i dati, con il salt e l’IV (Vettore di Inizializzazione) generati al volo.
  3. L’output crittografato è un ArrayBuffer che incapsula il conteggio delle iterazioni, il salt, l’IV e i dati crittografati.

In parole semplici, questo processo di crittografia rispetta le pratiche standard del settore per rendere i dati crittografati sia indecifrabili che protetti contro la manomissione.

Validazione del token 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{
 7return new Response(JSON.stringify({
 8result: null,
 9success: false,
10// An ambigous message; don't tell the hacker what's missing.
11error: { code: 1001, message: "Invalid CSRF Token. Please refresh the page and try again." }
12}),
13{
14status: 403,
15headers: { '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{
26return new Response(JSON.stringify({
27result: null,
28success: false,
29// An ambigous message; don't tell the hacker what's missing.
30error: { code: 1002, message: "Invalid CSRF Token. Please refresh the page and try again." }
31}),
32{
33status: 403,
34headers: { 'content-type': 'application/json;charset=UTF-8'
35}
36});
37}

Il processo di validazione controlla rigorosamente il token CSRF, assicurando che:

  • Provenga dallo stesso indirizzo IP.
  • Sia inviato dallo stesso paese.
  • Sia inviato dallo stesso browser.
  • Sia inviato entro il suo periodo di attività.
  • Se uno qualsiasi di questi controlli fallisce, il sistema identifica il token CSRF come non valido, offrendo una protezione robusta contro potenziali minacce.

Se uno qualsiasi di questi controlli fallisce, il sistema identifica il token CSRF come non valido, offrendo una protezione robusta contro potenziali minacce.

Hashing/crittografia delle password

Per l’hashing/crittografia delle password, ho utilizzato PBKDF2 (Password-Based Key Derivation Function 2) in combinazione con l’algoritmo di hashing SHA-256 .

Il metodo utilizza un “salt” pseudocasuale unico e crittograficamente sicuro per ogni password, garantendo una sicurezza potenziata. Questo approccio offre protezione contro attacchi rainbow table e brute force.

La versione hashata della password viene archiviata, non il testo in chiaro, rafforzando ulteriormente l’integrità dei dati.

Perché non usare i token JWT?

I JWT (JSON Web Token) sono un metodo popolare per gestire l’autenticazione degli utenti e trasmettere informazioni tra le parti in modo compatto e sicuro per gli URL.

Sebbene abbiano molti vantaggi, ci sono ragioni per cui si potrebbe scegliere di non usarli in contesti specifici, come un sistema completo di registrazione utente. Ecco un’analisi più approfondita:

  1. Assenza di stato e revoca: Uno dei tratti distintivi dei JWT è la loro natura stateless. Tuttavia, questa stessa natura può essere problematica per i sistemi di gestione utenti. Ad esempio, se il token JWT di un utente viene rubato o compromesso, non c’è un modo diretto per revocare quel token a meno che non si mantenga una blacklist di token, il che vanifica lo scopo della statelessness.
  2. Dimensione: Man mano che aggiungi più dati a un JWT, cresce in dimensione. Se hai un sistema di registrazione utente completo dove potresti dover archiviare più dati relativi all’utente nel token, questo può portare a header HTTP più grandi e maggiore latenza.
  3. Sicurezza dell’archiviazione: Per l’archiviazione lato client dei JWT, le posizioni di archiviazione comuni includono il local storage o i cookie. Il local storage è vulnerabile agli attacchi XSS, e sebbene i cookie possano essere protetti in misura maggiore, aprono strade per attacchi CSRF se non gestiti correttamente.
  4. Gestione della scadenza: Gestire la scadenza dei JWT può essere complesso. Mentre i token a breve durata riducono il rischio, richiedono un meccanismo di refresh, che introduce maggiore complessità nel flusso di autenticazione.
  5. Nessuna revoca integrata: Come menzionato in precedenza, se un token è compromesso, non c’è un meccanismo intrinseco per revocarlo o invalidarlo fino alla sua scadenza. Questo può rappresentare un rischio significativo per la sicurezza se il token ha una lunga durata.
  6. Complessità per gli sviluppatori: Per chi non ha familiarità con i JWT, c’è una curva di apprendimento nel capire come generarli, validarli e usarli correttamente. Questa complessità può introdurre errori e vulnerabilità se non compresa e implementata a fondo.

Nella mia esplorazione di vari sistemi e del loro trattamento dei dati utente, ho scoperto che alcune aziende archiviano un’abbondanza di informazioni nei loro token JWT, inclusi dettagli sensibili come i ruoli utente, tra gli altri.

Per essere chiaro, la mia intenzione non è criticare o denigrare altri approcci. Tuttavia, basandomi sulla mia posizione professionale sulla sicurezza, incorporare informazioni così critiche direttamente nei token solleva preoccupazioni. C’è un rischio intrinseco associato all’esposizione di più dati del necessario, specialmente quando quei dati potrebbero potenzialmente essere sfruttati per dedurre privilegi utente o altre funzionalità.

Date queste riserve, ho preso la decisione consapevole di allontanarmi da questo metodo. Invece, mi sono orientato verso la collaudata autenticazione lato server combinata con la gestione sicura dei cookie. Questo approccio, a mio parere, trova un equilibrio tra esperienza utente e sicurezza robusta, assicurando che i dati sensibili rimangano protetti e il sistema rimanga resiliente contro potenziali vulnerabilità.

Invio affidabile delle email

Ho optato per SendGrid con template dinamici data la sua ampia adozione e la mia profonda familiarità con le sue sfumature. Per approfondire, consulta la loro documentazione API .

Sebbene la sua integrazione possa presentare alcune complessità, fa tutto parte della sfida gratificante. Ecco un esempio per te:

 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 {
 5to: string;
 6from: string;
 7subject: string;
 8content: string;
 9}
10
11async function sendEmail(details: EmailDetails): Promise<void> {
12const data = {
13personalizations: [{
14to: [{
15email: details.to
16}]
17}],
18from: {
19email: details.from
20},
21subject: details.subject,
22content: [{
23type: 'text/plain',
24value: 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
50// Usage example:
51sendEmail({
52to: '[email protected]',
53from: '[email protected]',
54subject: 'Hello from SendGrid!',
55content: 'This is a test email sent using the SendGrid API.'
56});

Come aumentare ulteriormente la sicurezza?

Anche se Cloudflare Workers KV crittografa i dati archiviati, puoi aggiungere un ulteriore livello di crittografia per i dati degli utenti.

O vuoi aggiungere un colpo di scena per i potenziali aggressori? Considera l’implementazione di input di form fittizi per confondere bot e hacker allo stesso modo:

 1return new HTMLRewriter()
 2.on("form",
 3{
 4element(form)
 5{
 6// Dummy inputs... just to give the hacker more headache and confuse him :)
 7const 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
19.transform(response);

Questo snippet genera dinamicamente da 1 a 5 input con nomi casuali ad ogni caricamento della pagina.

Le possibilità sono quasi illimitate, devi solo usare la tua immaginazione.

Conclusione

Ed eccoci qui! Spero sinceramente che questo articolo sia stato per te sia illuminante che coinvolgente. Che tu sia uno sviluppatore esperto o che tu stia appena immergendoti nel mondo dei siti web cloud scalabili, condividere la conoscenza è vitale per la crescita della nostra comunità.

Se hai trovato questo articolo prezioso, considera di condividerlo con colleghi sviluppatori o con chi è interessato all’argomento. Ogni condivisione amplia la nostra comprensione collettiva.

Il tuo feedback e le tue domande sono immensamente apprezzati. Non esitare a lasciare un commento o a contattarmi – manteniamo viva la conversazione. Buon coding sicuro!