Data

10 out, 2017

Autenticação em Node.js com Passport – Parte 02

Publicidade

Este artigo é uma continuação do tutorial de como implantar um mecanismo de autenticação completo em Node.js usando o middleware Passport. No tutorial passado, fizemos funcionar a autenticação através de uma tela de login e impedimos que usuários anônimos entrassem na tela de chat.

Nesta segunda parte, vamos criar a tela de cadastro de usuário (Sign Up) e a tela de recuperação de senha (Forget Password), além que aprenderemos a enviar emails em Node.js.

Vamos lá!

#1 – Criando o cadastro de usuário

No tutorial anterior deixamos nosso banco preparado e a conexão compartilhada em uma variável global.db. Usaremos essa conexão global para fazer as operações de banco necessárias. Também usaremos o mesmo módulo bcrypt para criptografar as senhas no banco.

Para começar, vamos criar a nossa tela de cadastro de usuário que pedirá username, password e email, chamei ela de views/signup.ejs:

<!doctype html>
<html>
  <body>
    <h1>Signup</h1>
    <form action="/users/signup" method="POST">
      Username: <input name="username" type="text" /><br />
      Password: <input name="password" type="password" /><br />
      E-mail: <input name="email" type="text" /><br />
      <input type="submit" value="Save" />
    </form>
    <% if(message) { %>
      <p>
        <label style="color:Red"><%= message %></label>
      </p>
    <%}%>
  </body>
</html>

Note que este form faz um POST para uma rota /users/signup e que ele já espera uma message no model para exibir erro, caso dê algum problema. Faremos isso tudo funcionar depois.

Para que essa view possa ser exibida, primeiro vamos criar um novo arquivo de rotas em routes/users.js. Aqui colocaremos todas as rotas relacionadas à usuários, a começar pela rota que exibe a view que acabamos de criar via GET:

var express = require('express');
var router = express.Router();
 
router.get('/signup', function(req, res, next) {
  if(req.query.fail)
    res.render('signup', { message: 'Falha no cadastro do usuário!' });
  else
    res.render('signup', { message: null });
})
 
module.exports = router;

Note que já fiz o teste para passar ou não uma mensagem de falha para a tela no model, pra não dar erro na minha view. Agora para que essa rota passe a funcionar, temos de configurar nosso app.js, como abaixo:

const users = require('./routes/users')
app.use('/users', users)

Salve tudo e mande rodar sua aplicação para ver ela funcionando no navegador (use o link de “Não possui cadastro?” que deixamos prontos na tela de login):

Cadastro de Usuário

Note que para acessar essa tela, a rota é /users/signup. Isso porque definimos no app.js que rotas ‘/’ são tratadas pela index.js e rotas ‘/users’ são tratadas pela users.js, ou seja, há um roteamento inicial com ‘/users’ e depois o outro roteamento com ‘/signup’, virando ‘/users/signup’ a rota completa.

Agora, antes de sair programando a rota POST /users/signup que está em nosso HTML FORM, devemos criar a função JS que vai salvar um novo usuário no MongoDB. Para armazenar não apenas essa, mas outras funções relacionadas a banco de dados (que não são de autenticação, pois estas deixamos no arquivo auth.js), vamos criar um arquivo db.js na raiz do nosso projeto. Dentro dele, vamos criar e expor uma função createUser, como abaixo:

const bcrypt = require('bcrypt') 
 
function createUser(username, password, email, callback){
    const cryptPwd = bcrypt.hashSync(password, 10)
    global.db.collection("users").insert({ username, password: cryptPwd, email }, function(err, result){
        callback(err, result)
    })
}
 
module.exports = { createUser }

Aqui eu pego a senha plain-text enviada pela view e gero o hash dela usando um salt de 10 rounds, que foi o que achei como sendo seguro atualmente, embora esta resposta dê um panorama mais completo de como calcular isso se seu sistema realmente precisar de uma segurança acima da média (bancos?). Ao final do processo, chamo uma função de callback que a createUser espera como último parâmetro.

Com esse módulo db.js pronto, programar essa rota de /users/signup será bem simples! Vá em routes/users.js e adicione uma rota POST para /signup, como abaixo (lembrando que o path /users já é tratado no app.js):

const db = require('../db')
router.post('/signup', function(req, res, next){
  db.createUser(req.body.username, req.body.password, req.body.email, (err, result) => {
    if(err) res.redirect('/signup?fail=true')
    res.redirect('/')
  })
})

Aqui eu carrego o módulo ‘db’ que acabamos de criar e com ele chamo a função createUser, passando os dados enviados no corpo da requisição HTTP: username, password e email. Como último parâmetro, passo uma arrow function como callback, que apenas vai redirecionar para a tela de login em caso de sucesso e para a própria tela de signup em caso de fracasso, onde uma mensagem de erro será exibida.

Execute novamente sua aplicação e teste a criação de usuário. Se tudo deu certo, após a criação você poderá se autenticar na tela de login usando esse novo usuário.

Mas o que você acha de mandarmos um email de boas vindas para esse usuário que recém se cadastrou?

#2 – Aprendendo a enviar emails

Após o usuário se cadastrar, vamos enviar um email de boas vindas para ele. Além de ser algo interessante, fazer a lógica de envio de emails será útil na próxima etapa do tutorial, que é reset de senha por email.

Para fazer o envio de emails vamos usar um módulo chamado Nodemailer, que deve ser instalado via linha de comando usando o NPM:

$ npm install -S nodemailer

Depois, vamos criar um módulo mail.js na raiz do nosso projeto, com o seguinte código:

module.exports = function(to, subject, text){
    const mailer = require("nodemailer")
    //usando SMTP para envio
    const smtpTransport = mailer.createTransport({
        host: process.env.SMTP_SERVER,
        port: parseInt(process.env.SMTP_PORT),
        secure: false, // upgrade later with STARTTLS
        auth: {
            user: process.env.SMTP_USERNAME,
            pass: process.env.SMTP_PASSWORD
        }
    })
 
    const message = {
        from: process.env.SMTP_USERNAME,
        to,
        subject,
        text,
        //html: "<b>Olá mundo!</b>" //opcional
    }
 
    smtpTransport.sendMail(message, function(error, response){
        if(error) 
            console.log(error)
        else
            console.log("Email enviado: " + response.message);
        smtpTransport.close();
    })
 
}

Nosso módulo mail.js apenas expõe uma função que espera o destinatário da mensagem (to), o assunto (subject) e o texto (text). Por fim, a função sendMail faz o serviço propriamente dito.

Note que as configurações de SMTP para envio de email estão definidas em variáveis de ambiente, assim como fizemos na parte 1 deste tutorial para guardar a connection string do MongoDB.

Não esqueça que para que essas variáveis de ambiente funcionem, você deve adicionar o modelo delas no arquivo .env.example (é um arquivo oculto, você só vai conseguir ver ele com VS Code ou no terminal):

# .env.example, committed to repo
MONGO_CONNECTION=
SMTP_SERVER=
SMTP_PORT=
SMTP_USERNAME=
SMTP_PASSWORD=

E com os valores finais no arquivo .env:

# .env, not committed to repo
MONGO_CONNECTION=mongodb://localhost:27017/chatdemo
SMTP_SERVER=smtp.umbler.com
SMTP_PORT=587
SMTP_USERNAME=contato@luiztools.com.br
SMTP_PASSWORD=XXXXXXXXXXXXXX

Aqui usei minha conta de email na Umbler para enviar os emails (você pode criar sua conta lá e ganhar créditos de graça para gastar com um email que custa centavos por mês). Como é uma aplicação de teste que vai enviar poucos emails, não vamos ter problemas. No entanto, caso queira usar o nodemailer para enviar muitos emails, você rapidamente será bloqueado em qualquer provedor de hospedagem. O certo é usar serviços de SMTP Gateway como Amazon SES, Mandrill e SendGrid.

Agora, para usarmos este módulo e finalmente fazer o envio de email de boas vindas funcionar, adicione apenas uma linha a mais em nossa rota POST em users.js:

const db = require('../db')
router.post('/signup', function(req, res, next){
  db.createUser(req.body.username, req.body.password, req.body.email, (err, result) => {
    if(err) res.redirect('/signup?fail=true')
    require('../mail')(req.body.email, 'Bem vindo ao nosso chat', 'Olá ' + req.body.username + ', obrigado por se cadastrar!')
    res.redirect('/')
  })
})

Obviamente você pode personalizar essa mensagem livremente, ao invés do “email espartano” que deixei configurado. Soluções mais profissionais geralmente envolvem criar templates HTML que são lidos usando o módulo ‘fs’, algumas variáveis dentro do template são trocadas e depois a string resultante é enviada como HTML.

Rode novamente sua aplicação com ‘npm start’ e realize o cadastro de um novo usuário, mas informando um email válido, para que a mensagem chegue e você possa ver o resultado no seu webmail.

Email enviado via Node.js

#3 – Criando a recuperação de senha

Agora a próxima parte é a de recuperação de senha. Lembra que deixamos um link pra isso lá na tela de login?

Vamos começar criando uma nova view, a views/forgot.ejs, com um formulário bem simples que pede a confirmação de seu email para envio de uma nova senha:

<!doctype html>
<html>
  <body>
    <h1>Lembrar senha</h1>
    <form action="/users/forgot" method="POST">
      <p>Digite o email de cadastro abaixo para receber novas credenciais de acesso:</p>
      <input name="email" type="text" />
      <input type="submit" value="Send" />
    </form>
    <p>
      Caso não possua cadastro <a href="/users/signup">clique aqui</a>.
    </p>
  </body>
</html>

E para que essa tela seja acessível, vamos criar uma nova rota GET em routes/users.js:

router.get('/forgot', function(req, res, next) {
  res.render('forgot', { });
})

Rodando sua aplicação e acessando no navegador, já deve ser possível navegar até esta página usando o link na tela de login:

Para fazer funcionar este formulário, vamos começar criando duas funções em nosso arquivo db.js: uma que busca um usuário pelo seu email e outra que muda a senha de um usuário:

function findUser(email, callback){
    global.db.collection("users").findOne({email}, callback)
}

function changePassword(email, password){
    const cryptPwd = bcrypt.hashSync(password, 10)
    global.db.collection("users").updateOne({email}, {$set:{password: cryptPwd}})
}

module.exports = { createUser, findUser, changePassword }

A primeira função é bem simples, um findOne por email, que executa um callback após encontrar (ou não) o usuário. A segunda recebe uma senha, criptografa ela e sobrescreve o hash de senha do usuário cujo id foi passado por parâmetro. O module.exports no final do db.js também foi atualizado de acordo.

Note que não estou criando as devidas validações em nenhum formulário para os artigos não ficarem gigantescos. Nem de campos obrigatórios, nem validações de regras de negócio como não permitir usuários e emails duplicados. Tenha em mente que você deverá implementar essas questões se for usar esses códigos em produção.

Antes de sairmos fazendo a rota POST, vamos criar um arquivo utils.js na raiz do nosso projeto e colocar dentro dele uma função de geração de senha aleatória:

function generatePassword(){
    const chars = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    var pass = ''
    for(var i=0; i< 10; i++)
      pass += chars.charAt(Math.random() * 61)
    return pass
}
 
module.exports = { generatePassword }

Esta função é bem simples e talvez você até queira usar alguma mais avançada. Aqui eu crio e retorno uma senha aleatória de 10 caracteres alfanuméricos (maiúsculas, minúsculas e números).

Agora que temos essas funções, podemos criar a nossa rota POST que vai receber os dados do formulário de “esqueci minha senha”. Abra o arquivo routes/users.js e crie a nova rota:

router.post('/forgot', function(req, res, next) {
  db.findUser(req.body.email, (err, doc) => {
    if(err || !doc) res.redirect('/')//manda pro login mesmo que não ache
    const newpass = require('../utils').generatePassword()
    db.changePassword(req.body.email, newpass)
    require('../mail')(req.body.email, 'Sua nova senha do chat', 'Olá ' + doc.username + ', sua nova senha é ' + newpass)
    res.redirect('/')
  })
})

Aqui começamos com o findUser que criamos anteriormente, que busca usuário por email. Caso não encontre, vamos jogar o usuário para a tela de login mesmo assim, embora você possa pensar em algo mais criativo para fazer.

Caso encontre, mandamos gerar uma nova senha usando a função que criei há pouco e uso o changePassword para mudar a senha do usuário que possui o email especificado (em um sistema de produção, para evitar problemas, além do email, peça alguma informação pessoal do usuário para confirmar esse processo de troca de senha). Por fim, um email é enviado com a senha que acabou de ser gerada, como na imagem abaixo.

Nova senha

Se você tentar se autenticar com a senha antiga, notará que ela não funciona mais, somente a nova.

É uma boa prática guardar o hash das últimas senhas do usuário e não deixar que ele use senhas antigas (mesmo senhas aleatórias podem conflitar ocasionalmente). Outra boa prática seria no primeiro login com a nova senha ele ser solicitado a cadastrar uma senha pessoal ao invés dessa aleatória. Enfim, sempre há margem para melhorar quando o assunto é segurança.

Pretendo em uma futura parte 3, abordar outras questões relacionadas à segurança, como autorização (visto que até o momento vimos apenas autenticação).

Até a próxima!