Ricerca nel sito web

Come gestire le password in modo sicuro con BcryptsJS in JavaScript


L'autore ha selezionato la OWASP Foundation per ricevere una donazione come parte del programma Write for DOnations.

introduzione

Proteggere le password dei siti Web è un'abilità essenziale che ogni sviluppatore dovrebbe avere. JavaScript fornisce un'opzione per garantire l'archiviazione e l'elaborazione sicura di password o altri dati sensibili utilizzando gli algoritmi di hashing forniti dal modulo BcryptJS di JavaScript.

In questo tutorial imparerai a conoscere BcryptJS e l'hashing per configurare un server espresso di base che memorizzerà le password come hash in un database invece che come stringhe grezze e le recupererà per autenticare la password.

Prerequisiti

Per continuare con questo tutorial, dovresti avere la seguente configurazione.

  1. Una versione stabile di Node.js installata sul tuo computer con la versione 12.x o successiva. Puoi utilizzare questo tutorial di DigitalOcean per installare l'ultima versione di Node Js sul tuo computer.

  2. Dovresti sapere come programmare in JavaScript.

  3. _Dovresti avere Express JS installato sul tuo computer. Puoi utilizzare questa guida per imparare come configurare un server Express.

  4. Infine, per completare questo tutorial avrai bisogno della community MongoDB o del database Atlas. Puoi installarlo utilizzando una di queste guide DigitalOcean su_ come installare MongoDB.

Perché usare BcryptJS?

Bcrypt è un algoritmo di hashing per creare hash per le password per archiviarle in caso di violazione dei dati. Questo algoritmo di hashing avanzato utilizza i sali, rendendolo difficile da decifrare con attacchi come la forzatura bruta.

BcryptJS è l'implementazione JavaScript dell'algoritmo di hashing Bcrypt, che ti consente di utilizzare la crittografia hash senza dover immischiarti con funzioni di hashing complesse. Alcuni dei motivi che rendono BcryptJS un'ottima scelta per la sicurezza delle password sono i seguenti:

  1. Sicurezza – BcryptJS implementa l'algoritmo Bcrypt, un algoritmo lento (che nell'hashing è una buona cosa) e richiede un'intensa potenza di calcolo. Ciò rende un compito rigoroso per gli aggressori violare l'hash della password, garantendo la sicurezza delle password anche in caso di violazione dei dati._

  2. Salting – BcryptJS gestisce la generazione di salt casuali per le password per garantire la sicurezza dell'archiviazione (impareremo informazioni su hash e salt nella sezione successiva in modo più dettagliato). I sali rendono più complesso l'hash di una password relativamente debole, rendendola più difficile da decrittografare.

  3. Facilità d'uso – BcryptJS fornisce agli sviluppatori JavaScript uno strumento per crittografare le proprie password senza richiedere una conoscenza approfondita dell'hashing._

Nel passaggio successivo impareremo brevemente gli hash e i sal.

Come funziona l'Hashing?

Prima di utilizzare questi concetti nei tuoi progetti, devi capire come funzionano l'hashing e il salting.

Hashing

L'hashing converte una stringa semplice o un testo in chiaro in una stringa di caratteri casuali (crittografia). Ciò consente l'archiviazione e/o la trasmissione sicura di dati sensibili. L'hashing prevede i seguenti passaggi chiave:

Inserimento dati: in primo luogo, vengono archiviati i dati che potrebbero essere di qualsiasi tipo (binario, carattere, decimale, ecc.) come testo semplice o stringa.

La funzione hash – La funzione hash è un algoritmo matematico che prende input dai dati e li converte in un insieme di caratteri o codici hash. Le funzioni hash sono deterministiche (producono lo stesso output per lo stesso input) e funzioni unidirezionali (ciò significa che è quasi impossibile decodificare l'output delle funzioni hash, ovvero un hash nei suoi dati di input).

Resistenza alle collisioni – Ciò significa che una funzione hash viene creata tenendo presente l'idea di resistenza alle collisioni, ovvero due input diversi non possono avere lo stesso output (codice hash).

Autenticazione: le funzioni hash sono deterministiche e producono lo stesso hash per lo stesso input. Pertanto, quando si autentica una password archiviata come hash, l'idea generale è che se la password da autenticare corrisponde all'hash archiviato nel database, la password è corretta.

Salatura

Poiché l'hashing esiste da decenni, ci sono stati sviluppi come le tabelle arcobaleno, che contengono miliardi di voci di dati contenenti stringhe di dati e i rispettivi hash basati su diversi algoritmi di hashing.

Consideriamo ora una situazione in cui un utente che crea un account sul tuo sito web crea una password debole. Pertanto, in caso di violazione dei dati, un utente malintenzionato può cercare gli hash dei tuoi utenti e trovare la corrispondenza dell'hash per l'account utente con una password debole. Ciò si rivelerebbe disastroso nelle applicazioni ad alta sicurezza. Per evitare che ciò accada si utilizzano i sali.

Il salting è un ulteriore livello di sicurezza aggiunto agli hash aggiungendo una stringa casuale di caratteri all'hash di una password prima di archiviarla in un database. Pertanto, anche se i dati vengono divulgati durante una violazione, sarà difficile per un utente malintenzionato decrittografare un hash contenente salt. Considera il seguente esempio:

Password = ‘sammy’
Hash = £%$^&£!23!3%!!
Salt = 2vqw£4Df$%sdfk
Hash + Salt = £%$^&£!23!3%!!2vqw£4Df$%sdfk

Come possiamo vedere chiaramente, è più improbabile che la password memorizzata con salt venga violata a causa della natura deterministica degli hash. Pertanto, se un utente malintenzionato, ad esempio, cerca questa stringa hash+password in una tabella arcobaleno, non otterrà la stringa della password effettiva ma qualcosa di completamente diverso.

Ora sei pronto per utilizzare BcryptJS e proteggere le tue password in un modo standard del settore.

Installazione di BcryptJS e altri moduli richiesti

Ora che conosci l'hashing e il salting, non ti resta che prendere il computer e iniziare a programmare. La struttura del progetto sarà la seguente:

Innanzitutto, inizieremo creando un progetto npm con i seguenti passaggi:

  1. Apri una cartella e crea un file, app.js.

  2. Apri una finestra di terminale in questa cartella e digita il comando

npm init

Successivamente, ti verranno richiesti degli input ma puoi premere Invio senza fornire alcun input.

  1. Quindi, crea altri 3 file, vale a dire
    auth.js
    db.js
    User.js
  1. Nella stessa finestra del terminale, digita il seguente comando per installare i pacchetti necessari.
npm install express mongoose BcryptJS nodemon

Ora disponi di una configurazione completa dell'ambiente di progetto da seguire in questo tutorial. Nel passaggio successivo imparerai come creare un server per utilizzare BcryptJS per archiviare e autenticare in modo sicuro le password con MongoDB.

Configurazione di un server con Express JS

Ora che hai impostato la struttura del progetto, puoi creare un server che utilizzi bcrytjs per proteggere le password memorizzandole come hash e autenticandole. Creeremo il server con i seguenti passaggi corretti.

Passaggio 1: creazione di una connessione al database MongoDB

Per connetterci a mongoDB, utilizziamo l'edizione community. Per mantenere il progetto organizzato, salverai il codice per impostare una connessione nel file db.js.

const mongoose = require("mongoose");
const mongoURI = "mongodb://127.0.0.1:27017/bcrypt_database";
const connectMongo = async () => {

  try {
    await mongoose.connect(mongoURI);
    console.log("Connected to MongoDB!");
  } catch (error) {
    console.error("Error connecting to MongoDB: ", error.message);
  }
};

module.exports = connectMongo;

Qui importi il pacchetto mongoose, che fornisce un'API per connettere javascript a MongoDB. Inoltre, in questo caso l'URI di connessione è per un'installazione locale di mongoDB, se tu stai utilizzando un database cloud (come Atlas), devi solo modificare l'URI nell'URI del tuo database specifico.

Informazioni Un URI è simile all'URL di un server con la differenza che un URI può identificare il nome e l'identità delle risorse e la loro posizione su Internet. Al contrario, un URL è un sottoinsieme di un URI capace di fare solo quest'ultima cosa.

La funzione connectToMongo è una funzione asincrona perché mongoose.connect restituisce una promessa javascript. Questa funzione fornirà una connessione all'esecuzione riuscita. Altrimenti, restituirà un errore.

Infine, utilizziamo module.exports per esportare questa funzione ogni volta che viene importato il modulo db.js.

Passaggio 2: creazione di uno schema utente

Avrai bisogno di uno schema di base per creare o autenticare gli utenti con un database MongoDB. Se non sai cos'è uno schema, puoi utilizzare questa eccellente guida DO per comprendere e creare lo schema in MongoDB. Utilizzeremo solo due campi per il nostro schema, email e password.

Utilizza il seguente codice nel tuo modulo User.js.

const mongoose  = require("mongoose");
const UserSchema = new mongoose.Schema({
    email:{
        type:String,
        required:true,
        unique:true
    },
    password:{
        type:String,
        required:true
    }
});
module.exports = mongoose.model('user', UserSchema)

Creiamo due campi in questo schema, email e password. Entrambi sono campi obbligatori e di tipo dati stringa. Inoltre, email è un campo univoco, il che significa che un'e-mail può essere utilizzata solo una volta per avere un account su questo server.

Infine, esportiamo un modello denominato users. Un modello rappresenta uno schema alla fine del database delle cose. Puoi pensare a uno schema come una regola per definire un modello, mentre il modello viene archiviato come una raccolta nel database MongoDB.

Puoi convertire uno schema in un modello utilizzando la funzione model() della libreria mongoose.

Passaggio 3: configurazione del server in app.js

Dopo aver seguito i passaggi precedenti, hai creato con successo un modello e un modulo per stabilire una connessione al database MongoDB. Ora imparerai a configurare il server. Utilizza il codice seguente nel file app.js.

const connectToMongo = require("./db");
const express = require("express");
const app = express();
connectToMongo();
app.use(express.json());
app.use("/auth", require("./auth"));
const port = 3300;
app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}`);
});

Qui importiamo i moduli express e db.js per connetterci a MongoDB. Quindi, utilizziamo il middleware express.json() per gestire le risposte JSON. I percorsi vengono creati in un modulo diverso (auth.js) per mantenere il codice pulito e organizzato. Quindi, infine, creiamo un endpoint per il server in ascolto sulla porta 3300 su localhost. (Puoi utilizzare qualsiasi porta di tua scelta)

Crittografare le password e memorizzarle in un database MongoDB

Fino a questo punto, il server è quasi pronto e ora creerai gli endpoint per il server. Creeremo due endpoint: signup e login. Nell'endpoint di registrazione, prenderemo l'e-mail e la password di un nuovo utente e le archivieremo con crittografia per una password utilizzando BcryptJS.

Nel file auth.js, digita il seguente codice:

const express = require("express");
const router = express.Router();
const User = require("./User");
const bcrypt = require("bcryptjs");

// ROUTE 1:
router.post("/signup", async (req, res) => {
  const salt = await bcrypt.genSalt(10);
  const secPass = await bcrypt.hash(req.body.password, salt);
  let user = await User.create({
    email: req.body.email,
    password: secPass,
  });
  res.json({ user });
});
module.exports = router;

Effettuiamo le importazioni necessarie e quindi configuriamo il router express per creare l'endpoint /signup. Utilizziamo il metodo POST in modo che le credenziali non vengano divulgate nell'URL dell'applicazione. Successivamente creiamo un Salt utilizzando la funzione genSalt del pacchetto scripts; il parametro passato alle funzioni genSalt() contiene la lunghezza del carattere salt. Quindi, utilizziamo la funzione hash() di BcryptJS, che accetta un parametro obbligatorio, la stringa della password da convertire in codice hash e un argomento opzionale, la stringa salt. Quindi restituisce un hash che contiene sia password che salt.

Successivamente, utilizziamo la funzione create() del modulo mongoose per creare un documento nel nostro database definito dalle regole del modello users. Richiede un oggetto Javascript contenente email e password, ma invece di fornirgli una stringa grezza, diamo secPass (password hash + salt) da memorizzare nel database. In questo modo, abbiamo archiviato in modo sicuro una password nel database utilizzando il suo hash combinato con un salt anziché con una stringa grezza. Alla fine, restituiamo una risposta JSON contenente il modello utente. (Questo metodo di invio delle risposte è solo per la fase di sviluppo; in produzione, lo sostituirai con un token di autenticazione o qualcos'altro).

Per testare questo endpoint, è necessario prima eseguire il server, operazione che può essere eseguita digitando il seguente comando nel terminale.

cd <path to your project folder>
nodemon ./app.js

Questo comando eseguirà il tuo server su localhost e porta 3300 (o qualunque porta specificata). Quindi, puoi inviare una richiesta HTTP all'URL http://localhost:3300/auth/signup con il seguente corpo:

{
  "email":"sammy@linux-console.net",
  "password":"sammy"
}

Ciò produrrà il seguente output/risposta:

{
  "user": {
    "email": "sammy@linux-console.net",
    "password": "$2a$10$JBka/WyJD0ohkzyu5Wu.JeCqQm33UIx/1xqIeNJ1AQI9kYZ0Gr0IS",
    "_id": "654510cd8f1edaa59a8bb589",
    "__v": 0
  }
}

Nota: l'hash e l'ID della password non saranno gli stessi, poiché sono sempre univoci.

Nella sezione seguente imparerai come accedere all'hash memorizzato per la password e autenticarlo con una password fornita al momento dell'accesso/autenticazione.

Accedere alla password crittografata e utilizzarla per l'autenticazione

Finora hai imparato a conoscere BcryptJS, hashing, salting e lo sviluppo di un server express che crea nuovi utenti con password memorizzate come hash. Ora imparerai come utilizzare la password memorizzata e autenticare un utente quando tenta di accedere all'applicazione.

Per aggiungere un metodo di autenticazione, aggiungi il seguente percorso nel tuo auth.js dopo il percorso /signup:

// ROUTE 2:
router.post("/login", async (req, res) => {
  let user = await User.findOne({ email: req.body.email });
  if (!user) {
    return res.status(400).json({ error: "Login with proper credentials!" });
  }
  
  const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  if (!passwordCompare) {
    return res
      .status(400)
      .json({ error: "Login with proper credentials!" });
  }
  
  res.json({ success: "Authenticated!" });
});

Qui utilizziamo il sottopercorso /auth/login per eseguire un'autenticazione di accesso per un utente già esistente. Simile all'endpoint /auth/signup, questa sarà una funzione di attesa asincrona poiché BcryptJS restituisce le promesse.

Innanzitutto, utilizziamo la funzione findOne della libreria Mongoose, che viene utilizzata per trovare un documento in una raccolta in base a una determinata query di ricerca. In questo caso, cerchiamo l'utente in base all'e-mail. Se non esiste alcun utente con l'e-mail fornita, verrà inviata una risposta con codice di stato 400 per credenziali non valide. (Non è una buona pratica fornire quale parametro non è corretto durante l'accesso, poiché gli aggressori possono utilizzare tali informazioni per trovare account esistenti).

Se l'utente con l'e-mail fornita esiste, il programma passa al confronto delle password. A questo scopo, BcryptJS fornisce il metodo compare(), che accetta una stringa grezza come primo argomento e un hash (con o senza salt) per il secondo argomento. Quindi restituisce una promessa booleana; true se la password corrisponde all'hash e false in caso contrario. Quindi, puoi aggiungere un semplice controllo utilizzando un'istruzione if e restituire un successo o un errore in base al confronto.

Infine, esporterai il express router utilizzando module.exports per il punto di partenza app.js per utilizzarlo per i percorsi.

Per testare questo percorso, puoi inviare un'altra risposta HTTP a questo URL http://localhost:3300/auth/login con il corpo come segue:

{
  "email":"sammy@linux-console.net",
  "password":"sammy"
}

Questa richiesta darà la seguente risposta:

{
  "success": "Authenticated!"
}

Alla fine, auth.js sarà simile a:

const express = require("express");
const router = express.Router();
const User = require("./User");
const bcrypt = require("bcryptjs");

// ROUTE 1:
router.post("/signup", async (req, res) => {
  const salt = await bcrypt.genSalt(10);
  const secPass = await bcrypt.hash(req.body.password, salt);
  let user = await User.create({
    email: req.body.email,
    password: secPass,
  });
  res.json({ user });
});

// ROUTE 2:
router.post("/login", async (req, res) => {
  let user = await User.findOne({ email: req.body.email });
  if (!user) {
    return res.status(400).json({ error: "Login with proper credentials!" });
  }
  const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  if (!passwordCompare) {
    return res
      .status(400)
      .json({ error: "Login with proper credentials!" });
  }
  res.json({ success: "Authenticated!" });
});
module.exports = router;

Conclusione

Questo tutorial ha creato un server per spiegare l'utilizzo di BcryptJS per archiviare e accedere in modo sicuro alle password del database. Puoi approfondire l'argomento:

  • Implementazione di più percorsi per altre attività come il recupero delle credenziali dell'utente, l'aggiornamento delle credenziali dell'utente, ecc.

  • Implementazione di token di autenticazione da inviare come risposte, ecc.

Questo inizia il tuo viaggio per gestire le password in modo sicuro; puoi sempre aggiungere maggiore sicurezza con diverse tecniche e tecnologie, consentendoti di creare applicazioni più sicure e resilienti.

Articoli correlati: