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: