Chris on Code
This tutorial is out of date and no longer maintained.
Note: This article is part of our Easy Node Authentication series.
This will be the final article in our Easy Node Authentication Series. We will be using all of the previous articles together.
Note: Edit 11/18/2017: Updated to reflect Facebook API changes.
This article will combine all the different Node Passport Strategies so that a user will be able to have one account and link all their social networks together.
There are many changes that need to take place from the previous articles to accomplish this. Here are the main cases we have to account for when moving from authenticating with only 1 account versus multiple accounts.
We’ll be going through each of these scenarios and updating our previous code to account for them.
We’ll be working with the Local Strategy and the Facebook Strategy to demonstrate linking accounts. The tactics used for the Facebook Strategy will carry over to Twitter and Google.
In order to add linking accounts to our application, we will need to:
When looking at the way we set up our user model, we deliberately set up all the user accounts to be set up within their own object. This ensures that we can link and unlink different accounts as our user sees fit. Notice that the social accounts will use token
and id
while our local account will use email
and password
.
...
var userSchema = mongoose.Schema({
local : {
email : String,
password : String,
},
facebook : {
id : String,
token : String,
email : String,
name : String
},
twitter : {
id : String,
token : String,
displayName : String,
username : String
},
google : {
id : String,
token : String,
email : String,
name : String
}
});
...
We have also added in email
, name
, displayName
, and username
for some accounts just to show that we can pull that information from the respective social connection.
Once a user has linked all their accounts together, they will have one user account in our database, with all of these fields full.
When we originally made these Strategies, we would use passport.authenticate
. This is what we should be using upon first authentication of our user. But what do we do if they are already logged in? They will be logged in and their user stored in session when we want to link them to their current account.
Luckily, Passport provides a way to “connect” a user’s account. They provide passport.authorize
for users that are already authenticated. To read more on the usage, visit the Passport authorize docs.
We will update our routes to handle the authorization first, and then we’ll update our Passport Strategies to handle the authorization.
Let’s create our routes first so that we can see how we link everything together. In the past articles, we created our routes for authentication. Let’s create a second set of routes for authorization. Once we’ve done that, we’ll change our Strategy to accommodate the new scenarios.
Our old routes will be commented to make a cleaner file.
module.exports = function(app, passport) {
// normal routes ===============================================================
// show the home page (will also have our login links)
// PROFILE SECTION =========================
// LOGOUT ==============================
// =============================================================================
// AUTHENTICATE (FIRST LOGIN) ==================================================
// =============================================================================
// locally --------------------------------
// LOGIN ===============================
// show the login form
// process the login form
// SIGNUP =================================
// show the signup form
// process the signup form
// facebook -------------------------------
// send to facebook to do the authentication
app.get('/auth/facebook', passport.authenticate('facebook', { scope : 'email' }));
// handle the callback after facebook has authenticated the user
app.get('/auth/facebook/callback',
passport.authenticate('facebook', {
successRedirect : '/profile',
failureRedirect : '/'
}));
// twitter --------------------------------
// send to twitter to do the authentication
// handle the callback after twitter has authenticated the user
// google ---------------------------------
// send to google to do the authentication
// the callback after google has authenticated the user
// =============================================================================
// AUTHORIZE (ALREADY LOGGED IN / CONNECTING OTHER SOCIAL ACCOUNT) =============
// =============================================================================
// locally --------------------------------
app.get('/connect/local', function(req, res) {
res.render('connect-local.ejs', { message: req.flash('loginMessage') });
});
app.post('/connect/local', passport.authenticate('local-signup', {
successRedirect : '/profile', // redirect to the secure profile section
failureRedirect : '/connect/local', // redirect back to the signup page if there is an error
failureFlash : true // allow flash messages
}));
// facebook -------------------------------
// send to facebook to do the authentication
app.get('/connect/facebook', passport.authorize('facebook', {
scope : ['public_profile', 'email']
}));
// handle the callback after facebook has authorized the user
app.get('/connect/facebook/callback',
passport.authorize('facebook', {
successRedirect : '/profile',
failureRedirect : '/'
}));
// twitter --------------------------------
// send to twitter to do the authentication
app.get('/connect/twitter', passport.authorize('twitter', { scope : 'email' }));
// handle the callback after twitter has authorized the user
app.get('/connect/twitter/callback',
passport.authorize('twitter', {
successRedirect : '/profile',
failureRedirect : '/'
}));
// google ---------------------------------
// send to google to do the authentication
app.get('/connect/google', passport.authorize('google', { scope : ['profile', 'email'] }));
// the callback after google has authorized the user
app.get('/connect/google/callback',
passport.authorize('google', {
successRedirect : '/profile',
failureRedirect : '/'
}));
};
// route middleware to ensure user is logged in
function isLoggedIn(req, res, next) {
if (req.isAuthenticated())
return next();
res.redirect('/');
}
As you can see, we have all the authentication routes and the routes to show our index and profile pages. Now we have added authorization routes which will look incredibly similar to our authentication routes.
With our newly created routes, let’s update the Strategy so that our authorization routes are utilized.
We will just update the Facebook and Local Strategies to get a feel for how we can accommodate all our different scenarios.
When using the passport.authorize
route, our user that is stored in session (since they are already logged in) will be passed to the Strategy. We will make sure we change our code to account for that.
We’re going to show the old Strategy and then the new Strategy. Read the comments to get a full understanding of the changes.
...
// =========================================================================
// FACEBOOK ================================================================
// =========================================================================
passport.use(new FacebookStrategy({
// pull in our app id and secret from our auth.js file
clientID : configAuth.facebookAuth.clientID,
clientSecret : configAuth.facebookAuth.clientSecret,
callbackURL : configAuth.facebookAuth.callbackURL
},
// facebook will send back the token and profile
function(token, refreshToken, profile, done) {
// asynchronous
process.nextTick(function() {
// find the user in the database based on their facebook id
User.findOne({ 'facebook.id' : profile.id }, function(err, user) {
// if there is an error, stop everything and return that
// ie an error connecting to the database
if (err)
return done(err);
// if the user is found, then log them in
if (user) {
return done(null, user); // user found, return that user
} else {
// if there is no user found with that facebook id, create them
var newUser = new User();
// set all of the facebook information in our user model
newUser.facebook.id = profile.id; // set the users facebook id
newUser.facebook.token = token; // we will save the token that facebook provides to the user
newUser.facebook.name = profile.name.givenName + ' ' + profile.name.familyName; // look at the passport user profile to see how names are returned
newUser.facebook.email = profile.emails[0].value; // facebook can return multiple emails so we'll take the first
// save our user to the database
newUser.save(function(err) {
if (err)
throw err;
// if successful, return the new user
return done(null, newUser);
});
}
});
});
}));
...
Now we want the ability to authorize a user.
...
// =========================================================================
// FACEBOOK ================================================================
// =========================================================================
passport.use(new FacebookStrategy({
// pull in our app id and secret from our auth.js file
clientID : configAuth.facebookAuth.clientID,
clientSecret : configAuth.facebookAuth.clientSecret,
callbackURL : configAuth.facebookAuth.callbackURL,
passReqToCallback : true // allows us to pass in the req from our route (lets us check if a user is logged in or not)
},
// facebook will send back the token and profile
function(req, token, refreshToken, profile, done) {
// asynchronous
process.nextTick(function() {
// check if the user is already logged in
if (!req.user) {
// find the user in the database based on their facebook id
User.findOne({ 'facebook.id' : profile.id }, function(err, user) {
// if there is an error, stop everything and return that
// ie an error connecting to the database
if (err)
return done(err);
// if the user is found, then log them in
if (user) {
return done(null, user); // user found, return that user
} else {
// if there is no user found with that facebook id, create them
var newUser = new User();
// set all of the facebook information in our user model
newUser.facebook.id = profile.id; // set the users facebook id
newUser.facebook.token = token; // we will save the token that facebook provides to the user
newUser.facebook.name = profile.name.givenName + ' ' + profile.name.familyName; // look at the passport user profile to see how names are returned
newUser.facebook.email = profile.emails[0].value; // facebook can return multiple emails so we'll take the first
// save our user to the database
newUser.save(function(err) {
if (err)
throw err;
// if successful, return the new user
return done(null, newUser);
});
}
});
} else {
// user already exists and is logged in, we have to link accounts
var user = req.user; // pull the user out of the session
// update the current users facebook credentials
user.facebook.id = profile.id;
user.facebook.token = token;
user.facebook.name = profile.name.givenName + ' ' + profile.name.familyName;
user.facebook.email = profile.emails[0].value;
// save the user
user.save(function(err) {
if (err)
throw err;
return done(null, user);
});
}
});
}));
...
Now we have accounted for linking an account if a user is already logged in. We still have the same functionality from before, now we just check if the user is logged in before we take action.
Using this new code in our Strategy, we will create a new user if they are not already logged in, or we will add our Facebook credentials to our user if they are currently logged in and stored in session.
Other Strategies: The code for the Facebook Strategy will be the same for Twitter and Google. Just apply that code to both of those to get this working. We will also provide the full code so you can look at and reference it.
Now that we have the routes that will pass our user to our new Facebook Strategy, let’s make sure our UI lets our user use the newly created routes.
We will update our index.ejs
and our profile.ejs
to show all the login buttons on the home page, and all the accounts and link buttons on the profile page. Here is the full code for both with the important parts highlighted.
<!doctype html>
<html>
<head>
<title>Node Authentication</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">
<div class="jumbotron text-center">
<h1><span class="fa fa-lock"></span> Node Authentication</h1>
<p>Login or Register with:</p>
<a href="/login" class="btn btn-default"><span class="fa fa-user"></span> Local Login</a>
<a href="/signup" class="btn btn-default"><span class="fa fa-user"></span> Local Signup</a>
<a href="/auth/facebook" class="btn btn-primary"><span class="fa fa-facebook"></span> Facebook</a>
<a href="/auth/twitter" class="btn btn-info"><span class="fa fa-twitter"></span> Twitter</a>
<a href="/auth/google" class="btn btn-danger"><span class="fa fa-google-plus"></span> Google+</a>
</div>
</div>
</body>
</html>
<!doctype html>
<html>
<head>
<title>Node Authentication</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; word-wrap:break-word; }
</style>
</head>
<body>
<div class="container">
<div class="page-header text-center">
<h1><span class="fa fa-anchor"></span> Profile Page</h1>
<a href="/logout" class="btn btn-default btn-sm">Logout</a>
</div>
<div class="row">
<!-- LOCAL INFORMATION -->
<div class="col-sm-6">
<div class="well">
<h3><span class="fa fa-user"></span> Local</h3>
<% if (user.local.email) { %>
<p>
<strong>id</strong>: <%= user._id %><br>
<strong>email</strong>: <%= user.local.email %><br>
<strong>password</strong>: <%= user.local.password %>
</p>
<a href="/unlink/local" class="btn btn-default">Unlink</a>
<% } else { %>
<a href="/connect/local" class="btn btn-default">Connect Local</a>
<% } %>
</div>
</div>
<!-- FACEBOOK INFORMATION -->
<div class="col-sm-6">
<div class="well">
<h3 class="text-primary"><span class="fa fa-facebook"></span> Facebook</h3>
<!-- check if the user has this token (is the user authenticated with this social account) -->
<% if (user.facebook.token) { %>
<p>
<strong>id</strong>: <%= user.facebook.id %><br>
<strong>token</strong>: <%= user.facebook.token %><br>
<strong>email</strong>: <%= user.facebook.email %><br>
<strong>name</strong>: <%= user.facebook.name %><br>
</p>
<a href="/unlink/facebook" class="btn btn-primary">Unlink</a>
<% } else { %>
<a href="/connect/facebook" class="btn btn-primary">Connect Facebook</a>
<% } %>
</div>
</div>
</div>
<div class="row">
<!-- TWITTER INFORMATION -->
<div class="col-sm-6">
<div class="well">
<h3 class="text-info"><span class="fa fa-twitter"></span> Twitter</h3>
<!-- check if the user has this token (is the user authenticated with this social account) -->
<% if (user.twitter.token) { %>
<p>
<strong>id</strong>: <%= user.twitter.id %><br>
<strong>token</strong>: <%= user.twitter.token %><br>
<strong>display name</strong>: <%= user.twitter.displayName %><br>
<strong>username</strong>: <%= user.twitter.username %>
</p>
<a href="/unlink/twitter" class="btn btn-info">Unlink</a>
<% } else { %>
<a href="/connect/twitter" class="btn btn-info">Connect Twitter</a>
<% } %>
</div>
</div>
<!-- GOOGLE INFORMATION -->
<div class="col-sm-6">
<div class="well">
<h3 class="text-danger"><span class="fa fa-google-plus"></span> Google+</h3>
<!-- check if the user has this token (is the user authenticated with this social account) -->
<% if (user.google.token) { %>
<p>
<strong>id</strong>: <%= user.google.id %><br>
<strong>token</strong>: <%= user.google.token %><br>
<strong>email</strong>: <%= user.google.email %><br>
<strong>name</strong>: <%= user.google.name %>
</p>
<a href="/unlink/google" class="btn btn-danger">Unlink</a>
<% } else { %>
<a href="/connect/google" class="btn btn-danger">Connect Google</a>
<% } %>
</div>
</div>
</div>
</div>
</body>
</html>
Now we will have the links to each of our login methods. Then after they have logged in with one, the profile will check which accounts are already linked and which are not.
If an account is not yet linked, it will show the Connect Button. If an account is already linked, then our view will show the account information and the unlink button.
Remember that our user is passed to our profile view from the routes.js
file.
Our social accounts can easily be configured this way. The only problem currently is if a user wanted to connect to a local account. The problem comes in because they will need to see a signup page to add their email and password.
We have already created a route to handle showing our new connection form (in our routes.js
file: (app.get('connect/local'))
). All we need to do is create the view that the route brings up.
Create a file in your views folder: views/connect-local.ejs
.
<!doctype html>
<html>
<head>
<title>Node Authentication</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">
<div class="col-sm-6 col-sm-offset-3">
<h1><span class="fa fa-user"></span> Add Local Account</h1>
<% if (message.length > 0) { %>
<div class="alert alert-danger"><%= message %></div>
<% } %>
<!-- LOGIN FORM -->
<form action="/connect/local" method="post">
<div class="form-group">
<label>Email</label>
<input type="text" class="form-control" name="email">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" class="form-control" name="password">
</div>
<button type="submit" class="btn btn-warning btn-lg">Add Local</button>
</form>
<hr>
<p><a href="/profile">Go back to profile</a></p>
</div>
</div>
</body>
</html>
This will look incredibly similar to our signup.ejs
form. That’s because it really is. We pretty much just changed out the verbiage and the action
URL for the form.
Now when someone tries to connect a local account, they will be directed to this form, and then when submitted, they will be directed to our Local Strategy. That links the accounts!
With just those routes and the update to our Passport Strategies, our application can now link accounts together! Take a look at a user in our database that has all their accounts linked using Robomongo:
Linking accounts was easy. What about unlinking? Let’s say a user no longer wants their Facebook account linked.
For our purposes, when a user wants to unlink an account, we will remove their token
only. We will keep their id
in the database just in case they realize their mistake of leaving and want to come back to join our application.
We can do this all in our routes file. You are welcome to create a controller and do all this logic there. Then you would just call the controller from the routes. For simplicity’s sake, we’ll throw that code directly into our routes.
Let’s add our unlinking routes after our newly created authorization routes.
...
// normal routes
// authentication routes
// authorization routes
// =============================================================================
// UNLINK ACCOUNTS =============================================================
// =============================================================================
// used to unlink accounts. for social accounts, just remove the token
// for local account, remove email and password
// user account will stay active in case they want to reconnect in the future
// local -----------------------------------
app.get('/unlink/local', function(req, res) {
var user = req.user;
user.local.email = undefined;
user.local.password = undefined;
user.save(function(err) {
res.redirect('/profile');
});
});
// facebook -------------------------------
app.get('/unlink/facebook', function(req, res) {
var user = req.user;
user.facebook.token = undefined;
user.save(function(err) {
res.redirect('/profile');
});
});
// twitter --------------------------------
app.get('/unlink/twitter', function(req, res) {
var user = req.user;
user.twitter.token = undefined;
user.save(function(err) {
res.redirect('/profile');
});
});
// google ---------------------------------
app.get('/unlink/google', function(req, res) {
var user = req.user;
user.google.token = undefined;
user.save(function(err) {
res.redirect('/profile');
});
});
...
In these routes, we just pull a user’s information out of the request (session) and then remove the correct information. Since we already had created our links to these routes in profile.ejs
, they will now work since we have created the routes finally.
Now you can link an account and unlink an account.
When trying to unlink, we will have to do a little more configuration for that to work. Since the id
is already stored in the database, we will have to plan for that scenario when a user links an account that was already previously linked.
After a user is unlinked, their id
still lives in the database. Therefore, when a user logs in or relinks an account, we have to check if their id
exists in the database.
We will handle this in our Strategy. Let’s add to our Facebook Strategy.
...
// =========================================================================
// FACEBOOK ================================================================
// =========================================================================
passport.use(new FacebookStrategy({
// pull in our app id and secret from our auth.js file
clientID : configAuth.facebookAuth.clientID,
clientSecret : configAuth.facebookAuth.clientSecret,
callbackURL : configAuth.facebookAuth.callbackURL,
passReqToCallback : true // allows us to pass in the req from our route (lets us check if a user is logged in or not)
},
// facebook will send back the token and profile
function(req, token, refreshToken, profile, done) {
// asynchronous
process.nextTick(function() {
// check if the user is already logged in
if (!req.user) {
// find the user in the database based on their facebook id
User.findOne({ 'facebook.id' : profile.id }, function(err, user) {
// if there is an error, stop everything and return that
// ie an error connecting to the database
if (err)
return done(err);
// if the user is found, then log them in
if (user) {
// if there is a user id already but no token (user was linked at one point and then removed)
// just add our token and profile information
if (!user.facebook.token) {
user.facebook.token = token;
user.facebook.name = profile.name.givenName + ' ' + profile.name.familyName;
user.facebook.email = profile.emails[0].value;
user.save(function(err) {
if (err)
throw err;
return done(null, user);
});
}
return done(null, user); // user found, return that user
} else {
// if there is no user found with that facebook id, create them
var newUser = new User();
// set all of the facebook information in our user model
newUser.facebook.id = profile.id; // set the users facebook id
newUser.facebook.token = token; // we will save the token that facebook provides to the user
newUser.facebook.name = profile.name.givenName + ' ' + profile.name.familyName; // look at the passport user profile to see how names are returned
newUser.facebook.email = profile.emails[0].value; // facebook can return multiple emails so we'll take the first
// save our user to the database
newUser.save(function(err) {
if (err)
throw err;
// if successful, return the new user
return done(null, newUser);
});
}
});
} else {
// user already exists and is logged in, we have to link accounts
var user = req.user; // pull the user out of the session
// update the current users facebook credentials
user.facebook.id = profile.id;
user.facebook.token = token;
user.facebook.name = profile.name.givenName + ' ' + profile.name.familyName;
user.facebook.email = profile.emails[0].value;
// save the user
user.save(function(err) {
if (err)
throw err;
return done(null, user);
});
}
});
}));
...
Now just add that same code across the board to all of our Strategies and we have an application that can register a user, link accounts, unlink accounts, and relink accounts.
For those interested in seeing the entire code altogether, make sure you check out the GitHub repo. Also, here are direct links to the two most important files:
Hopefully, we covered most of the cases that you’ll run into when authenticating and authorizing users. Make sure to take a look at the full code and the demo to make sure that everything is working properly. If you see anything that raises questions, just let me know and be sure to go look at the full code for clarification!
Thanks for sticking with us throughout this entire series. We hope you enjoyed it. We’ll be expanding on authentication further in the future by doing a Node and Angular authentication tutorial. Until then, happy authenticating!
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!