quarta-feira, 21 de janeiro de 2015

Exemplo de Autenticação em Node.JS - Autenticação via Twitter e unindo contas.

Demais posts desta série:

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 aqui, e este tutorial foi baseado neste  excelente 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 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 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, 
  • /twitter/callback: URL que o Twitter vai usar para voltar à nossa aplicação.
No arquivo config/routes.js, temos: 


 '/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:

 <a href="/twitter/connect" class="btn btn-info"><span class="fa fa-twitter"></span> Twitter</a>

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

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

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:

<div class="col-sm-6 col-sm-offset-3">
    <h1><span class="fa fa-sign-in"></span> Add Account</h1>

    <form action="/local/link" 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>
</div>


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

<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>
                    {{#if user.auth.local.email}}
                        <p>
                            <strong>id</strong>: {{user._id}}<br/>
                            <strong>email</strong>: {{user.auth.local.email}}<br/>
                            <strong>password</strong>: {{user.auth.local.password}}<br/> 
                        </p>
                        <a href="/local/unlink" class="btn btn-default">Desassociar conta</a>
                    {{else}}
                        <a href="/local/link" class="btn btn-default">Cadastrar conta local</a>                    
                    {{/if}}
                </div>
            </div>

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

        </div>
    </div>
</div>


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:

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

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:

Nenhum comentário:

Postar um comentário