Como lidar com senhas com segurança com BcryptsJS em JavaScript
O autor selecionou a Fundação OWASP para receber uma doação como parte do programa Write for DOnations.
Introdução
Proteger senhas de sites é uma habilidade essencial que qualquer desenvolvedor deve ter. JavaScript
oferece uma opção para garantir o armazenamento e processamento seguro de senhas ou outros dados confidenciais usando os algoritmos de hash fornecidos pelo módulo BcryptJS
do JavaScript
.
Neste tutorial, você aprenderá sobre BcryptJS
e hashing para configurar um servidor expresso 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.
Uma versão estável do Node.js instalada no seu computador com versão 12.x ou superior. Você pode usar este tutorial da DigitalOcean para instalar a versão mais recente do Node Js em seu computador.
-
Você deve saber como codificar em JavaScript.
_Você deve ter o Express JS instalado no seu computador. Você pode usar este guia para aprender como configurar um servidor Express.
Por último, você precisará da comunidade MongoDB ou do banco de dados Atlas para concluir este tutorial. Você pode instalá-lo usando um destes guias da DigitalOcean sobre como instalar o MongoDB.
Por que usar BcryptJS?
Bcrypt é um algoritmo de hash para criar hashes de senhas para armazená-las em caso de violação de dados. Este algoritmo de hash avançado usa sais, tornando-o difícil de quebrar por ataques como força bruta.
BcryptJS é a implementação JavaScript do algoritmo de hash Bcrypt
, permitindo que você use a criptografia de hash sem ter que se preocupar com funções de hash complexas. Alguns dos motivos que tornam o BcryptJS uma ótima opção para segurança de senha são os seguintes:
Segurança – BcryptJS implementa o algoritmo Bcrypt, um algoritmo lento (que em hashing é uma coisa boa) e requer intenso poder computacional. Isso torna uma tarefa rigorosa para os invasores quebrar o hash da senha, garantindo a segurança das senhas mesmo em caso de violação de dados._
Salting – BcryptJS lida com a geração de sais aleatórios para senhas para garantir a segurança do armazenamento (aprenderemos sobre hashes e sais na próxima seção com mais detalhes). Os sais tornam o hash de uma senha relativamente fraca mais complexo, dificultando sua descriptografia.
Fácil de usar – BcryptJS fornece aos desenvolvedores JavaScript uma ferramenta para criptografar suas senhas sem exigir um conhecimento profundo de hash._
Na próxima etapa, aprenderemos brevemente sobre hashes e sais.
Como funciona o Hashing?
Antes de usar esses conceitos em seus projetos, você deve entender como funcionam o hash e o salting.
Hashing
Hashing é a conversão de uma string simples ou texto simples em uma string de caracteres aleatórios (criptografia). Isso permite o armazenamento e/ou transmissão segura de dados confidenciais. Hashing envolve as seguintes etapas principais:
Entrada de dados – Em primeiro lugar, são armazenados dados que podem ser de qualquer tipo (binário, caractere, decimal, etc.) como texto simples ou string.
A Função Hash – A função hash é um algoritmo matemático que recebe informações dos dados e os converte em um conjunto de caracteres ou códigos hash. As funções hash são determinísticas (produzem a mesma saída para a mesma entrada) e funções unidirecionais (isso significa que é quase impossível fazer engenharia reversa na saída das funções hash, ou seja, um hash em seus dados de entrada).
Resistência à colisão – Isso significa que uma função hash é criada tendo em mente a ideia de resistência à colisão, ou seja, duas entradas diferentes não podem ter a mesma saída (código hash).
Autenticação – As funções hash são determinísticas, produzindo o mesmo hash para a mesma entrada. Assim, ao autenticar uma senha armazenada como hash, a ideia geral é que se a senha para autenticação corresponder ao hash armazenado no banco de dados, a senha está correta.
Salga
Como o hash existe há décadas, houve desenvolvimentos como as tabelas arco-íris, que contêm bilhões de entradas de dados contendo cadeias de dados e seus respectivos hashes baseados em diferentes algoritmos de hash.
Agora, considere uma situação em que um usuário que cria uma conta em seu site cria uma senha fraca. Assim, em caso de violação de dados, um invasor pode procurar os 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, são usados sais.
Salting é uma camada adicional de segurança adicionada aos hashes, adicionando uma sequência aleatória de caracteres ao hash de uma senha antes de armazená-la em um banco de dados. Assim, mesmo que os dados vazem em uma violação, será difícil para um invasor descriptografar um hash contendo sal. Considere o seguinte exemplo:
Password = ‘sammy’
Hash = £%$^&£!23!3%!!
Salt = 2vqw£4Df$%sdfk
Hash + Salt = £%$^&£!23!3%!!2vqw£4Df$%sdfk
Como podemos ver claramente, é mais improvável que a senha armazenada com salt seja quebrada devido à natureza determinística dos hashes. Portanto, se um invasor, por exemplo, procurar essa string hash+senha em uma tabela arco-íris, ele não obterá a string de senha real, mas algo completamente diferente.
Agora você está pronto para usar o BcryptJS e proteger suas senhas de maneira padrão do setor.
Instalando BcryptJS e outros módulos necessários
Agora que você sabe sobre hashing e salting, tudo o que resta é pegar seu computador e começar a codificar. A estrutura do projeto será a seguinte:
Primeiro, começaremos criando um projeto npm com as seguintes etapas:
Abra uma pasta e crie um arquivo,
app.js.
Abra uma janela de terminal nesta pasta e digite o comando
npm init
Depois disso, serão solicitadas informações, mas você poderá pressionar Enter sem fornecer nenhuma entrada.
- Em seguida, crie mais 3 arquivos, a saber
auth.js
db.js
User.js
- Na mesma janela do terminal, digite o seguinte comando para instalar os pacotes necessários.
npm install express mongoose BcryptJS nodemon
Agora você tem uma configuração completa do ambiente do projeto para seguir neste tutorial. Na etapa seguinte, você aprenderá como criar um servidor para usar BcryptJS para armazenar e autenticar senhas com segurança com MongoDB.
Configurando um servidor com Express JS
Agora que configurou a estrutura do projeto, você pode criar um servidor que use bcrytjs
para proteger senhas, armazenando-as como hashes e autenticando-as. Criaremos o servidor com as seguintes etapas adequadas.
Passo 1 – Criando uma conexão com o banco de dados MongoDB
Para conectar-se ao mongoDB
, estamos usando a edição da comunidade. Para manter o projeto organizado, você salvará o código para configurar uma conexão no arquivo 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;
Aqui, você importa o pacote mongoose
, que fornece uma API para conectar javascript ao MongoDB.
Além disso, neste caso o URI de conexão é para uma instalação local do mongoDB, se você estiver usando um banco de dados em nuvem (como Atlas), você só precisa alterar o URI para o URI do seu banco de dados específico.
Informações 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 e sua localização na Internet. Em contraste, uma URL é um subconjunto de uma URI capaz de fazer apenas o último.
A função connectToMongo
é uma função assíncrona porque mongoose.connect
retorna uma promessa javascript. Esta função fornecerá uma conexão para uma execução bem-sucedida. Caso contrário, ele retornará um erro.
Finalmente, usamos 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 sabe o que é um esquema, pode usar este excelente guia DO para entender e criar o esquema no MongoDB. Usaremos apenas dois campos para nosso esquema, email
e password
.
Use o código a seguir em seu módulo 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)
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, o que significa que um email pode ser usado apenas uma vez para ter uma conta neste servidor.
Finalmente, exportamos um modelo chamado users
. Um modelo representa um esquema no final do 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 as etapas anteriores, você criou com sucesso um modelo e um módulo para fazer uma conexão com o banco de dados MongoDB
. Agora você aprenderá a configurar o servidor. Use o código a seguir em seu arquivo 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}`);
});
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 escutar na porta 3300
no localhost. (Você pode usar qualquer porta de sua escolha)
Criptografando senhas e armazenando-as em um banco de dados MongoDB
Até este ponto, o servidor está quase pronto e agora você criará endpoints para o servidor. Criaremos dois endpoints – signup
e login
. No endpoint de inscrição, pegaremos o e-mail e a senha de um novo usuário e os armazenaremos com criptografia para uma senha usando BcryptJS.
No arquivo auth.js
, digite o seguinte código:
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;
Fazemos as importações necessárias e então configuramos o roteador expresso 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 Salt usando a função genSalt
do pacote scripts
; o parâmetro passado para as funções genSalt() contém o comprimento do caractere salt. Em seguida, usamos a função hash() do BcryptJS, que recebe um parâmetro obrigatório, a string da senha a ser convertida em código hash e um argumento opcional, a string salt. E então ele retorna um hash que contém password
e salt
.
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 users. É necessário um objeto Javascript
contendo email e senha, mas em vez de fornecer uma string bruta, fornecemos o secPass (hash de senha
+ salt
) a 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 do usuário. (Este método de envio de respostas é apenas para a fase de desenvolvimento
; na produção, você substituirá isso por um token de autenticação ou outra coisa).
Para testar este endpoint, você deve primeiro executar o servidor, 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 na porta 3300
(ou em qualquer porta que você especificar). Então, você pode enviar uma solicitação HTTP
para a URL http://localhost:3300/auth/signup
com o seguinte corpo:
{
"email":"sammy@linux-console.net",
"password":"sammy"
}
Isso produzirá a seguinte saída/resposta:
{
"user": {
"email": "sammy@linux-console.net",
"password": "$2a$10$JBka/WyJD0ohkzyu5Wu.JeCqQm33UIx/1xqIeNJ1AQI9kYZ0Gr0IS",
"_id": "654510cd8f1edaa59a8bb589",
"__v": 0
}
}
Nota: O hash e o ID da senha não serão iguais, pois são sempre únicos.
Na seção a seguir, 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 como desenvolver um servidor expresso
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 tentar fazer login na aplicação.
Para adicionar um método de autenticação, anexe a seguinte rota em seu auth.js
após a rota /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!" });
});
Aqui, 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 de espera assíncrona, pois BcryptJS retorna promessas.
Primeiramente, usamos a função findOne
da biblioteca Mongoose, que é usada para localizar 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 não existir nenhum usuário com o e-mail fornecido, uma resposta será enviada 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 invasores podem usar essas informações para encontrar contas existentes).
Se existir o usuário com o e-mail fornecido, o programa passa para a comparação de senha. Para este propósito, BcryptJS
fornece o método compare()
, que recebe uma string bruta como primeiro argumento e um hash
(com ou sem salt
) para o segundo argumento. Então, ele retorna uma promessa booleana
; true
se a senha corresponder ao hash e false se não corresponderem. Em seguida, você pode adicionar uma verificação simples usando uma instrução if e retornar sucesso ou erro com base na comparação.
Por fim, você exportará o roteador expresso
usando module.exports para o ponto de partida app.js
para usá-lo em rotas.
Para testar esta rota, você pode enviar outra resposta HTTP
para esta URL http://localhost:3300/auth/login
com o corpo da seguinte forma:
{
"email":"sammy@linux-console.net",
"password":"sammy"
}
Esta solicitação dará a seguinte resposta:
{
"success": "Authenticated!"
}
Por fim, auth.js
ficará assim:
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;
Conclusão
Este tutorial criou um servidor para explicar o uso de BcryptJS para armazenar e acessar senhas de banco de dados com segurança. Você pode levar isso adiante:
Implementar mais rotas para outras tarefas, como buscar credenciais de usuário, atualizar credenciais de usuário, etc.
Implementar tokens de autenticação para enviar como respostas, etc.
Isso inicia sua jornada para lidar com senhas com segurança; você sempre pode adicionar mais segurança com diferentes técnicas e tecnologias, permitindo criar aplicações mais seguras e resilientes.