Come possiamo implementare un sistema di autenticazione e autorizzazione nel nostro applicativo React in modo semplice, sicuro e veloce?
Esistono molte soluzioni per implementare un sistema di login, ma in questo tutorial ci concentreremo su Google Firebase. Questa piattaforma di Google offre una serie di strumenti (con un free molto generoso), tra cui analisi, database noSQL, messaggistica e, soprattutto, un sistema per gestire l'autenticazione e l'autorizzazione in modo semplice e sicuro all'interno del nostro applicativo.
Ma prima di cominciare con il tutorial, vediamo cosa saremo in grado di fare al termine di questa guida: realizzare un'applicazione con React, che utilizza un sistema di routing gestito attraverso React Router 6. Nell'applicazione ci saranno delle rotte protette gestite da Google Firebase Authentication. Le informazioni dell'utente saranno salvate all'interno di un context.
Ecco la lista degli ingredienti:
Creazione e setup del progetto
Come prima cosa, creiamo il nostro progetto React utilizzando Vite, quindi dal terminale lanciamo questo comando:
npm create vite@latest
Con questo comando stiamo dicendo al nostro terminale di contattare il programma NPM e di ordinargli di eseguire il comando create di un progetto Vite. @latest ci serve per specificare quale versione di Vite vogliamo usare per creare il nostro progetto e nel nostro caso sarà quella più recente.
Nel terminale si aprirà una finestra con una serie di step da seguire. Alla fine, verrà creata la cartella del progetto. Entriamo nella cartella cd react-firebase-auth
ed eseguiamo npm install
per installare tutte le dipendenze.
Ci siamo, il nostro progetto è pronto, ora non ci resta che lanciare il comando npm run dev
Questo è ciò che ci troviamo a schermo una volta lanciato il nostro applicativo:
Ok, caruccio, ma non è ciò che vogliamo, quindi ora cancelliamo tutto il codice che non ci interessa, così da partire da una pagina bianca e installeremo Tailwind così da semplificarci un pochino la vita, almeno in questo tutorial. (non che io non ami i CSS nudi e crudi, ma adesso non me và).
Come prima cosa, cancelliamo tutto il codice che non ci interessa all’interno di App.jsx
, fino a ottenere questo codice:
// App.jsx
function App() {
return (
<>
<h1>Hello, World!</h1>
</>
);
}
export default App;
Ora è giunto il momento di eliminare il file App.css
che non utilizzeremo più, in quanto useremo le utility class di Tailwind direttamente nei file jsx. Cancelliamo anche tutto il contenuto di index.css
, dove tra poco dovremo inserire del codice che ci fornirà Tailwind stesso.
Installazione di Tailwind al progetto
Ora che il nostro progetto è una vera e propria tela bianca, a parte l’hello, world che non può mai mancare, possiamo aggiungere Tailwind al nostro progetto; ci aiuterà con lo stile dei componenti, anche se non ne creeremo molti.
👉 Digitare nel terminale i seguenti comandi che serviranno per installare Tailwind, le sue dipendenze e generare i file di configurazione tailwind.config.js
e postcss.config.js
:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Qui è possibile vedere i due file di configurazione generati dal secondo comando:
👉 Aggiungiamo questo codice all’interno del file tailwind.config.js
:
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html", // <== New Line
"./src/**/*.{js,ts,jsx,tsx}", // <== New Line
],
theme: {
extend: {},
},
plugins: [],
};
👉 Infine, ultimo passo per completare il setup di Tailwind, aggiungiamo anche queste tre righe di codice nel file index.css
:
/* index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Anche è impostato. Per verificare che funzioni correttamente, aggiungiamo delle utility class nell’h1
, all’interno del file App.jsx
e vediamo che succede:
// App.jsx
function App() {
return (
<>
<h1 className="bg-red-500">Hello, World!</h1> {/* New Line */}
</>
);
}
export default App;
Aggiungendo un bg-red-500
all’h1 il risultato dovrebbe essere il seguente. Se anche tu vedi il mio stesso risultato, allora hai impostato tutto correttamente.
Attenzione: potresti aver bisogno di interrompere il server con un
ctrl + c
ed eseguire nuovamente unnpm run dev
.
Pensavi che fosse finita qui?
E invece no, ora installiamo e impostiamo React Router (ma chi me l’ha fatto fa?!).
Installiazione e setup di React Router
Per gestire la navigazione all’interno dell’applicazione avremo bisogno di un ulteriore set di funzionalità che ci viene fornito attraverso la libreria React Router.
👉 Lanciare questo comando da terminale per installare React Router (RR):
npm install react-router-dom
Una volta installato React Router, apriamo il file App.jsx
e configuriamo il router che gestirà la navigazione all'interno della nostra applicazione.
Per creare il router, utilizziamo la funzione createBrowserRouter
fornita dalla libreria React Router. Questa funzione accetta un array di rotte, ciascuna rappresentata come un oggetto. La rotta principale restituirà l’elemento h1 che avevamo precedentemente all’interno di <App />
Per distribuire le regole del router in tutta l'applicazione, utilizziamo il componente RouterProvider
. Questo componente viene restituito dalla funzione App
, permettendo la gestione della navigazione a livello globale. Il componente RouterProvider ora contiene tutto il nostro applicativo.
Ecco come appare il codice aggiornato:
// App.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom"; // <== New Line
const router = createBrowserRouter([
// <== New Line
{
// <== New Line
path: "/", // <== New Line
element: <h1 className="bg-red-500">Hello, World!</h1>, // <== New Line
}, // <== New Line
]); // <== New Line
function App() {
return <RouterProvider router={router} />; // <== New Line
}
export default App;
Per una maggiore pulizia e organizzazione del codice, creiamo una pagina/componente Home e facciamogli restituire <h1 className="bg-red-500">Hello, Home!</h1>
. Poi importiamo il componente <Home />
, che abbiamo creato, in App.jxs
e lo associamo al path: “/”
.
👉 In src
creiamo una cartella pages e al suo interno creiamo il file Home.jsx
:
// src/pages/Home.jsx
function Home() {
return <h1 className="bg-red-500">Hello, World!</h1>;
}
export default Home;
Importiamo il componente <Home />
in App.jsx
// App.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Home from "./pages/Home";
const router = createBrowserRouter([
{
path: "/",
element: <Home />, // <== New Line
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
Ci siamo, ora il setup del nostro progetto è completo e il nostro applicativo, ogni volta che sarà sulla rotta principale, http://localhost:5173/
ci restituirà il componente <Home />
.
Ora che il setup del nostro applicativo è pronto, possiamo dedicarci a processo di autenticazione e autorizzazione attraverso Google Firebase.
Setup di Google Firebase
Come prima cosa, dobbiamo creare il nostro primo progetto in nella piattaforma Firebase. Per farlo, da browser, rechiamoci a questo url https://console.firebase.google.com/. Si aprirà questa finestra:
Clicchiamo su Inizia con un nuovo progetto Firebase e seguiamo i seguenti passaggi:
In questo modo abbiamo creato il nostro primo progetto Firebase.
Ora non ci resta che assegnare al nostro progetto Firebase una web app, come nel video qui di seguito:
Ora che abbiamo creato il nostro primo progetto Firebase e registrato la nostra webapp, dobbiamo eseguire il comando npm install firebase
dal nostro applicativo per installare l’SDK (Software Development Kit). Un SDK è un insieme di strumenti, librerie, documentazione e campioni di codice che permette agli sviluppatori di utilizzare tutte le funzionalità offerte da Firebase all’interno del loro applicativo.
Non ci resta configurare Firebase all’interno del nostro codice. Per farlo, nella root principale del nostro progetto react, creiamo un file chiamato firebaseconfig.js
, dove inseriremo i dati di configurazione del nostro progetto Firebase. Il codice da copiare e incollare nel firebaseconfig.js
è quello che ci viene restituito nel browser una vota registrata la nostra webapp. In questa schermata:
Mi raccomando, non lasciamo API Key in chiaro nel nostro applicativo. Tutti i dati sensibili li inseriamo in un file
.env
// firebaseconfig.js
export const firebaseConfig = {
apiKey: "AIzaSyBcB5EMUuKRwYk9r17DQ27SqtxTbmEnx4c", // <== Questo è un dato sensibile da portare nel .env
authDomain: "react-firebase-63758.firebaseapp.com",
projectId: "react-firebase-63758",
storageBucket: "react-firebase-63758.appspot.com",
messagingSenderId: "217149329938",
appId: "1:217149329938:web:c93d54793bf0bc6b143ecb",
measurementId: "G-56TENJR1XJ",
};
Ecco come dovrebbe essere il nostro codice nella maniera corretta:
// firebaseconfig.js
export const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY, // <== New Line
authDomain: "react-firebase-63758.firebaseapp.com",
projectId: "react-firebase-63758",
storageBucket: "react-firebase-63758.appspot.com",
messagingSenderId: "217149329938",
appId: "1:217149329938:web:c93d54793bf0bc6b143ecb",
measurementId: "G-56TENJR1XJ",
};
Infine, nel main.jsx
creiamo un’istanza di Firebase:
// src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import { initializeApp } from "firebase/app"; // New Line
import { firebaseConfig } from "../firebaseconfig.js"; // New Line
import "./index.css";
// Initialize Firebase
initializeApp(firebaseConfig); // New Line
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Ci siamo, ora possiamo concentrarci definitamente sul nostro codice.
Era ora direi 😪
Signup e signin
È il momento di dedicarsi alle pagine di Signin e Signup, ossia accedere a un account esistente e creare un nuovo account.
Nella cartella pages creiamo i file SignIn.jsx e SignUp.jsx. Al loro interno inseriamo i seguenti codici:
// src/pages/SignIn.jsx
function SignIn() {
return <div>SignIn page</div>;
}
export default SignIn;
// src/pages/SignUp.jsx
function SignUp() {
return <div>SignUp page</div>;
}
export default SignUp;
All’interno del file App.jsx
aggiorniamo il router implementando due nuove rotte, una per la pagina di signin e un’altra per quella di signup:
// src/App.jsx
/*
*
* Codice precedente
*/
const router = createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "signin", // New Line
element: <SignIn />, // New Line
},
{
path: "signup", // New Line
element: <SignUp />, // New Line
},
]);
/*
*
* Codice successivo
*/
Se proviamo a navigare su queste due rotte, per testare il funzionamento del nostro router, ci troveremo di fronte a questa situazione:
Implementazione del sign-up
Una volta create le pagine di Sign-in e Sign-up e aver istruito il router a restituire questi componenti (pagine) ogni volta che atterriamo sulle specifiche rotte, è il momento di dedicarsi al contenuto vero e proprio delle pagine di sign-up e sign-in.
Incominceremo dalla pagina di Sing-up che deve contenere una form con tre valori di input:
- email dell’utente;
- password dell’utente;
- password di conferma dell’utente.
Se la password del punto due non sarà identica alla password del punto tre, non sarà possibile confermare la form per in i dati di registrazione dell’utente.
Seguendo le nostre indicazioni e necessità, il codice della pagina sign-up diventerà così:
// src/pages/SignUp.jsx
import { useState } from "react";
function SignUp() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPass, setConfirmPass] = useState("");
// validazione input della form
const emailValidation = !email.length;
const passwordValidation = !password.length;
const confirmPasswordValidation = !confirmPass.length;
const arePasswordAncConfirmPassEqual = password !== confirmPass;
// validazione della form
const formValidation =
emailValidation ||
passwordValidation ||
confirmPasswordValidation ||
arePasswordAncConfirmPassEqual;
// istruzioni da eseguire al submit della form
const handleSubmit = (e) => {
e.preventDefault();
};
return (
<div className="flex flex-col p-2">
<h1>SignUp page</h1>
<form className="w-[400px] flex flex-col gap-2" onSubmit={handleSubmit}>
<input
className="border-2 border-slate-400 p-2"
placeholder="Email"
onChange={(e)=> setEmail(e.target.value)}
/>
<input
className="border-2 border-slate-400 p-2"
placeholder="Password"
onChange={(e)=> setPassword(e.target.value)}
/>
<input
className="border-2 border-slate-400 p-2"
placeholder="Confirm Password"
onChange={(e)=> setConfirmPass(e.target.value)}
/>
<button
className={`bg-green-700 ${formValidation ? "bg-green-200" : ""} p-2`}
disabled={formValidation}
type="submit"
>
Sing-up
</button>
</form>
</div>
);
}
export default SignUp;
Al momento del submit, come è possibile vedere dalla funzione di handleSubmit
, il codice non esegue nessuna istruzione a parte un preventDefault
.
Ecco comunque il risultato finale:
È il momento di utilizzare Firebase e le sue funzionalità per gestire la registrazione degli utenti (sign-up).
In questo caso, importeremo le funzioni getAuth
e createUserWithEmailAndPassword
nel nostro file SignUp.jsx
. Come possiamo vedere, Google Firebase offre funzioni che fanno al caso nostro proprio per questa specifica situazione e non ci sarà bisogno di reinventare la ruota.
Implementiamo il codice della pagina di SignUp in questa maniera:
// src/pages/SignUp.jsx
import { useState } from "react";
import { getAuth, createUserWithEmailAndPassword } from "firebase/auth"; // New Line
function SignUp() {
const auth = getAuth(); // New Line
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPass, setConfirmPass] = useState("");
const emailValidation = !email.length;
const passwordValidation = !password.length;
const confirmPasswordValidation = !confirmPass.length;
const arePasswordAncConfirmPassEqual = password !== confirmPass;
const formValidation =
emailValidation ||
passwordValidation ||
confirmPasswordValidation ||
arePasswordAncConfirmPassEqual;
const handleSubmit = (e) => {
e.preventDefault();
createUserWithEmailAndPassword(auth, email, password) // New Line
.then((user) => console.log(user)) // New Line
.catch((error) => console.log(error)); // New Line
};
return (
<div className="flex flex-col p-2">
<h1>SignUp page</h1>
<form className="w-[400px] flex flex-col gap-2" onSubmit={handleSubmit}>
<input
className="border-2 border-slate-400 p-2"
placeholder="Email"
onChange={(e)=> setEmail(e.target.value)}
/>
<input
className="border-2 border-slate-400 p-2"
placeholder="Password"
onChange={(e)=> setPassword(e.target.value)}
/>
<input
className="border-2 border-slate-400 p-2"
placeholder="Confirm Password"
onChange={(e)=> setConfirmPass(e.target.value)}
/>
<button
className={`bg-green-700 ${formValidation ? "bg-green-200" : ""} p-2`}
disabled={formValidation}
type="submit"
>
Sing-up
</button>
</form>
</div>
);
}
export default SignUp;
Prima di testare il codice dobbiamo fare un’ultima cosa, ossia attivare il servizio di autenticazione in Firebase e attivare l’email provider:
Una attivato il provider di autenticazione di Firebase, possiamo provare la prima registrazione di un utente:
Implementazione del sing-in
Ora che il modulo di registrazione funziona egregiamente, è arrivato il momento di procedere con l’implementazione del sistema di singin. Siccome il modulo di signin non è tanto diverso da quello di signup, se non per alcuni dettagli, faremo ciò per cui noi sviluppatori siamo famosi in tutto il mondo: copia e incolla.
👉 Nel file SignIn.jsx implementiamo questo codice:
// src/pages/SignIn.jsx
import { useState } from "react";
import { getAuth, signInWithEmailAndPassword } from "firebase/auth"; // New Line
import { useNavigate } from "react-router-dom"; // New Line
function SignIn() {
const auth = getAuth();
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const emailValidation = !email.length;
const passwordValidation = !password.length;
const formValidation = emailValidation || passwordValidation;
const handleSubmit = (e) => {
e.preventDefault();
/*
*
* Utilizzo la funzione signInWithEmailAndPassword
* invece di createUserWithEmailAndPassword
*/
signInWithEmailAndPassword(auth, email, password) // New Line
.then((user) => {
console.log(user);
/*
*
* Se il signin va a buon fine
* vengo indirizzato alla pagina home
*/
navigate("/"); // New Line
})
.catch((error) => console.log(error));
};
return (
<div className="flex flex-col p-2">
<h1>SignIn page</h1>
<form className="w-[400px] flex flex-col gap-2" onSubmit={handleSubmit}>
<input
className="border-2 border-slate-400 p-2"
placeholder="Email"
onChange={(e)=> setEmail(e.target.value)}
/>
<input
className="border-2 border-slate-400 p-2"
placeholder="Password"
onChange={(e)=> setPassword(e.target.value)}
/>
<button
className={`bg-green-700 ${formValidation ? "bg-green-200" : ""} p-2`}
disabled={formValidation}
type="submit"
>
Sing-up
</button>
</form>
</div>
);
}
export default SignIn;
Questa volta, essendo una pagina di signin, non ho bisogno di tutti i controlli di validazione della form che utilizzavamo in precedenza, per controllare che le password inserite fossero identiche prima di creare il profilo.
Per fare il signin, Firebase ci mette a disposizione un’altra funzionalità, ossia il signInWithEmailAndPassword che utilizzeremo al posto di createUserWithEmailAndPassword.
Qui il codice in azione:
Una volta registrati e loggati all’interno di un’applicativo, non ci resta che implementare anche la funzionalità di Signout.
Implementiamo il signout
Il nostro obiettivo ora è implementare la funzionalità di signout nella pagina Home.
Per farlo, utilizzeremo le funzionalità di getAuth
e il signOut
che ci mette a disposizione Firebase direttamente dalla libreria che abbiamo utilizzato fino a questo momento.
👉 Implementiamo il codice del file Home.jsx come segue:
// src/pages/Home.jsx
import { signOut, getAuth } from "firebase/auth";
function Home() {
const auth = getAuth();
const handleClick = () => {
signOut(auth)
.then((res) => console.log(res))
.catch((error) => console.log(error));
};
return (
<div className="flex flex-col w-[300px]">
<h1>Hello, World!</h1>
<p>
Questa è la pagina home, volendo puoi anche fare un Signout premendo su
questo bottone 👇👇👇{" "}
</p>
<button onClick={handleClick} className="bg-red-500 p-2">
Sign Out
</button>
</div>
);
}
export default Home;
In questo caso, il processo di signout è piuttosto semplice. Per verificare il corretto funzionamento del signout, configureremo React Router in modo che alcune rotte dell'applicazione siano protette e accessibili solo agli utenti autenticati.
Dopo aver effettuato il signout, l'utente verrà reindirizzato a pagine di accesso pubblico, alle quali non si potrà accedere senza essere registrati e loggati.
Proteggere le rotte con un context
All'interno della cartella src
, creiamo una nuova cartella chiamata context
, e al suo interno, un file chiamato AuthContext.jsx
.
Il file AuthContext.jsx
conterrà i context predisposti a mantenere nella memoria dell'applicazione i dati dell'utente autenticato. In questo file, importeremo useState
, createContext
e useEffect
dalla libreria di React, e le funzionalità getAuth
e onAuthStateChanged
da Firebase.
Quest'ultima funzione monitora le richieste di sign-in e sign-out inviate a Firebase e aggiorna lo stato dell'applicazione di conseguenza.
Implementiamo il seguente codice nel file AuthContext.jsx
:
// src/context/AuthContext.jsx
// Importiamo le funzioni e gli hook necessari da React e Firebase.
import { useState, useEffect, createContext } from "react";
import { getAuth, onAuthStateChanged } from "firebase/auth";
// Creiamo due contesti, uno per l'utente e uno per la funzione che permette di impostare l'utente.
export const UserContext = createContext();
export const SetUserContext = createContext();
// Definiamo il componente AuthContext che avvolgerà i componenti figli con i contesti definiti sopra.
function AuthContext({ children }) {
// Otteniamo l'oggetto auth dall'SDK di Firebase.
const auth = getAuth();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Configuriamo un listener per i cambiamenti dello stato di autenticazione.
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
// Una volta ottenuto il risultato, impostiamo isLoading a false.
setIsLoading(false);
// Se c'è un utente autenticato, aggiorniamo lo stato dell'utente, altrimenti lo impostiamo a null.
if (currentUser) {
setUser(currentUser);
} else {
setUser(null);
}
});
// Puliamo l'effetto restituendo una funzione che annulla l'iscrizione al listener quando il componente viene smontato.
return () => {
if (unsubscribe) unsubscribe();
};
}, [auth]);
// Restituiamo i componenti Provider per entrambi i contesti, avvolgendo i componenti figli passati come children.
// Passiamo il valore dell'utente e della funzione setUser rispettivamente ai contesti UserContext e SetUserContext.
// Visualizziamo i componenti figli solo se il caricamento è terminato.
return (
<SetUserContext.Provider value={{ setUser }}>
<UserContext.Provider value={{ user }}>
{!isLoading && children}
</UserContext.Provider>
</SetUserContext.Provider>
);
}
// Esportiamo il componente AuthContext per l'utilizzo in altre parti dell'applicazione.
export default AuthContext;
Un componente per monitorare le richieste di rotta dell’utente
Una volta implementato il nostro context, abbiamo bisogno di un componente che monitori le richieste di routing all'interno dell'applicazione e verifichi se l'utente è autenticato e ha i permessi necessari.
Se l'utente non è autenticato e tenta di accedere a una rotta protetta, verrà reindirizzato alla pagina di login. Se invece l'utente è autenticato, verrà indirizzato direttamente alla Home.
Nella cartella pages creiamo il file Protected.jsx
e implementiamoci questo codice:
// src/pages/Protected.jsx
import { Navigate } from "react-router-dom";
import { useContext } from "react";
import { UserContext } from "../context/AuthContext";
export function Protected({ children }) {
const { user } = useContext(UserContext);
if (!user) {
return <Navigate to="/signin" replace />;
} else {
return children;
}
}
Aggiorniamo il router
Aggiorniamo il componente App
importando AuthContext
e il componente Protected
. AuthContext
avvolgerà l'intera applicazione, distribuendo i dati del contesto a tutti i componenti. Protected
avvolgerà il componente <Home />
, gestendo le richieste degli utenti e verificando se sono autenticati.
// src/App.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Home from "./pages/Home";
import SignIn from "./pages/SignIn";
import SignUp from "./pages/SignUp";
import { Protected } from "./pages/Protected";
import AuthContext from "./context/AuthContext";
const router = createBrowserRouter([
{
path: "/",
element: (
<Protected>
<Home />
</Protected>
),
},
{
path: "signin",
element: <SignIn />,
},
{
path: "signup",
element: <SignUp />,
},
]);
function App() {
return (
<AuthContext>
<RouterProvider router={router} />
</AuthContext>
);
}
export default App;
Risultato finale
Et voilà! Funziona tutto perfettamente!
Chiaramente, questo è solo un piccolo esempio di ciò che possiamo fare con Google Firebase perché le funzionalità e i servizi che ci mette a disposizione questo strumento sono davvero tanti.