Autorização em Node.js com Passport – Parte 4

Esse tutorial é uma continuação de uma série de artigos sobre segurança em Node.js que iniciei há algum tempo, usando o módulo Passport, o mais popular para autenticação em Node.js. Nas etapas anteriores nós criamos o login, o cadastro, a recuperação de senha e o logout. Hoje vamos criar a camada de autorização, ou seja, uma vez que o usuário esteja autenticado, devemos permitir ou não que ele faça determinadas operações no sistema conforme o seu perfil de acesso.

Atenção: para que possa acompanhar este tutorial é necessário que tenha realizado as etapas anteriores ou ao menos baixado o zip do tutorial anterior, disponível via formulário sempre ao final dos tutoriais. Note que mesmo baixando o zip, você terá de ter o Node.js e o MongoDB instalados na sua máquina (este último rodando) e tem de revisar o arquivo .env para configurar ele com os seus dados (email, string de conexão do banco, etc). Sugiro cadastrar também alguns usuários no seu Mongo para poder usar o sistema corretamente.

Vale salientar que este artigo possui uma versão em videoaula, disponível aos inscritos em meu curso de Node.js e MongoDB.

Vamos lá!

O tripé de segurança

Os elementos mais básicos de segurança em sistemas são o tripé AAA: Autenticação, Autorização e Auditoria. Já falamos extensivamente sobre autenticação nos artigos anteriores e nesse vamos falar de autorização.

A diferença mais básica é que autenticação se resume a dizer quem pode ou não entrar. Já autorização, é o que cada usuário que entrou, pode fazer no sistema.

Existem várias maneiras de implementar isso, das mais elaboradas (usando recursos dedicados de infraestrutura como Active Directory ou LDAP, por exemplo) às mais simples, ter perfis no seu banco de dados e dar match com eles na hora de ver se o usuário pode ou não fazer algo que ele quer. Eu irei neste tutorial pela segunda abordagem.

Espero em artigos futuros falar do terceiro A do tripé: a auditoria, que nada mais é do que o registro das atividades dos usuários no sistema, para consulta futura.

Autorização com ExpressJS e Passport

Como já foi implementado nos tutoriais anteriores e extensivamente explicado, o Passport é um framework genérico para construir de maneira modular a segurança do seu sistema. Nos tutoriais passados usamos ele em conjunto com um conector de MongoDB, pra usar o respectivo banco como sendo a verdade em termos de autenticação do sistema.

Assim, a cada requisição do ExpressJS, usamos o middleware para chamar uma função de verificação e dizer se o usuário pode ou não continuar fazendo aquela requisição ou se deve ser jogado para a tela de login. O que iremos fazer aqui são três coisas:

  • fornecer funções genéricas para que possa ser verificado a autorização do usuário para determinada operação;
  • aplicar tais funções no middleware do Express para, nas chamadas do Passport, avaliar não apenas a autenticação, mas também a autorização;
  • chamar tais funções também na renderização da interface, para garantir que ela atenda às necessidades de segurança conforme usuário autenticado;

Com isso, você não terá um sistema completo, mas um overview de como aplicar tais conceitos e códigos em um sistema real.

Adaptando o cadastro

A primeira coisa que vamos fazer é adaptar a nossa tela de signup para incluir a informação de perfil do usuário. Aqui faremos algo super simples: ao cadastrar um novo usuário, você vai selecionar entre os perfis de administrador e de usuário.

Modifique a view signup.ejs para incluir um select de perfil, como abaixo:

O que vai resultar em uma tela renderizada assim:

Signup com Perfil
Signup com Perfil

O usuário é salvo no banco de dados através de uma função createUser que fica no nosso módulo db.js. Assim, vamo nesse módulo alterar a respectiva função:

Note que você já tem usuários cadastrados no sistema, eles não terão a informação de profile (perfil) salva neles. Aí você tem duas opções: tratar quem não tem perfil como sendo um usuário padrão (que não é administrador), ou fazer um script para alterar todos os usuários já cadastrados no MongoDB para terem a informação de profile também.

Eu vou de primeira opção, vou considerar que por padrão os usuários sem profile NÃO são administradores.

Agora, para fazer o botão Save voltar a funcionar, vamos na rota users.js e vamos editar o código que trata requisições POST no endpoint /signup para que ele espere e salve o dado do perfil também, usando a função que acabamos de alterar, como abaixo:

Com isso, seu cadastro já deve estar funcionando novamente e sugiro que cadastre um novo usuário como sendo administrador, já que os anteriores vão ser todos considerados comuns.

Está curtindo o post? Para uma formação ainda mais completa como programador web recomendo meu livro sobre programação web com Node.js clicando no banner abaixo!

Inserindo autorização por página

Agora que você tem a informação de perfil junto ao usuário cadastrado e cadastrou um usuário administrador, vamos dar alguns privilégios apenas para administradores. Que tal criar uma página que somente eles podem acessar?

Vamos criar uma nova view chamada views/reports.ejs com um conteúdo bem simples, só pra testarmos o conceito:

E vamos criar um novo arquivo de rota em routes/reports.js com apenas o tratamento de um GET, como abaixo:

Note como o global.autheticationMiddeware() é chamado, não apenas nesta, mas em todas rotas do sistema. Isso garante uma centralização das regras de segurança de acesso, que vamos modificar depois.

Registre essa nova rota no app.js como abaixo:

Se você rodar essa aplicação agora e tentar acessar digitando a URL http://localhost:3000/reports no seu navegador, conseguirá acessar normalmente esta página com qualquer usuário devidamente autenticado, mas não conseguirá acessar antes de fazer login (o servidor vai te redirecionar pra página de login se tentar fazer isso).

Mas e se quisermos restringir o acesso a essa página somente a administradores?

Existem várias maneiras de fazer isso. Vou te mostrar uma possibilidade que é centralizando em um módulo a estrutura de decisão para acesso às páginas. Certamente você deve ter várias ideias de como fazer melhor isso, mas essa é a mais simples e didática que consegui imaginar.

Crie na raiz do seu projeto um arquivo permissions.js e vamos criar nele uma lógica que, dada uma requisição, verificamos o usuário autenticado e que página ele está tentando acessar. Batemos essas informações com um switch/case e concedemos ou não a autorização para aquela página.

Note que eu inicio o código criando uma constante e uma função muito simples para dizer se um perfil é ou não é admin. Mas é no module.exports que realmente programo algo de valor pois, dado uma request, eu pego as informações que me interessam e confronto elas com as URLs e suas lógicas de autorização.

Essa é uma abordagem centralizada em um módulo para as regras de autorização. Assim fica bem fácil de manter a aplicação com as regras necessárias. Outra abordagem que já usei no passado era de fazer isso em cada página, ao invés de fazer centralizado e ficou bem ruim de dar manutenção pois era fácil de esquecer de adicionar as regras certas. Aqui, do jeito que está, caso você se esquecer, ao testar já não vai abrir a página e você vai lembrar que deve permitir no arquivo permissions.js.

Mas como fazer esse código de fato passar a valer?

Volte ao app.js e você vai encontrar o código que configura o global.authenticationMiddleware. Você vai modificá-lo para, além de verificar se o usuário está autenticado, verificar se ele tem a permissão necessária para acessar aquela rota, usando o módulo que acabamos de criar, assim:

Agora, toda vez que uma rota for acionada, além de verificar se o usuário está autenticado ou não, vai ser verificado se ele possui permissão para acessar aquela rota. Eu deixei um comentário no código do permissions.js que lhe sugere no futuro verificar o method da request também. Assim, você pode diferenciar tentativas de acesso de tentativas de submissão de formulários, por exemplo.

Esse código, do jeito que está, já funciona e se você acessar sua aplicação e se autenticar, dependendo do usuário que utilizou vai conseguir ou não acessar a página /reports.

Customizando a interface pelo perfil

Para finalizar esse artigo, outra coisa bem comum de ser utilizado em sistemas são as interfaces customizadas conforme a permissão do usuário. Por exemplo, um botão pode ser visto apenas um perfil específico, pois é um botão importante.

Claro, você nunca deve confiar apenas em esconder elementos de interface, mas considerando que já garantimos na seção anterior a segurança por acesso, independente se foi feito digitando a URL ou clicando em links, agora vamos fazer um exemplo bem simples de esconder ou mostrar um elemento de interface baseado no perfil autenticado.

Primeiro, vamos na rota index.js e vamos mudar o código levemente, para informar o perfil do usuário autenticado no model que enviaremos pra view, como abaixo:

Note que a única linha que mexi foi aquela do profile, para passá-lo para a view.

Agora, na view views/index.ejs, vamos editar o código HTML para, baseado no perfil do usuário, exibir ou não um link para a página de relatórios. Basta inserir esse trecho em qualquer parte da página:

Com isso, você pode testar sua aplicação com diferentes usuários e verificar que somente quando se autentica com os administradores é que consegue ver o dito-cujo link que acabou de adicionar.

Link para Admins
Link para Admins

O mais legal é que, independente do usuário ver ou não o link, se ele tentar burlar acessando diretamente a página de reports, a nossa segurança que está no middleware vai barrar ele também.

E por hoje é isso, espero que tenha gostado, deixe suas dúvidas nos comentários. Você pode baixar os fontes usando o formulário que pede seu e-mail e, se quiser participar do meu curso de Node.js e MongoDB, além de fazer parte do meu grupo online de Whatsapp com mais de 100 estudantes de Node, clique no banner abaixo.

Este artigo tem uma versão em videoaula em uma das aulas do meu curso de Nodejs e MongoDB que você confere clicando no banner abaixo.

Curso Node.js e MongoDB
Curso Node.js e MongoDB

Tutorial CRUD em Node.js com driver nativo do MongoDB – Parte 2

O tutorial de hoje é uma continuação de um outro tutorial aqui do blog, muito acessado aliás, onde ensino como fazer um sistema de cadastro bem simples em Node.js, usando o web framework ExpressJS e o banco de dados MongoDB. Ou seja, o famoso CRUD.

Nesta segunda parte, parto para um conceitos mais avançados em cima do mesmo projeto: paginação de resultados.

Para conseguir acompanhar todos os códigos, é importante que você tenha realizado a parte anterior ou pelo menos baixe os códigos-fonte que se encontram no formulário ao final do tutorial anterior. Caso prefira assistir um vídeo, o tutorial anterior pode ser resumido com este vídeo abaixo que é uma aula gratuita do meu curso de Node.js e MongoDB. Aproveite!

Vamos lá!

Atenção, este mesmo tutorial está disponível em videoaula em meu curso de Node.js e MongoDB.

Paginação com Node.js e MongoDB

Nossa aplicação simplesmente lista todos os documentos da nossa coleção no MongoDB, sem qualquer distinção. Nas primeiras duas etapas deste tutorial vamos restringir isso, primeiramente através de uma paginação de resultados e depois através de uma busca.

Conforme trato em detalhes no artigo Tutorial MongoDB para iniciantes em NoSQL: Parte 2, fazemos paginação no MongoDB usando as funções skip e limit de maneira apropriada.

A função skip indica ao MongoDB que deve-se ignorar um número x de resultados da consulta na hora de retornar do servidor de banco. Já a função limit diz ao Mongo que ele deve retornar apenas um número limitado de documentos, independente se a busca retornaria mais elementos normalmente. Ambas funções devem ser usadas após um find, como veremos a seguir.

A lógica para criar paginação é bem simples: determine um tamanho de página, por exemplo 10 elementos, descubra o total de elementos que a consulta retornaria, por exemplo 21 elementos, e depois dividida o total pelo tamanho de página, arredondando sempre pra cima. Pronto, você tem a quantidade de páginas para aquela consulta! Neste exemplo de 21 elementos com uma paginação de tamanho 10, serão 3 páginas, sendo que as duas primeiras terão exatamente 10 elementos e a última apenas 1.

Entendeu?

Para determinar o tamanho de página ideal para seu sistema você tem de levar em consideração a performance da sua aplicação e a experiência do seu usuário. Muitas páginas com poucos elementos é fácil do banco retornar, mas ruim pro usuário ficar navegando. Poucas páginas com muitos elementos é o oposto e o segredo está no equilíbrio. Neste exemplo, ficaremos com tamanho 10 mesmo, para fins de teste.

Paginação de Resultados em Node.js

Então nosso primeiro passo será modificar uma function já existente no módulo db.js da nossa aplicação para que ela retorne os resultados de maneira paginada, como abaixo:

Note que comecei definindo uma constante com o tamanho das páginas sendo 10. Depois, adicionei um novo parâmetro na função findAll que espera a página que a aplicação deseja apresentar. Este parâmetro eu uso para calcular o skip, ou seja, quantos elementos da consulta eu devo ignorar. Se a página for a primeira, o skip será zero e serão mostrados os primeiros 10 elementos. Se a página for a segunda, o skip será 10 pela fórmula e serão mostrados os elementos das posições 11 a 20 (ordinal, ou 10 a 19 considerando um array zero-based).

Agora vamos modificar onde esta função findAll é chamada: na pasta routes, módulo index.js, que vamos modificar levemente a rota GET padrão apenas para adicionar o parâmetro página nela:

Note que coloquei o parâmetro no path como sendo opcional (?) e se ele não tiver sido passado na URL, será atribuído como um. Esse truque do || para atribuir um valor default eu ensinei no artigo 15 Dicas de JavaScript, lembra?

Certifique-se de que esta rota seja a última do arquivo index.js, logo antes do module.exports!

Como as rotas são testadas da primeira até a última para ver qual que processará a requisição deixaremos esta pro final para não interferir nas demais avaliações.

Agora execute a aplicação e teste a index passando a página na URL, como abaixo. Note que tive de adicionar muitos customers no banco de dados para podermos ter alguma paginação.

Paginação funcionando via URL
Paginação funcionando via URL

Mas obviamente não faremos nosso usuário ter de mudar parâmetros na URL para acessar as páginas, certo?

Está curtindo o post? Para uma formação ainda mais completa como programador web recomendo meu livro sobre programação web com Node.js clicando no banner abaixo!

Paginação de resultados com ExpressJs e EJS

Para que o usuário saiba quantas páginas existem e para que consiga acessar as mesmas, vamos ter de criar uma lógica para construir o HTML de paginação no frontend e uma lógica para retornar algumas informações importantes no backend.

Vamos começar pelo backend que é mais a minha praia. XD

Abra seu arquivo db.js novamente e vamos criar uma function countAll que executa o callback passado por parâmetro retornando um erro (opcional) e a quantidade de documentos na coleção customers. Note que atualizei o module.exports com a nova função e também exportando a constante com o tamanho de página.

Agora na routes/index.js, mais especificamente no nosso GET default (a última rota do arquivo), vamos ajustar as chamadas para construir o model com as informações que precisaremos no frontend EJS:

Note que tive de encadear as duas chamadas de banco uma dentro da outra, para garantir que a página somente será renderizada quando tivermos tanto os documentos da consulta quanto a quantidade total de documentos na coleção. Esse é justamente o ponto fraco do uso de callbacks, o chamado Callback Hell, mas falarei disso em outro artigo.

A cereja do bolo fica para o cálculo de quantidade de páginas que fizemos ali, dividindo o total de documentos pelo tamanho da página, usando a constante existente no módulo db.js. Ah sim, você não esqueceu de expor esta constante no module.exports, certo?

Agora que nosso model está completo, vamos mexer na index.ejs, nossa view inicial da aplicação para renderizar a quantidade total de elementos e o HTML de paginação para que o usuário possa navegar entre as páginas:

Esse código eu coloquei bem no final do arquivo EJS, onde antes ficava apenas o botão de Cadastrar novo cliente. Se você fez tudo certo até aqui, quando testar novamente você deve ver o seguinte resultado na interface da listagem:

Paginação funcionando
Paginação funcionando

Melhorando usabilidade da paginação

Para encerrar este tutorial, vamos adicionar uma perfumaria para melhorar ainda mais a usabilidade desta página: a página atual não deve ter link, apenas as demais páginas, assim, o usuário saberá em que página ele se encontra atualmente.

Vamos iniciar ajustando o model retornado no nosso index.js para informar também a página atual solicitada pelo usuário:

Note que a mudança foi bem sutil, apenas uma nova propriedade no JSON ‘pagina’.

Para fazer a lógica necessário para que o HTML ora tenha link, ora não tenha, basta adicionar um if na lógica de construção do nosso EJS usando a informação da página oriunda do model:

Ou seja, se a variável de iteração ‘i’ for diferente da página atual, escreve um link HTML na tela, caso contrário, escreve apenas o número da página. Visualmente falando temos a imagem abaixo como referência:

Indicando a página atual
Indicando a página atual

Note como ficou evidente que estamos na página 2, pois não tem link pra ela!

Outras melhorias poderiam incluir legendas quando se passa o mouse sobre as páginas, links de próxima página e página anterior, lógica para quando tivermos muitas páginas (e se tiver 100 páginas, como ficará a tela?) e até mesmo informação textual de que você está vendo os itens x a y da página z.

Mas…isso fica para você pensar meu amigo ou minha amiga! 🙂

Até o próximo post!

Gostou do tutorial? Quer aprender ainda mais sobre Node.js, ExpressJS, EJS e MongoDB comigo? Conheça meu curso de Nodejs e MongoDB clicando no banner abaixo!

Curso Node.js e MongoDB
Curso Node.js e MongoDB

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

Este artigo é uma continuação da parte 2 do meu tutorial de autenticação em Node.js usando a biblioteca Passport. Nessa terceira parte veremos como fazer um recurso bem simples de recuperação de senha via email e de logout do usuário autenticado.

Caso você deseje ver este artigo em vídeoaula, saiba que ela está disponível em meu curso online de Node.js e MongoDB.

Vamos lá!

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:

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

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:

Esqueceu a senha
Esqueceu a senha

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:

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:

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 estas 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:

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
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.

Está curtindo o post? Para uma formação ainda mais completa como programador web recomendo meu livro sobre programação web com Node.js clicando no banner abaixo!

Logout

Até o momento nos preocupamos em autenticar os usuários que desejam entrar no sistema. Mas e os usuários que desejam sair? Como podemos implementar uma funcionalidade de logout na nossa aplicação?

Muito simples!

Na view index.ejs (ou onde quiser), adicione um form HTML com um botão que fará um post em uma rota logoff, como abaixo:

Agora é hora de criarmos esta rota, o que devemos fazer em routes/login.js, nosso módulo de rotas de autenticação. Adicione a nova rota abaixo:

Note que o objeto req (request) possui um método logOut(). Isso porque foi injetada esta função nele através do Passport. Se você executar esta aplicação agora, se autenticar e depois clicar no botão de logoff, não apenas irá voltar para a tela de login, mas não conseguirá acessar a área logada até que se autentique novamente.

Teste e verá que funciona perfeitamente!

Você confere a continuação deste tutorial neste artigo aqui.

Este artigo tem uma versão em videoaula em uma das aulas do meu curso de Nodejs e MongoDB que você confere clicando no banner abaixo.

Curso Node.js e MongoDB
Curso Node.js e MongoDB