Como Lidar com Senhas com Segurança usando BcryptsJS em JavaScript

Introdução

Proteger senhas de websites é uma habilidade essencial que todo desenvolvedor deve ter. JavaScript oferece uma opção para garantir o armazenamento seguro e processamento de senhas ou outros dados sensíveis usando os algoritmos de hash fornecidos pelo módulo BcryptJS do JavaScript.

Neste tutorial, você aprenderá sobre BcryptJS e o processo de hash para configurar um servidor express básico que armazenará senhas como hashes em um banco de dados em vez de strings brutas e as recuperará para autenticar a senha.

Pré-requisitos

Para continuar com este tutorial, você deve ter a seguinte configuração.

  1. Uma versão estável do Node.js instalada em seu computador com a versão 12.x ou superior. Você pode seguir este tutorial da DigitalOcean para instalar a versão mais recente do Node.js em seu computador.

  2. Você deve saber como programar em JavaScript.

  3. _Você deve ter o Express JS instalado no seu computador. Você pode usar este guia para aprender como configurar um servidor Express.

  4. Por último, você precisará do banco de dados MongoDB Community ou Atlas para completar este tutorial. Você pode instalá-lo usando um desses guias da DigitalOcean sobre_ como instalar o MongoDB.

Por que usar o BcryptJS?

O Bcrypt é um algoritmo de hash usado para criar hashes para senhas e armazená-las em caso de violação de dados. Esse algoritmo de hash avançado utiliza salts, tornando difícil de quebrar por ataques como força bruta.

BcryptJS é a implementação em JavaScript do algoritmo de hash Bcrypt, permitindo o uso da criptografia de hash sem a necessidade de lidar com funções de hash complexas. Algumas das razões que tornam o BcryptJS uma ótima escolha para a segurança de senhas são as seguintes:

  1. Segurança – BcryptJS implementa o algoritmo Bcrypt, um algoritmo lento (o que é bom em hashing) e requer uma intensa potência computacional. Isso torna uma tarefa rigorosa para os atacantes que tentam quebrar o hash da senha, garantindo a segurança das senhas mesmo em caso de violação de dados.

  2. Salt – BcryptJS lida com a geração de salts aleatórios para senhas para garantir a segurança de armazenamento (Vamos aprender sobre hashes e salts na próxima seção com mais detalhes). Salts tornam o hash de uma senha relativamente fraca mais complexo, tornando mais difícil decriptá-lo.

  3. Facilidade de Uso – BcryptJS fornece aos desenvolvedores JavaScript uma ferramenta para criptografar suas senhas sem exigir um profundo entendimento de hashing.

Na próxima etapa, aprenderemos brevemente sobre hashes e salts.

Como funciona o Hashing?

Antes de usar esses conceitos em seus projetos, você deve entender como o hashing e o salting funcionam.

Hashing

O Hashing é a conversão de uma simples string ou texto simples em uma string de caracteres aleatórios (criptografia). Isso permite o armazenamento e/ou transmissão segura de dados sensíveis. O Hashing envolve os seguintes passos-chave:

Entrada de Dados – Primeiramente, os dados que podem ser de qualquer tipo (binário, caractere, decimal, etc.) como texto simples ou string são armazenados.

A Função de Hash – A função de hash é um algoritmo matemático que recebe entrada dos dados e os converte em um conjunto de caracteres ou códigos de hash. As funções de hash são determinísticas (produzem a mesma saída para a mesma entrada) e funções unidirecionais (Isso significa que é quase impossível engenharia reversa a saída das funções de hash, ou seja, um hash em seus dados de entrada).

Resistência à Colisão – Isso significa que uma função de hash é criada tendo em mente a ideia de resistência à colisão, ou seja, dois inputs diferentes não podem ter a mesma saída (código de hash).

Autenticação – As funções de hash são determinísticas, produzindo o mesmo hash para o mesmo input. Assim, ao autenticar uma senha armazenada como um hash, a ideia geral é que se a senha para autenticar corresponder ao hash armazenado no banco de dados, a senha está correta.

Salting

Já que o hashing existe há décadas, houve desenvolvimentos como tabelas arco-íris, que contêm bilhões de entradas de dados contendo strings de dados e seus respectivos hashes baseados em diferentes algoritmos de hashing.

Agora, considere uma situação em que um usuário cria uma conta em seu site com uma senha fraca. Assim, em caso de violação de dados, um atacante pode procurar pelos hashes de seus usuários e encontrar a correspondência do hash para a conta do usuário com uma senha fraca. Isso seria desastroso em aplicações de alta segurança. Para evitar que isso aconteça, os salts são usados.

Salting é uma camada adicional de segurança adicionada aos hashes adicionando uma string aleatória de caracteres ao hash de uma senha antes de armazená-lo em um banco de dados. Assim, mesmo se os dados vazarem em uma violação, será difícil para um atacante decifrar um hash contendo sal. Considere o seguinte exemplo:

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

Como podemos ver claramente, a senha armazenada com sal é menos provável de ser quebrada devido à natureza determinística das hashes. Portanto, se um atacante, por exemplo, procurar por essa sequência de hash+senha em uma tabela rainbow, ele não obterá a senha real, mas algo completamente diferente.

Agora, você está pronto para usar o BcryptJS e proteger suas senhas de forma padrão da indústria.

Instalando o BcryptJS e outros módulos necessários

Agora que você sabe sobre hash e sal, tudo o que resta é pegar seu computador e começar a programar. A estrutura do projeto será a seguinte:

Project Structure

Primeiro, vamos começar criando um projeto npm com os seguintes passos:

  1. Abra uma pasta e crie um arquivo, app.js.

  2. Abra uma janela do terminal nesta pasta e digite o comando

npm init

Depois disso, você será solicitado a inserir entradas, mas pode pressionar Enter sem fornecer nenhuma entrada.

  1. Em seguida, crie mais 3 arquivos, a saber
  • auth.js
  • db.js
  • User.js
  1. Na mesma janela do terminal, digite o seguinte comando para instalar os pacotes necessários.
npm install express mongoose BcryptJS nodemon

Agora você tem um ambiente de projeto completo para seguir neste tutorial. No próximo passo, você aprenderá como criar um servidor para usar o BcryptJS para armazenar e autenticar senhas com segurança no MongoDB.

Configurando um Servidor com Express JS

Agora que você configurou a estrutura do projeto, você pode criar um servidor que usa bcrytjs para proteger senhas armazenando-as como hashes e autenticando-as. Vamos criar o servidor com os seguintes passos apropriados.

Passo 1 – Criando uma conexão com o banco de dados MongoDB

Para conectar ao mongoDB, estamos usando a edição comunitária. Para manter o projeto organizado, você irá salvar o código para configurar uma conexão no arquivo db.js.

  1. const mongoose = require("mongoose");
  2. const mongoURI = "mongodb://127.0.0.1:27017/bcrypt_database";
  3. const connectMongo = async () => {
  4. try {
  5. await mongoose.connect(mongoURI);
  6. console.log("Connected to MongoDB!");
  7. } catch (error) {
  8. console.error("Error connecting to MongoDB: ", error.message);
  9. }
  10. };
  11. module.exports = connectMongo;

Aqui, você importa o pacote mongoose, que fornece uma API para conectar JavaScript ao MongoDB. Além disso, neste caso, a URI de conexão é para uma instalação local do MongoDB. Se você estiver usando um banco de dados na nuvem (como o Atlas), você só precisa alterar a URI para a URI específica do seu banco de dados.

Informação
Um URI é semelhante a um URL de servidor com a diferença de que um URI pode identificar o nome e a identidade dos recursos, assim como a sua localização na internet. Em contraste, um URL é um subconjunto de um URI capaz de realizar apenas esta última função.

A função connectToMongo é uma função async porque mongoose.connect retorna uma promessa em JavaScript. Esta função fornecerá uma conexão após a execução bem-sucedida. Caso contrário, retornará um erro.

Finalmente, utilizamos module.exports para exportar esta função sempre que o módulo db.js for importado.

Passo 2 – Criando um Esquema de Usuário

Você precisará de um esquema básico para criar ou autenticar usuários com um banco de dados MongoDB. Se você não souber o que é um esquema, pode usar este excelente guia da DO para entender e criar o esquema no MongoDB. Vamos usar apenas dois campos para o nosso esquema, email e password.

Use o seguinte código no seu módulo User.js.

  1. const mongoose = require("mongoose");
  2. const UserSchema = new mongoose.Schema({
  3. email:{
  4. type:String,
  5. required:true,
  6. unique:true
  7. },
  8. password:{
  9. type:String,
  10. required:true
  11. }
  12. });
  13. module.exports = mongoose.model('user', UserSchema)

Criamos dois campos neste esquema, email e password. Ambos são campos obrigatórios e do tipo de dados string. Além disso, email é um campo único, significando que um email pode ser usado apenas uma vez para criar uma conta neste servidor.

Finalmente, exportamos um modelo chamado users. Um modelo representa um esquema no banco de dados. Você pode pensar em um esquema como uma regra para definir um modelo, enquanto o modelo é armazenado como uma coleção no banco de dados MongoDB.

Você pode converter um esquema em um modelo usando a função model() da biblioteca mongoose.

Passo 3 – Configurando o servidor em app.js

Após seguir os passos anteriores, você criou com sucesso um modelo e um módulo para estabelecer uma conexão com o banco de dados MongoDB. Agora, você aprenderá a configurar o servidor. Use o seguinte código em seu arquivo app.js.

  1. const connectToMongo = require("./db");
  2. const express = require("express");
  3. const app = express();
  4. connectToMongo();
  5. app.use(express.json());
  6. app.use("/auth", require("./auth"));
  7. const port = 3300;
  8. app.listen(port, () => {
  9. console.log(`Listening at http://localhost:${port}`);
  10. });

Aqui, importamos os módulos express e db.js para conectar ao MongoDB. Em seguida, usamos o middleware express.json() para lidar com respostas JSON. As rotas são criadas em um módulo diferente (auth.js) para manter o código limpo e organizado. Então, finalmente, criamos um endpoint para o servidor ouvir na porta 3300 no localhost. (Você pode usar qualquer porta de sua escolha)

Encriptando Senhas e Armazenando-as em um Banco de Dados MongoDB

Até este ponto, o servidor está quase pronto, e agora você irá criar endpoints para o servidor. Vamos criar dois endpoints – signup e login. No endpoint de cadastro (signup), iremos pegar o email e a senha de um novo usuário e armazená-los com criptografia para a senha usando BcryptJS.

No arquivo auth.js, digite o seguinte código:

  1. const express = require("express");
  2. const router = express.Router();
  3. const User = require("./User");
  4. const bcrypt = require("bcryptjs");
  5. // ROTA 1:
  6. router.post("/signup", async (req, res) => {
  7. const salt = await bcrypt.genSalt(10);
  8. const secPass = await bcrypt.hash(req.body.password, salt);
  9. let user = await User.create({
  10. email: req.body.email,
  11. password: secPass,
  12. });
  13. res.json({ user });
  14. });
  15. module.exports = router;

Fazemos as importações necessárias e então configuramos o roteador express para criar o endpoint /signup. Estamos utilizando o método POST para que as credenciais não sejam divulgadas na URL da aplicação. Depois disso, criamos um Sal usando a função genSalt do pacote scripts; o parâmetro passado para a função genSalt() contém o comprimento dos caracteres do sal. Em seguida, utilizamos a função hash() do BcryptJS, que recebe um parâmetro obrigatório, a string de senha a ser convertida em código hash, e um argumento opcional, a string de sal. E então ela retorna um hash que contém tanto a senha quanto o sal.

Depois disso, usamos a função create() do módulo mongoose para criar um documento em nosso banco de dados definido pelas regras do modelo usuários. Ele recebe um objeto Javascript contendo email e senha, mas em vez de fornecer uma string bruta, fornecemos o secPass (hash de senha + salt) para ser armazenado no banco de dados. Dessa forma, armazenamos com segurança uma senha no banco de dados usando seu hash combinado com um salt em vez de uma string bruta. Por fim, retornamos uma resposta JSON contendo o modelo de usuário. (Este método de envio de respostas é apenas para a fase de desenvolvimento; em produção, você substituirá isso por um token de autenticação ou algo semelhante).

Para testar este endpoint, você deve executar o servidor primeiro, o que pode ser feito digitando o seguinte comando no terminal.

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

Este comando executará seu servidor em localhost e porta 3300 (ou qualquer porta que você especificar). Em seguida, você pode enviar uma solicitação HTTP para a URL http://localhost:3300/auth/signup com o seguinte corpo:

  1. {
  2. "email":"[email protected]",
  3. "password":"sammy"
  4. }

Isto produzirá a seguinte saída/resposta:

{
  "user": {
    "email": "[email protected]",
    "password": "$2a$10$JBka/WyJD0ohkzyu5Wu.JeCqQm33UIx/1xqIeNJ1AQI9kYZ0Gr0IS",
    "_id": "654510cd8f1edaa59a8bb589",
    "__v": 0
  }
}

Nota: O hash da senha e o ID não serão os mesmos, pois são sempre únicos.

Na próxima seção, você aprenderá como acessar o hash armazenado para a senha e autenticá-lo com uma senha fornecida no login/autenticação.

Acessando a senha criptografada e usando-a para autenticação

Até agora, você aprendeu sobre BcryptJS, hashing, salting, e desenvolveu um servidor express que cria novos usuários com senhas armazenadas como hashes. Agora, você aprenderá como usar a senha armazenada e autenticar um usuário quando ele tenta fazer login na aplicação.

Para adicionar um método de autenticação, acrescente a seguinte rota no seu arquivo auth.js após a rota /signup:

  1. // ROTA 2:
  2. router.post("/login", async (req, res) => {
  3. let user = await User.findOne({ email: req.body.email });
  4. if (!user) {
  5. return res.status(400).json({ error: "Login with proper credentials!" });
  6. }
  7. const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  8. if (!passwordCompare) {
  9. return res
  10. .status(400)
  11. .json({ error: "Login with proper credentials!" });
  12. }
  13. res.json({ success: "Authenticated!" });
  14. });

Aqui, nós usamos o subcaminho /auth/login para realizar uma autenticação de login para um usuário já existente. Semelhante ao endpoint /auth/signup, esta será uma função async-await, já que o BcryptJS retorna promessas.

Primeiramente, utilizamos a função findOne da biblioteca Mongoose, que é usada para encontrar um documento em uma coleção com base em uma determinada consulta de pesquisa. Neste caso, estamos procurando o usuário com base no email. Se nenhum usuário com o email fornecido existir, então isso enviará uma resposta com o código de status 400 para credenciais inválidas. (Não é uma boa prática fornecer qual parâmetro está incorreto durante o login, pois os atacantes podem usar essa informação para encontrar contas existentes).

Se o usuário com o email fornecido existir, o programa avança para a comparação de senhas. Para este fim, o BcryptJS fornece o método compare(), que recebe uma string bruta como primeiro argumento e um hash (com ou sem salt) como segundo argumento. Em seguida, ele retorna uma promessa Booleana; true se a senha corresponder ao hash e false se não corresponderem. Então, você pode adicionar uma verificação simples usando um if statement e retornar sucesso ou erro com base na comparação.

Finalmente, você irá exportar o express router usando module.exports para o ponto de partida do app.js para utilizá-lo para rotas.

Para testar esta rota, você pode enviar outra resposta HTTP para este URL http://localhost:3300/auth/login com o corpo conforme abaixo:

  1. {
  2. "email":"[email protected]",
  3. "password":"sammy"
  4. }

Esta requisição dará a seguinte resposta:

{
  "success": "Authenticated!"
}

Por fim, auth.js ficará assim:

  1. const express = require("express");
  2. const router = express.Router();
  3. const User = require("./User");
  4. const bcrypt = require("bcryptjs");
  5. // ROTA 1:
  6. router.post("/signup", async (req, res) => {
  7. const salt = await bcrypt.genSalt(10);
  8. const secPass = await bcrypt.hash(req.body.password, salt);
  9. let user = await User.create({
  10. email: req.body.email,
  11. password: secPass,
  12. });
  13. res.json({ user });
  14. });
  15. // ROTA 2:
  16. router.post("/login", async (req, res) => {
  17. let user = await User.findOne({ email: req.body.email });
  18. if (!user) {
  19. return res.status(400).json({ error: "Login with proper credentials!" });
  20. }
  21. const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  22. if (!passwordCompare) {
  23. return res
  24. .status(400)
  25. .json({ error: "Login with proper credentials!" });
  26. }
  27. res.json({ success: "Authenticated!" });
  28. });
  29. module.exports = router;

Conclusão

Este tutorial criou um servidor para explicar o uso do BcryptJS para armazenar e acessar senhas de banco de dados de forma segura. Você pode avançar mais ainda, implementando:

  • Mais rotas para outras tarefas, como buscar credenciais de usuário, atualizar credenciais de usuário, etc.

  • Implementando tokens de autenticação para enviar como respostas, etc.

Isso inicia sua jornada para lidar com senhas de forma segura; você sempre pode adicionar mais segurança com diferentes técnicas e tecnologias, permitindo que você crie aplicativos mais seguros e resilientes.

Source:
https://www.digitalocean.com/community/tutorials/how-to-handle-passwords-safely-with-bcryptsjs-in-javascript