quarta-feira, 21 de janeiro de 2015

Exemplo de Autenticação em Node.JS - Autenticação via Facebook

Demais posts desta série:


Olá a todos!

Finalizando nossa série de posts sobre autenticação utilizando Node.JS, vamos adicionar à nossa aplicação o login via Facebook. O procedimento é o mesmo adotado para as autenticações externas anteriores (gerar as chaves e o ID do cliente, atualizar o config.js, atualizar o passport.js, etc). Porém, se você pensa que só é possível adicionar login externo somente com os serviços mostrados nesta série de posts, se engana. Neste link, estão relacionados todos os módulos do passport capazes de realizar autenticação externa. Lembre-se de olhar a documentação de cada módulo para ter certeza do que fazer para adicionar a sua aplicação.

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

Como é de praxe, vamos agora gerar as nossas chaves. Acesse a página de desenvolvedores do Facebook, no menu superior, clique em My Apps, e se registre como desenvolvedor. Ao final do processo, esta página aparecerá:



Clique em website. Em seguida, digite o nome da nossa aplicação e clique em "Create New Facebook App ID". No pop-up, escolha a categoria (pode ser a primeira) e clique no botão azul para finalizar o processo. Após o carregamento da página do app, clique em "start over", no canto superior esquerdo para acessar o dashboard da nossa aplicação. De cara, você já encontra o App ID e o App Secret. Copie os dois para o arquivo config.js.

facebook:{
    consumer_key:'seu app id aqui',
    consumer_secret:'seu app secret aqui',
    callback_url:'http://127.0.0.1:3000/facebook/callback',
},

Depois, volte para o facebook, pois temos mais uma configuração para fazer.  Por padrão, o Facebook não aceita que nenhuma URL se conecte com ele via OAuth. Assim, precisamos informar a ele qual URL ele deve aceitar. Em Configurações > Aba advanced, na parte inferior da página tem um campo chamado "Valid OAuth redirect URIs". Neste campo, coloque a URL da nossa aplicação (http://127.0.0.1:3000). Salve tudo e, agora sim, podemos voltar para a nossa aplicação.

Próxima fase: atualizar o modelo (arquivo models/User.js):

    facebook:{
            id:String,
            token:String,
            email:String,
            name:String,
        },

Depois, instalamos o módulo do passport que se conecta ao facebook e configuramos a estratégia no arquivo lib/passport.js

npm install --save passport-facebook

...
var FacebookStrategy=require('passport-facebook').Strategy;
...

    /*************************
     ***** FACEBOOK AUTH *****
     *************************/
     
     passport.use(new FacebookStrategy({
        clientID: config.secret.facebook.consumer_key,
        clientSecret: config.secret.facebook.consumer_secret,
        callbackURL: config.secret.facebook.callback_url,
        passReqToCallback: true,
    }, function(req,token, refreshToken, profile, done){
        process.nextTick(function(){
            if(!req.user){
                User.findOne({'auth.facebook.id':profile.id}, function(err, user){
                    if(err) return done(err);
                    if(user){
                        return done(null, user);
                    }else{
                        var newUser=new User();
                        newUser.auth.facebook.id=profile.id;
                        newUser.auth.facebook.token=token;
                        newUser.auth.facebook.email=profile.emails[0].value;
                        newUser.auth.facebook.name=profile.name.givenName+' '+profile.name.familyName;

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

                            return done(null, newUser)
                        });
                    }
                });
            }else{
                var _user=req.user;
                
                _user.auth.facebook.id=profile.id;
                _user.auth.facebook.token=token;
                _user.auth.facebook.email=profile.emails[0].value;
                _user.auth.facebook.name=profile.name.givenName+' '+profile.name.familyName;
                
                _user.save(function(err){
                    if(err) throw err;
                    
                    return done(null, _user);
                });
            }            
        });
    }));

Em seguida, as rotas (arquivo config/routes.js):

'/facebook/connect':{
        controller:'LoginController',
        action:'facebookConnect',
    },
    
    '/facebook/callback':{
        controller:'LoginController',
        action:'facebookCallback',
    },
    
    '/facebook/link':{
        controller:'LoginController',
        action:'facebookLink',
        policy:'isAuthenticated',
    },
    
    '/facebook/link/callback':{
        controller:'LoginController',
        action:'facebookLinkCallback',
        policy:'isAuthenticated',
    },
    
    '/facebook/unlink':{
        controller:'LoginController',
        action:'facebookUnlink',
        policy:'isAuthenticated',
    },

Para finalizar, atualizamos as views. Primeiro o dashboard.hbs:

<!--Facebook Information-->
            <div class="col-sm-6">
                <div class="well">
                    <h3 class="text-primary"><span class="fa fa-facebook"></span> Facebook</h3>
                    {{#if user.auth.facebook.id}}
                        <p>
                            <strong>id</strong>: {{user.auth.facebook.id}}<br/>
                            <strong>token</strong>: {{user.auth.facebook.token}}<br/>
                            <strong>email</strong>: {{user.auth.facebook.email}}<br/>
                            <strong>name</strong>: {{user.auth.facebook.name}}<br/>
                        </p>
                        <a href="/facebook/unlink" class="btn btn-primary">Desassociar conta</a>
                    {{else}}
                        <a href="/facebook/link" class="btn btn-primary">Conectar ao Facebook</a>
                    {{/if}}
                </div>
            </div>

Depois, o index.hbs:

<a href="/facebook/connect" class="btn btn-primary"><span class="fa fa-facebook"></span> Facebook</a>

Quando executarmos a aplicação, tudo deve estar funcionando devidamente.

Index

Dashboard

Com isso, encerramos a nossa série sobre autenticação utilizando o Node.JS. Lembrando que todo o código desenvolvido neste tutorial está disponível aqui, e qualquer dúvida referente ao desenvolvimento, podem postar nos comentários que responderei o máximo possível.

Inté.

Demais posts desta série:

Exemplo de Autenticação em Node.JS - Autenticação via Google

Demais posts desta série:


Olá a todos,

Continuando nossa série de posts sobre autenticação utilizando Node.JS, vamos adicionar à nossa aplicação o login via Google. Este post é naturalmente mais curto que os demais, pois se trata de atualizações de código para um novo login. Já faremos as atualizações para suportar os múltiplos logins e união de contas.

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

Primeiro, precisaremos do nosso ID e token de acesso. Acessando a página do Google Cloud Platform, e fazendo o seu login do google, chegamos a seguinte página:


A criação do projeto não possui mistérios. Após a criação, a página será redirecionada para a página do projeto. No lado esquerdo, acesse APIs e Autenticação > Credenciais. Em seguida, na área destinada ao OAuth, clique em "Criar um novo ID de cliente". No pop-up que se abrirá, escolha a opção "Aplicativo da Web", e clique em "configurar tela de consentimento". A tela seguinte, é para configurar como será a tela de autorização de login. Só coloquei as opções padrão (nome do projeto, e-mail e URL).

Após salvar as configurações da tela de autorização, voltaremos para o pop-up para configurarmos as URLs de origem de de callback. Para este login, utilizaremos as seguintes URLs/rotas:

  • /google/connect (URL de redirecionamento)
  • /google/callback (URL de callback da tela do google para nossa aplicação)
  • /google/link (URL de união de logins do google a uma conta já existente)
  • /google/unlink (URL de remoção do login do google da nossa conta).
A URL de origem, para o google é a URL principal da aplicação, e a URL de callback especificamos acima. Quando tudo estiver preenchido, o pop-up deve ficar assim:


Após clicar no botão azul, copie o Client ID e o Client Secret e coloque no arquivo config/config.js, como espeficificado:

google:{
    consumer_key: 'seu_client_id_aqui',
    consumer_secret: 'sua_chave_secreta_aqui',
    callback_url:'http://127.0.0.1:3000/google/callback',
},

Em seguida, vamos fazer as alterações de praxe. Primeiro, instalamos o módulo do passport para comunicação do google:

npm install --save passport-google-oauth

Em seguida, atualizamos o nosso modelo (models/User.js)....

google:{
    id:String,
    token:String,
    email:String,
    name:String,
},

.... e o arquivo lib/passport.js com a nossa nova estratégia:

...

var GoogleStrategy=require('passport-google-oauth').OAuth2Strategy;

...

passport.use(new GoogleStrategy({
        clientID:config.secret.google.consumer_key,
        clientSecret:config.secret.google.consumer_secret,
        callbackURL:config.secet.google.callback_url,
        passReqToCallback:true,
    }, function(req, token, refreshToken, profile, done){
        process.nextTick(function(){
            if(!req.user){
                User.findOne({'auth.google.id': profile.id}, function(err,user){
                    if(err) return done(err);
                    
                    if(user){
                        return done(null, user);
                    }else{
                        var newUser=new User();
                        newUser.auth.google.id=profile.id;
                        newUser.auth.google.token=token;
                        newUser.auth.google.email=profile.emails[0].value;
                        newUser.auth.google.name=profile.displayName;
                        
                        newUser.save(function(err){
                            if(err) return done(err);
                            
                            return done(null, newUser);
                        });
                    }
                });
            }else{
                var _user=req.user;
                
                _user.auth.google.id=profile.id;
                _user.auth.google.token=token;
                _user.auth.google.email=profile.emails[0].value;
                _user.auth.google.name=profile.displayName;
                
                _user.save(function(err){
                    if(err) return done(err);
                    
                    return done(null, _user);
                });
            }
        });
    }));

Em seguida, configuramos as rotas...

    /* GOOGLE AUTH */
    
    '/google/connect':{
        controller:'LoginController',
        action:'googleConnect',
    },
    
    '/google/callback':{
        controller:'LoginController',
        action:'googleCallback',
    },
    
    '/google/link':{
        controller:'LoginController',
        action:'googleLink',
        policy:'isAuthenticated',
    },
    
    '/google/link/callback':{
        controller:'LoginController',
        action:'googleLinkCallback',
        policy:'isAuthenticated',
    },
    
    '/google/unlink':{
        controller:'LoginController',
        action:'googleUnlink',
        policy:'isAuthenticated',
    },

... e o controller:

    googleConnect:passport.authenticate('google'),
    
    googleCallback: passport.authenticate('google'{
        successRedirect:'/dashboard',
        failureRedirect:'/',
    }),
    
    googleLink: passport.authorize('google', {scope:['profile', 'email']}),
    
    googleLinkCallback: passport.authorize('google', {
        successRedirect:'/dashboard',
        failureRedirect:'/',
    }),
    
    googleUnlink: function(req,res){
        var user=req.user;
        user.auth.google.token=undefined;
        user.auth.google.id=undefined;
        user.save(function(err){
            if(err) throw err;
            
            res.redirect('/dashboard');
        });
    },

Para finalizar, ajustamos o arquivo views/dashboard.hbs...

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

... E a página principal:

<a href="/google/connect" class="btn btn-danger"><span class="fa fa-google-plus"></span> Google</a>

O resultado final pode ser visto nas imagens abaixo:

Página inicial
Dashboard


Com isso, finalizamos a autenticação para o Google. No próximo post (último da série), veremos como realizar a autenticação via Facebook.

Inté.


Demais posts desta série:

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:

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: