quarta-feira, 21 de janeiro de 2015

Exemplo de Autenticação em Node.JS - Passport e autenticação local

Demais posts desta série:

Olá a todos,


Continuando nossa série de posts sobre autenticação com Node.JS, agora vamos finalmente instalar, configurar e testar o passport, que é a biblioteca que vai gerenciar os nossos logins.

Lembrando que o projeto completo está disponível aqui, e este tutorial foi baseado neste  excelente link.

Para nossa aplicação, trabalharemos com as seguintes rotas (por enquanto):
  • /
  • /local/signin (get e post), formulário de login
  • /local/signup (get e post), formulário de cadastro
  • /signout, sair da página
  • /dashboard, página acessada após o login
Views iniciais e Modelo de usuário

Porém, antes de configurar e instalar o passport, precisamos fazer as views iniciais da nossa aplicação. Serão 3 (index, signin, signup). Porém, vamos usar o handlebars (que já instalamos via express-generator) para facilitar um pouco a nossa vida. Utilizando o handlebars, podemos criar layouts para a nossa página, evitando que se tenha muito código duplicado.

Primeiro, vamos editar o arquivo layout.hbs, substituindo o que estiver lá pelo seguinte conteúdo:

<!DOCTYPE html>                                                                                   
<html>                                                                                            
  <head>                                                                                    
    <title>Exemplo de autenticação com Node.JS, Passport e Express.JS</title>                    
    <link rel='stylesheet' href='//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css' />
    <link rel='stylesheet' href='//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css' />
    <style>   
        body: { padding-top : 80px; }                                            
    </style>                                                                               
  </head>                                                                                    
  <body>
        <div class="container">            
                {{#if message }}                                         
                     <div class="alert alert-danger">{{message}}</div>
                {{/if}}
                                                                         
                {{{body}}}                                                                   
        </div>                                                                                
  </body>                                                                                    
</html>                                                                                         

E depois, substituindo o conteúdo do arquivo index.hbs por este:

<div class="jumbotron text-center">                                                              
        <h1><span class="fa fa-lock"></span> Node Auth</h1>              
                                                                           
        <p>Choose your Destiny:</p>                                                                                                                                 
        <a href="/local/signin" class="btn btn-default"><span class="fa fa-user"></span> Local Login</a> 
        <a href="/local/signup" class="btn btn-default"><span class="fa fa-user"></span> Cadastro Local</a>      
</div>                                                           

Agora, se nós executarmos a aplicação, nossa página inicial terá esta cara, mais ou menos:


Em seguida, vamos criar as páginas restantes na pasta views (signin, signup).

Primeiro a signin.hbs:

<div class="col-sm-6 col-sm-offset-3">                                                  
        <h1><span class="fa fa-sign-in"></span> Signin</h1>                             
                                                                                        
        <form action="/local/signin" method="post">                                           
                <div class="form-group">                                                
                        <label>Email</label>                                            
                        <input type="text" class="form-control" name="email">           
                </div>                                                                  
                <div class="form-group">                                                
                        <label>Senha</label>                                            
                        <input type="password" class="form-control" name="password">    
                </div>                                                                  
                                                                                        
                <button type="submit" class="btn btn-warning btn-lg">Acessar</button>   
        </form>                                                                         
                                                                                        
        <hr>                                                                            
        <p><a href="/local/signup">Criar uma conta</a></p>                                    
        <p><a href="/">Home</a></p>                                                     
</div>                                                                                  

Depois, a signup.hbs:

<div class="col-sm-6 col-sm-offset-3">                                                   
        <h1><span class="fa fa-sign-in"></span> Signup</h1>                              
                                                                                         
        <form action="/local/signup" method="post">                                            
                <div class="form-group">                                                 
                        <label>Email</label>                                             
                        <input type="text" class="form-control" name="email">            
                </div>                                                                   
                <div class="form-group">                                                 
                        <label>Senha</label>                                             
                        <input type="password" class="form-control" name="password">     
                </div>                                                                   
                                                                                         
                <button type="submit" class="btn btn-warning btn-lg">Cadastrar</button>  
        </form>                                                                          
                                                                                         
        <hr>                                                                             
        <p><a href="/local/signin">Já possui uma conta?</a></p>                                
        <p><a href="/">Home</a></p>                                                      
</div>                                                                                   

Agora, precisamos adicionar as rotas necessárias no arquivo config/routes.js:

                                                
        '/local/signin':{                             
                controller:'LoginController',   
                action:'signin',                
        },                                      
                                                
        '/local/signup':{                             
                controller:'LoginController',   
                action:'signup',                
        },                                      

Só falta criar o o arquivo controllers/LoginController.js e testar nossa aplicação:

var LoginController={                  
        signin:function(req,res){      
                res.render('signin');  
        },                             
                                       
        signup:function(req,res){      
                res.render('signup');  
        },                             
};                                     
                                       
module.exports=LoginController;        

Agora, se rodarmos nossa aplicação e acessar as urls, encontraremos as seguintes telas:
http://localhost:3000/signup


http://localhost:3000/signin


Passport

Agora sim! Estamos prontos para finalmente utilizar o passport! É necessário porém conhecermos um pouco do processo de autenticação antes de nos aventurarmos de vez. Quando nos logamos em um site, o sistema precisa primeiro verificar se o usuário está cadastrado na base de dados e se suas credenciais são válidas, para só então fazer o redirecionamento para a área privada (acessada somente por usuários logados), ou para a área pública (acessada por todos). Como nossa autenticação pode ser feita de várias formas (lembrem-se de que nossa aplicação suporta várias formas de login), gerenciar cada um desses logins pode ser custoso, principalmente porque temos que lidar com elementos específicos de cada tipo de autenticação como tokens e IDs diferentes, além de juntar tudo isso em uma sessão de usuário única. É aí que o passport pode nos ajudar. Deixaremos todo o trabalho pesado com ele, permitindo que nos concentremos apenas nas regras de negócio.

Pra começar, instalamos o passport, obviamente:

npm install --save passport passport-local bcrypt express-session

Caso você use Windows, recomendo adicionar a flag msvs_version, com o ano do seu visual studio:

npm install --save --msvs_version=2013 passport passport-local bcrypt express-session

Foram instaladas mais 3 bibliotecas nesse comando: passport-local, que é a "estratégia" que vamos utilizar na nossa aplicação. O passport por padrão não possui nenhum módulo de autenticação (ele é apenas a API), por isso que seus módulos foram separados em projetos distintos. O passport-local é o módulo que implementa a autenticação pelo método tradicional (login e senha). O bcrypt é o módulo responsável por encriptar as senhas, e o express-session é o módulo responsável por gerenciar as sessões de usuário.

Dito isso, precisamos agora criar o nosso modelo de usuário. Estamos aqui implementando a arquitetura MVC para nossa aplicação. Criamos as Views, os controllers e agora só falta o modelo. O nosso modelo de usuário é bastante simples, já que ele contém os dados necessários para qualquer autenticação. E de acordo com a necessidade, podemos adicionar mais recursos a ele. Mas por enquanto, ele está de bom tamanho:

var mongoose=require('mongoose');                                                         
var bcrypt=require('bcrypt');                                                             
                                                                                          
/*                                                                                        
 * Criação do schema, que é a estrutura da nosso documento.                               
 * Só lembrando que como o Mongo é um banco no-SQL, não existe o conceito de tabela e sim 
 * de documento.                                                                          
 */                                                                                       
var schema=mongoose.Schema({                                                                  
        auth:{                                                                            
                local:{                                                                   
                        email:String,                                                     
                        password:String,                                                  
                },                                                                        
        },                                                                                
});                                                                                       
                                                                                          
/*                                                                                        
 * Métodos pertencentes ao schema. Devem ser declarados antes da criação do modelo.                                                  
 */                                                                                       
// encripta a senha                                                                       
schema.methods.generateHash=function(password){                                           
        return bcrypt.hashSync(password,bcrypt.genSaltSync(8),null);                      
};                                                                                        
                                                                                          
// Checa se a senha informada é igual a senha do banco                                    
schema.methods.checkPassword=function(password){                                          
        return bcrypt.compareSync(password, this.auth.local.password);                    
};                                                                                        
                                                                                          
// Cria o modelo, o que equivale a criação do documento no banco.                         
var User=mongoose.model('User', schema);                                                  
                                                                                          
module.exports=User;                                                                      

Em seguida, configuramos o arquivo lib/passport.js, que será o núcleo da autenticação. Aqui, estarão as configurações para todas as nossas estratégias. Para começar, vamos apenas criar a estratégia de signup:

var User=require('../models/User');             
var LocalStrategy=require('passport-local');                                                                        
module.exports=function(passport){                        
        /*                                                                       
         * Estas configurações permitem o login consistente e permamente     
         */                                                            
        passport.serializeUser(function(user,done){                                 
                done(null,user.id);                                                       
        });                                                                                                                                       
        passport.deserializeUser(function(id, done){                       
                User.findById(id, function(err, user){       
                        done(err,user);      
                }); 
        });                                                                                                                                                                                                      
        // Setup do módulo passport-local, que é chamado de estratégia. Como o passport suporta vários                                
        // tipos de configurações e abordagens diferentes, é recomendável nomear as estratégias.
        // Para a estratégia local, utilizaremos os nomes 'local-signup' e 'local-signin'
                                                                                                                                      
        passport.use('local-signup', new LocalStrategy({                                          
                usernameField: 'email',                                                           
                passwordField: 'password',                                     
                passReqToCallback: true // permite passar a requisição inteira no callback        
        }, function(req,email,pasword,done){                                                     
                // é recomendável que todo o procedimento seja feito de forma assíncrona          
                process.nextTick(function(){                                                      
                        /*                                                                        
                         * Verificando se o usuário já está cadastrado                            
                         */                                                                       
                        User.findOne({'auth.local.email':email}, function(err,user){              
                                if(err) done(err);                                                
                                // se o usuário existir, exibe uma mensagem de erro.              
                                if(user){                                                         
                                        return done(null, false, req.flash('signupMessage', 'Este e-mail já está sendo utilizado.')); 
                                }else{                                                            
                                        // caso contrário, crie o novo usuário.                   
                                        var newUser=new User();                                   
                                        newUser.auth.local.email=email;                           
                                        newUser.auth.local.password=newUser.generateHash(password); // a senha deve ser encriptada antes da gravação no banco. 
                                        newUser.save(function(err){                               
                                                if(err) throw err;                                
                                                return done(null, newUser, true, req.flash('signupMessage', 'Usuário cadastrado com sucesso.'));               
                                        });                                               
                                }                                                                 
                        });                                                                       
                });                                                                               
        }); // fim da estratégia para o signup.
};                                                     

Agora, só precisamos ligar esta estratégia ao controller e as rotas. Primeiro, Criamos a rota de signup para o método POST no arquivo de rotas:

 'POST /signup':{                      
         controller:'LoginController', 
         action:'cadastroUsuario',     
 },                                    

Depois, criamos a action correspondente no controller de login. Note que chamamos o método passport.authenticate, que utiliza a estratégia definida anteriormente no arquivo passport.js. Não se esqueça de chamar o passport na primeira linha do arquivo (var passport = require('passport'))

 cadastroUsuario:passport.authenticate('local-signup',{                                   
         successRedirect:'/dashboard', // em caso de sucesso, redirecione para esta rota. 
         failureRedirect:'/local/signup', // Em caso de falha, redirecione para esta rota       
         failureFlash:true //allow flash messages                                         
 }),                                                                                      

Agora, só falta ligarmos tudo no arquivo app.js. Antes das rotas, adicione as seguintes linhas:

/****************                                                                    
 *** PASSPORT ***                                                                    
 ****************/                                                                   
require('./lib/passport')(passport);                                                 
app.use(require('express-session')({secret: 'qualquercoisaksadnckjadscdscdscndc'})); 
app.use(passport.initialize());                                                      
app.use(passport.session());                                                         

Porém, existe uma configuração extra que vamos fazer. Quando o login der errado por algum motivo, é exibida uma mensagem flash. Portanto, precisamos configurá-la. Primeiro, precisamos instalar o pacote connect-flash:

npm install --save connect-flash

Depois, no arquivo app.js, adicionamos as seguintes linhas, abaixo da seção das configurações do passport:


/**********************              
 *** FLASH MESSAGES ***              
 **********************/             
app.use(require('connect-flash')()); 

Se tudo deu certo, quando executarmos a operação e cadastrarmos um usuário qualquer, teremos esta tela:


WTF??

Deu certo mas deu errado?? Porréssa?!

Calma, jovem gafanhoto. Ainda não configuramos a rota e nem o login do Dashboard, que é a rota acionada caso o cadastro tenha sido bem sucedido. Vamos resolver isso daqui a pouco. Primeiro, volte para a página de signup e tente cadastrar o mesmo usuário de novo. Possivelmente sua tela será essa:


Provando que o usuário não foi cadastrado, se nós olharmos no banco (eu olhei via robomongo, usando como autenticação as mesmas credenciais da aplicação), obtemos esta tela:


Ou seja: IT'S ALIVE! :D



Login e Dashboard

Agora, vamos adicionar a estratégia para login, que chamaremos de local-signin. Coloque o seguinte código logo abaixo da estratégia do signup:

 passport.use('local-signin', new LocalStrategy({             
         usernameField: 'email',                                                                           passwordField: 'password',                                                                        passReqToCallback: true                                                                   }, function(req,email,password,done){                                                                     User.findOne({'auth.local.email':email}, function(err,user){                                              if(err) return done(err);                                                                         if(!user) return done(null, false, req.flash('signinMessage', 'Usuário não encontrado'));                  
                 if(!user.checkPassword(password)) return done(null, false, req.flash('signinMessage', 'Senha incorreta')); 
                 return done(null, user);                                                                  });                                                                                      }));                                                                                                                       

Depois, no LoginController, adicione:

        login:passport.authenticate('local-signin',{  
                successRedirect:'/dashboard',         
                failureRedirect:'/local/signin',            
                failureFlash: true                    
        }),                                           

E pra finalizar, no arquivo config/routes.js, adicione:

'POST /signin':{                      
        controller:'LoginController', 
        action:'login',               
},                                    

Para testar, acesse http://localhost:3000/signin e faça 3 testes: primeiro, teste acessar com um usuário que não existe. Esta tela deve aparecer:


Depois, teste com a senha errada. A seguinte tela aparecerá:

E, é claro que você já deve ter adivinhado o que vai acontecer se você digitar tudo corretamente. :D. Portanto, para resolver esse problema, vamos arrumar logo essa página do dashboard. Primeiro, vamos criar a view (views/dashboard.hbs):

<div class="container">                                                        
        <div class="page-header text-center">                                                 
                <h1><span class="fa fa-anchor"></span> Dashboard</h1>                             
                                                                            
                <a href="/signout" class="btn btn-default btn-sm">Logout</a>                         
                <div class="row">                                                                     
                                                             
                     <!--Local information-->                                                     
                        <div class="col-sm-6">                                               
                                <div class="well">                                             
                                        <h3><span class="fa fa-user"></span> Local</h3>         
                                        <p>                                                
                                                <strong>id</strong>: {{user._id}}<br/>           
                                                <strong>email</strong>: {{user.auth.local.email}}<br/>
                                                <strong>password</strong>: {{user.auth.local.password}}<br/>  
                                        </p>                                                   
                                </div>                                                       
                        </div>                                                                
                                    
                </div>                                                                            
        </div>                                                                                    
</div>

Depois, criamos um controller exclusivo para o dashboard, já que é uma outra área da aplicação:

var DashboardController={                                                           
        index:function(req,res){                                                    
                // o passport automaticamente coloca na requisição o usuário logado 
                res.render('dashboard', {user: req.user});                          
        }                                                                           
};                                                                                  
                                                                                    
module.exports=DashboardController;                                                 

Agora, só falta adicionarmos a rota para o dashboard:

        '/dashboard':{                            
                controller:'DashboardController', 
                action:'index',                   
        },                                        

Agora sim, inicie a aplicação e logue normalmente. A seguinte tela aparecerá para o dashboard:


Policies e Logout

Tá tudo muito bom, mas ainda temos 2 problemas pra resolver. O primeiro, é mais simples: O botão de logout não está funcionando. O segundo é um pouco mais complicado: Podemos acessar o dashboard mesmo sem nos logarmos no sistema. Mas vamos resolver primeiro o do logout. Basta criarmos a action no controller de login...

 signout: function(req,res){                            
         req.logout(); // já fornecida pelo passport    
         res.redirect('/'); // redireciona para a raiz. 
 },                                                     

... e criar a rota correspondente.

 '/signout':{                          
         controller:'LoginController', 
         action:'signout',             
 },                                    

Agora tudo deve funcionar legal. O último problema é com relação ao acesso privado ao dashboard. Precisamos criar algum mecanismo que impeça o acesso não autorizado. Felizmente, o express suporta diversos middlewares e funções encadeadas. Assim, podemos criar um framework de policies que serão disparadas antes da função principal da rota. A primeira coisa é criar uma pasta chamada policies dentro da pasta config. Depois, criamos um arquivo chamado isAuthenticated.js dentro desta nova pasta com o seguinte conteúdo:

module.exports=function(req,res,next){                                                                        
        if(req.isAuthenticated()){                                                                            
                // avança para a próxima função da rota, que será a função principal.                         
                return next();                                                                                
        }else{                                                                                                
                // avança para a próxima ROTA. Como não existe uma rota encadeada nem um redirect,            
                // a próxima rota é a 404 (page not found), que serve para os nossos propósitos de bloqueio.  
                return next('route');                                                                         
        }                                                                                                     
};                                                                                                            

Depois, adicionamos o nome do arquivo criado na rota do dashboard, do arquivo config/routes,js:

'/dashboard':{                            
        controller:'DashboardController', 
        action:'index',                   
        policy:'isAuthenticated',         
},                                        

A ideia aqui é carregar o nome do arquivo de políticas (policies) antes da função principal da rota ser chamada. Para isso, modificaremos o código presente no arquivo lib/route_loader.js. Modificamos somente a parte final da função, que vai ficar assim:

...                                                                                                
var controller_filename=route.controller;                                                         var action_name=route.action;                                                                     var policy_name=route.policy;                                                                                        
// caso a policy não tenha sido declarada                                                         var policy=undefined;                                                                             if(!policy_name){                                                                                         console.log("[WARNING] Policy não declarada para esta rota. Utilizando a policy padrão (sem restrições).");  
        policy=function(req,res,next){ return next() };                                           }else{                                                                                                    try{                                                                                                      policy=require(policies_folder+'/'+policy_name+'.js');                                    }catch(err){                                                                                              throw new Error("Arquivo não encontrado: "+policy_name);                                  }                                                                                         }                                                                                                                    
// carregando a rota                                                                              var controller=require(controllers_folder+"/"+controller_filename+".js");                         app[method](url, policy, controller[action_name]);                                                  
...                 

Agora, quando a aplicação é executada, só será permitido o acesso ao dashboard para os usuários logados. Caso sua permissão seja negada, será exibida a página 404. Aproveite para adicionar esta mesma policy na rota de signout, para prevenir problemas.

Com isso, encerramos a etapa de arquitetura e organização do código para uma aplicação node.js. Nos posts seguintes (que serão mais curtos), serão apenas complementos ao código atual, inserindo logins pelo Facebook, Twitter e demais formas de autenticação, explorando todas as possibilidades do passport.

Inté.


Demais posts desta série:

Nenhum comentário:

Postar um comentário