Exemplo de Autenticação em Node.JS - Autenticação via Twitter e unindo contas.
Autor: Giovane Boaviagem | Publicado: 2015-01-21T13:23:00-08:00

Demais posts desta série:

Setup Local
Passport e Autenticação Local
Autenticação via Google
Autenticação via Facebook


Olá a todos,

Continuando nossa série de posts sobre autenticação com Node.JS, vamos expandir um pouco nossas possibilidades de autenticação adicionando uma autenticação externa. Isso é bom porque adiciona uma camada a mais de segurança ao sistema, uma vez que a responsabilidade de autenticação não está mais com você, facilita para o usuário, já que ele só precisa logar uma vez no sistema (A não ser que ele saia do sistema externo), além de ser aumentar a confiança do sistema, já que o usuário vai saber que suas credenciais não vão estar armazenadas no sistema local.

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

Algo em comum a este tipo de autenticação externa é o protocolo, chamado OAuth. Ele permite que uma aplicação se autentique "em nome do usuário", mesmo sem conhecer as credenciais. Isto é possível porque existem credenciais que registram a "permissão" do usuário para que as informações sejam trocadas. E esta "permissão" independe da senha ou outros tipos de credenciais.

Sendo assim, precisamos criar, na aplicação externa, as nossas credenciais, que vão nos permitir acessar a API da aplicação externa e a mágica acontecer. Primeiro, vamos na página de apps de desenvolvimento do Twitter&nbsp;e após entrarmos, teremos esta página:




Após clicarmos em Create New App, preenchemos as informações necessárias do nosso app. No meu caso, as partes mais importantes ficaram preenchidas assim:

Nome: node-auth-example
Callback URL: http://127.0.0.1:3000/login/twitter/callback


Depois, quando clicarmos no botão de confirmar, a app é criada. Na aba Settings, marque a opção "Allow this application to be used to&nbsp;Sign in with Twitter" e e salve as mudanças. Na aba "Keys and Access Tokens", copie o Consumer Key e o Consumer Secret para o arquivo config/config.js no seguinte formato:


 secret:{                                          
         cookie:'lquercoisaksadnckjadscdscdscndc', 
         twitter:{                                 
                 consumer_key:'',                  
                 consumer_secret:'',               
         },                                        
 },                                                




Percebam que coloquei também o cookie secret nas configurações. É opcional, claro.

Depois, vamos atualizar o nosso modelo de Usuário para armazenar as informações do twitter:

auth:{                              
        local:{                     
                email:String,       
                password:String,    
        },                          
        twitter:{                   
                id:String,          
                token:String,       
                displayName:String, 
                username:String,    
        },                          
},                                  


Em seguida, as rotas. Vamos precisar de 2 rotas:


/twitter/connect: Acessar o twitter, obter as informações do usuário e fazer o login,&nbsp;
/twitter/callback: URL que o Twitter vai usar para voltar à nossa aplicação.


No arquivo config/routes.js, temos:&nbsp;



 '/twitter/connect':{                     
         controller:'LoginController',  
         action:'twitterConnect',              
 },                                     
                                        
 '/twitter/callback':{            
         controller:'LoginController',  
         action:'twitterCallback',      
 },                                     


Perceba que a URL de callback deve bater com a URL de callback que você cadastrou no Twitter app.

Depois, vamos instalar o módulo do passport de conexão com o Twitter...

npm install --save passport-twitter


... e atualizar o arquivo passport.js:

passport.use(new TwitterStrategy({                                                    
        consumerKey: config.secret.twitter.consumer_key,                              
        consumerSecret: config.secret.twitter.consumer_secret,                        
        callbackURL: config.secret.twitter.callback_url,                              
}, function(token, tokenSecret, profile, done){                                       
        process.nextTick(function(){                                                  
                console.log(profile);                                                 
                User.findOne({'auth.twitter.id':profile.id}, function(err, user){     
                        if(err) return done(err);                                     
                                                                                      
                        if(user){                                                     
                                return done(null, user);                              
                        }else{                                                        
                                var newUser=new User();                               
                                newUser.auth.twitter.id=profile.id;                   
                                newUser.auth.twitter.token=token;             
                                newUser.auth.twitter.username=profile.username;       
                                newUser.auth.twitter.displayName=profile.displayName; 
                                                                                      
                                newUser.save(function(err){                           
                                        if(err) throw err;                            
                                                                                      
                                        return done(null, newUser)                    
                                });                                                   
                        }                                                             
                });                                                                   
        });                                                                           
}));                                                                                  


Perceba que o código é muito parecido com a estratégia de signup local.

Depois, atualizaremos o controller de login:

twitterConnect: passport.authenticate('twitter'),   
                                                    
twitterCallback: passport.authenticate('twitter', { 
        successRedirect: '/dashboard',              
        failureRedirect: '/',                       
}),                                                 



Para finalizar, vamos atualizar as views para que a autenticação fique completa:

No arquivo views/index.hbs, logo abaixo do link de signup, adicione a seguinte linha:

 &lt;a href="/twitter/connect" class="btn btn-info"&gt;&lt;span class="fa fa-twitter"&gt;&lt;/span&gt; Twitter&lt;/a&gt;


No arquivo views\dashboard.hbs, adicione o seguinte bloco, após a div das informações locais:

&lt;!--Twitter Information--&gt;                                                                    
&lt;div class="col-sm-6"&gt;                                                                        
        &lt;div class="well"&gt;                                                                    
                &lt;h3 class="text-info"&gt;&lt;span class="fa fa-twitter"&gt;&lt;/span&gt; Twitter&lt;/h3&gt;        
                &lt;p&gt;                                                                           
                        &lt;strong&gt;id&lt;/strong&gt;: {{user.auth.twitter.id}}&lt;br/&gt;                    
                        &lt;strong&gt;token&lt;/strong&gt;: {{user.auth.twitter.token}}&lt;br/&gt;              
                        &lt;strong&gt;username&lt;/strong&gt;: {{user.auth.twitter.username}}&lt;br/&gt;        
                        &lt;strong&gt;displayName&lt;/strong&gt;: {{user.auth.twitter.displayName}}&lt;br/&gt;  
                &lt;/p&gt;                                                                          
        &lt;/div&gt;                                                                                
&lt;/div&gt;                                                                                        


Agora, basta executar a aplicação. Porém, o código não vai funcionar se digitarmos localhost:3000. Isso porque o Twitter não reconhece a palavra localhost. Então daqui por diante, vamos usar a url por ip para os nossos testes (http://127.0.0.1:3000)



Após clicar no botão do twitter, você será redirecionado para a página de autorização do aplicativo. Após confirmar, você será redirecionado para o dashboard, que vai ficar assim:



Perceba que a área local está com o email e senha em branco. Isso porque a conta do twitter e a conta local estão separadas (o ID local na verdade é o ID do MongoDB do registro dos dados do Twitter). Deste modo, no banco, temos armazenados 2 usuários:

Usuário A, que é o usuário cadastrado pela autenticação local;
Usuário B, que é o usuário cadastrado pela autenticação via Twitter.

Na figura abaixo, vemos como está a situação atual do nosso banco:





De fato, nós nunca fizemos nenhum mecanismo de associação de contas. Assim, precisamos criar um mecanismo para associar todas as contas em uma só.

Primeiro, vamos preparar o terreno para a autenticação local (Depois expandimos para as próximas) criando 2 novas rotas:

/local/link (get e post)
/local/unlink


No arquivo config/routes.js, adicionamos:


'/local/link':{                        
        controller:'LoginController',  
        action:'localLink',            
        policy:'isAuthenticated',      
},                                     
                                       
'POST /local/link':{                   
        controller:'LoginController',  
        action:'linkAccount',          
        policy:'isAuthenticated',      
},                                     
                                       
'/local/unlink':{                      
        controller:'LoginController',  
        action:'unlinkAccount',        
        policy:'isAuthenticated',      
},                                                         




Em seguida, modificamos o arquivo lib/passport.js, para atualizar a nossa estratégia e contemplar as ações de link e unlink. No final, todas as estratégias vão ter a mesma alteração, que é verificar se o usuário está logado. Caso esteja, basta atualizar o registro com os dados novos de autenticação. Em caso contrário, segue o que estava antes. Aqui será mostrado apenas o código final da estratégia local do signup, que é a única das 2 estratégias locais em que esta alteração será necessária.

passport.use('local-signup', new LocalStrategy({
        usernameField: 'email',
        passwordField: 'password',
        passReqToCallback: true
        }, function(req,email,password,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);

                if(!req.user){
                    // se o usuário existir, exibe uma mensagem de erro.
                    if(user){
                        return done(null, false, req.flash('signupMessage', 'Usuário já cadastrado.'));
                    }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, req.flash('signupMessage', 'Usuário cadastrado com sucesso.'));
                        });
                    }
                }else{
                    //atualizar o usuário com os dados novos.
                    var _user=req.user;
                    _user.auth.local.email=email;
                    _user.auth.local.password=_user.generateHash(password);

                    _user.save(function(err){
                        if(err) throw err;

                        return done(null, _user, req.flash('signupMessage', 'Usuário atualizado.'));
                    });
                }
            });
        });
    })); // fim da estratégia para o signup.


Agora, atualizamos o arquivo controllers/LoginController para adicionar nossas novas actions (link e unlink):

    localLink:function(req,res){
        res.render('addAccount');
    },
    
    linkAccount:passport.authenticate('local-signup',{
        successRedirect:'/dashboard',
        failureRedirect:'/local/link',
    }),
    
    unlinkAccount:function(req,res){
        var _user=req.user;
        _user.auth.local.email=undefined;
        _user.save(function(err){
            if(err) throw err;
            
            res.redirect('/dashboard');
        });
    },


Como vocês puderam perceber, a view renderizada para a action localLink tem o nome 'addAccount'. Portanto, vamos criar esta view, que é praticamente uma cópia da view de signup:

&lt;div class="col-sm-6 col-sm-offset-3"&gt;
    &lt;h1&gt;&lt;span class="fa fa-sign-in"&gt;&lt;/span&gt; Add Account&lt;/h1&gt;

    &lt;form action="/local/link" method="post"&gt;
        &lt;div class="form-group"&gt;
            &lt;label&gt;Email&lt;/label&gt;
            &lt;input type="text" class="form-control" name="email"&gt;
        &lt;/div&gt;
        &lt;div class="form-group"&gt;
            &lt;label&gt;Senha&lt;/label&gt;
            &lt;input type="password" class="form-control" name="password"&gt;
        &lt;/div&gt;
        
        &lt;button type="submit" class="btn btn-warning btn-lg"&gt;Cadastrar&lt;/button&gt;
    &lt;/form&gt;
&lt;/div&gt;



Para finalizar, alteramos a view de dashboard para refletir as nossas mudanças.

&lt;div class="container"&gt;
    &lt;div class="page-header text-center"&gt;
        &lt;h1&gt;&lt;span class="fa fa-anchor"&gt;&lt;/span&gt; Dashboard&lt;/h1&gt;

        &lt;a href="/signout" class="btn btn-default btn-sm"&gt;Logout&lt;/a&gt;

        &lt;div class="row"&gt;
        
            &lt;!--Local information--&gt;    
            &lt;div class="col-sm-6"&gt;
                &lt;div class="well"&gt;
                    &lt;h3&gt;&lt;span class="fa fa-user"&gt;&lt;/span&gt; Local&lt;/h3&gt;
                    {{#if user.auth.local.email}}
                        &lt;p&gt;
                            &lt;strong&gt;id&lt;/strong&gt;: {{user._id}}&lt;br/&gt;
                            &lt;strong&gt;email&lt;/strong&gt;: {{user.auth.local.email}}&lt;br/&gt;
                            &lt;strong&gt;password&lt;/strong&gt;: {{user.auth.local.password}}&lt;br/&gt; 
                        &lt;/p&gt;
                        &lt;a href="/local/unlink" class="btn btn-default"&gt;Desassociar conta&lt;/a&gt;
                    {{else}}
                        &lt;a href="/local/link" class="btn btn-default"&gt;Cadastrar conta local&lt;/a&gt;                    
                    {{/if}}
                &lt;/div&gt;
            &lt;/div&gt;

            &lt;!--Twitter Information--&gt;
            &lt;div class="col-sm-6"&gt;
                &lt;div class="well"&gt;
                    &lt;h3 class="text-info"&gt;&lt;span class="fa fa-twitter"&gt;&lt;/span&gt; Twitter&lt;/h3&gt;
                    &lt;p&gt;
                        &lt;strong&gt;id&lt;/strong&gt;: {{user.auth.twitter.id}}&lt;br/&gt;
                        &lt;strong&gt;token&lt;/strong&gt;: {{user.auth.twitter.token}}&lt;br/&gt;
                        &lt;strong&gt;username&lt;/strong&gt;: {{user.auth.twitter.username}}&lt;br/&gt;
                        &lt;strong&gt;displayName&lt;/strong&gt;: {{user.auth.twitter.displayName}}&lt;br/&gt;
                    &lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            
            

        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



Agora, quando executarmos nossa aplicação e nos logarmos pelo Usuário B (via Twitter), a tela ficará parecida com essa:







Ou seja, a view verifica se o usuário possui autenticação local. Caso não tenha, o botão de cadastrar autenticação local aparece. Após apertar no botão e cadastrar o email e senha (cadastre um e-mail diferente, para que não ocorram erros), a tela ficará assim:




Se nós clicarmos em Desassociar Conta, tanto o e-mail quanto a senha serão apagados, e a aplicação volta para a tela anterior.

Para associarmos com a conta do Twitter, o processo é semelhante. Primeiramente as rotas:

    '/twitter/link':{
        controller:'LoginController',
        action:'linkTwitter',
        policy:'isAuthenticated',
    },
    
    '/twitter/link/callback':{
        controller:'LoginController',
        action:'linkTwitterCallback',
        policy:'isAuthenticated',
    },
    
    '/twitter/unlink':{
        controller:'LoginController',
        action:'unlinkTwitter',
        policy:'isAuthenticated',
    },


Depois, a atualização do arquivo lib/passport.js

passport.use(new TwitterStrategy({
        consumerKey: config.secret.twitter.consumer_key,
        consumerSecret: config.secret.twitter.consumer_secret,
        callbackURL: config.secret.twitter.callback_url,
    }, function(token, tokenSecret, profile, done){
        process.nextTick(function(){
            console.log(profile);
            User.findOne({'auth.twitter.id':profile.id}, function(err, user){
                if(err) return done(err);
                
                if(!req.user){
                    if(user){
                        return done(null, user);
                    }else{
                        var newUser=new User();
                        newUser.auth.twitter.id=profile.id;
                        newUser.auth.twitter.token=token;
                        newUser.auth.twitter.username=profile.username;
                        newUser.auth.twitter.displayName=profile.displayName;

                        newUser.save(function(err){
                            if(err) throw err;

                            return done(null, newUser)
                        });
                    }                
                }else{
                    var _user=req.user;
                    
                    _user.auth.twitter.id=profile.id;
                    _user.auth.twitter.token=token;
                    _user.auth.twitter.username=profile.username;
                    _user.auth.twitter.displayName=profile.displayName;
                    
                    _user.save(function(err){
                        if(err) throw err;
                        
                        return done(null, _user);
                    });
                }
            });
        });
    }));


Agora a principal diferença: No controller, utilizaremos a função authorize, que passa o usuário logado para a estratégia. No final, o código ficará assim:

    linkTwitter:passport.authorize('twitter', {scope: 'email'}),
    
    linkTwitterCallback:passport.authorize('twitter', {
        successRedirect:'/dashboard',
        failureRedirect:'/',
    }),


Agora, só falta alterar o dashboard:

&lt;!--Twitter Information--&gt;
            &lt;div class="col-sm-6"&gt;
                &lt;div class="well"&gt;
                    &lt;h3 class="text-info"&gt;&lt;span class="fa fa-twitter"&gt;&lt;/span&gt; Twitter&lt;/h3&gt;
                    {{#if user.auth.twitter.id}}
                        &lt;p&gt;
                            &lt;strong&gt;id&lt;/strong&gt;: {{user.auth.twitter.id}}&lt;br/&gt;
                            &lt;strong&gt;token&lt;/strong&gt;: {{user.auth.twitter.token}}&lt;br/&gt;
                            &lt;strong&gt;username&lt;/strong&gt;: {{user.auth.twitter.username}}&lt;br/&gt;
                            &lt;strong&gt;displayName&lt;/strong&gt;: {{user.auth.twitter.displayName}}&lt;br/&gt;
                        &lt;/p&gt;
                        &lt;a href="/twitter/unlink" class="btn btn-info"&gt;Desconectar&lt;/a&gt;
                    {{else}}
                        &lt;a href="/twitter/link" class="btn btn-info"&gt;Conectar ao Twitter&lt;/a&gt;
                    {{/if}}
                &lt;/div&gt;
            &lt;/div&gt;


Quando iniciamos a aplicação e nos logamos com a autenticação local, percebemos esta tela:




Quando nos conectarmos pelo Twitter, ficará assim:




Agora, podemos remover um dos usuários, que a aplicação já suporta múltiplas contas. Os próximos posts, continuaremos com a atualização do código para adicionar as demais contas (Facebook e Google)

Inté.


Demais posts desta série:

Setup Local
Passport e Autenticação Local
Autenticação via Google
Autenticação via Facebook