Scott Smith

Blog Tutorials Projects Speaking RSS

Twitatron: Building a Production Web App With Node - User Accounts

Welcome to part 3 of the Twitaron series

  1. Getting started
  2. Views & Controllers
  3. User Accounts
  4. Under development…

In our previous article we leared how to add views, layouts, partials, controllers, and more.

In this installment of the Twitatron series, we will be diving into how to implement user accounts. By the end of this article you will have learned how to connect to MongoDB, used Mongoose for object modeling, implemented Passport for user authentication, allow users to login with their Twitter account, and have full support for user accounts.

Secrets

Before we go further into setting up support for logging in with Twitter, we need to add a way to easily develop locally and run in production. There are going to be settings that are different locally versus production and we don’t want these production values in our source code. There are many ways to handle this, but one way I like is to use a secrets module.

If you don’t already have a config directory in the root of your application, create one now. Inside this directory, create a new filed named secrets.js. Update this file to contain the following. We will be using many of these items in this and future tutorials.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
  db: process.env.MONGODB || 'mongodb://localhost:27017/twitatron',

  cryptos: {
    algorithm: 'aes256',
    key: process.env.CRYPTO_KEY || 'Your crypto key goes here'
  },

  sessionSecret: process.env.SESSION_SECRET || 'Your session secret goes here',

  twitter: {
    consumerKey: process.env.TWITTER_KEY || 'Your Twitter consumer key',
    consumerSecret: process.env.TWITTER_SECRET  || 'Your Twitter consumer secret',
    callbackURL: process.env.TWITTER_CALLBACK || 'http://localhost:3000/auth/twitter/callback',
    passReqToCallback: true
  }
};

When your application runs in production, you can setup all the necessary environment variables so they are used within your application. When you run locally, it will use the values specified within this module.

The last thing we need to do is use this module within our application. Update the code in server.js from our previous article to look like the following.

1
2
3
4
5
// Load required packages
var path = require('path');
var express = require('express');
var compression = require('compression');
var secrets = require('./config/secrets');

Connecting to MongoDB

If you don’t already have MondgoDB installed and running, you will want to go their official site and follow their installation instructions.

There are three things we need to do to connect to MongoDB.

  1. Install the Mongoose package
  2. Load the Mongoose package
  3. Connect to it using our connection string

Install the package manually using the following command:

1
npm install mongoose --save

Update the code in server.js from our previous article to look like the following.

1
2
3
4
5
6
// Load required packages
var path = require('path');
var express = require('express');
var compression = require('compression');
var secrets = require('./config/secrets');
var mongoose = require('mongoose');

Connect to MongoDB

1
2
3
4
5
6
7
8
9
// Load required packages
var path = require('path');
var express = require('express');
var compression = require('compression');
var secrets = require('./config/secrets');
var mongoose = require('mongoose');

// Connect to the twitatron MongoDB
mongoose.connect(secrets.db);

If all goes well, your application should start up just fine. You will notice we are already using our secrets module for the MongoDB connection string.

User Model

We now need a model to store our user. Inside the models directory, create a file named user.js and add the following code to it. If you don’t have a models directory, go ahead and create one in the root of your application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Load required packages
var mongoose = require('mongoose');
var crypto = require('crypto');
var secrets = require('../config/secrets');

// Define our user schema
var UserSchema = new mongoose.Schema({
  twitterId: { type: String, unique: true, required: true },
  username: { type: String, unique: true, lowercase: true, required: true },
  email: { type: String, lowercase: true },
  name: { type: String, default: '' },
  created: { type: Date, default: new Date() },
  accessToken: { type: String, required: true },
  tokenSecret: { type: String, required: true }
});

UserSchema.methods.encrypt = function(text) {
  var algorithm = secrets.cryptos.algorithm;
  var key = secrets.cryptos.key;

  var cipher = crypto.createCipher(algorithm, key);
  return cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
};

UserSchema.methods.decrypt = function(text) {
  var algorithm = secrets.cryptos.algorithm;
  var key = secrets.cryptos.key;

  var decipher = crypto.createDecipher(algorithm, key);
  return decipher.update(text, 'hex', 'utf8') + decipher.final('utf8');
};

// Export the Mongoose model
module.exports = mongoose.model('User', UserSchema);

So what is going on here?

  1. We loaded the Mongoose package
  2. Created a Mongoose schema which maps to a MongoDB collection and defines the shape of the documents within that collection.
  3. We defined our schema to contain twitterId, username, email, name, created date, access token, and token secret.
  4. We exported the Mongoose user model for use within our application.
  5. We created two methods on our schema that we will use to encrypt and decrypt the access token and token secret.

Auth Controller

1
2
npm install passport --save
npm install passport-twitter --save

This will install the standard passport package along with passport-twitter. Passport-twitter will provide our application with Twitter authentication strategies. It will allow us to easily add Twitter login to our app.

In the controllers directory, add a file named auth.js with the following contents.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Load required packages
var passport = require('passport');
var TwitterStrategy = require('passport-twitter').Strategy;
var User = require('../models/user');
var secrets = require('../config/secrets');

passport.serializeUser(function(user, done) {
  done(null, user);
});

passport.deserializeUser(function(user, done) {
  done(null, user);
});

passport.use(new TwitterStrategy(secrets.twitter, function(req, accessToken, tokenSecret, profile, done) {
  User.findOne({ twitterId: profile.id }, function(err, existingUser) {
    if (existingUser) return done(null, existingUser);

    var user = new User();

    user.twitterId = profile.id;
    user.username = profile.id;
    user.email = '';
    user.name = profile.displayName;
    user.created = new Date();
    user.accessToken = user.encrypt(accessToken);
    user.tokenSecret = user.encrypt(tokenSecret);

    user.save(function(err) {
      done(err, user);
    });
  });
}));

exports.twitter = passport.authenticate('twitter');
exports.twitterCallback = passport.authenticate('twitter', { failureRedirect: '/' });

What we are doing here is setting up passport to use the Twitter authentication strategy provided by the passport-twitter package. For our TwitterStrategy, we are defining a callback that will attempt to look up the user using the Twitter profile id and if found not found, create a new user. If all works well, it will return an existing user or create a new user.

The final piece of this is exporting the auth and authCallback functions which will be used within our application as route endpoints responsible for creating and logging users in via Twitter. Open up server.js and set it to the following code.

Also, because Passport Twitter strategy requires sessions, be sure to install the express-session package.

1
npm install express-session --save
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// Load required packages
var path = require('path');
var express = require('express');
var compression = require('compression');
var secrets = require('./config/secrets');
var mongoose = require('mongoose');
var passport = require('passport');
var session = require('express-session');

// Connect to the twitatron MongoDB
mongoose.connect(secrets.db);

// Load controllers
var homeController = require('./controllers/home');
var authController = require('./controllers/auth');

// Create our Express application
var app = express();

// Tell Express to use sessions
app.use(session({
  secret: secrets.sessionSecret,
  resave: false,
  saveUninitialized: false,
}));

// Use the passport package in our application
app.use(passport.initialize());
app.use(passport.session());

// Add content compression middleware
app.use(compression());

// Add static middleware
var oneDay = 86400000;
app.use(express.static(path.join(__dirname, 'public'), { maxAge: oneDay }));

// Add jade view engine
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

// Create our Express router
var router = express.Router();

// Landing page route
router.get('/', homeController.index);

// Auth routes
router.get('/auth/twitter', authController.twitter);
router.get('/auth/twitter/callback', authController.twitterCallback, function(req, res) {
  res.redirect(req.session.returnTo || '/');});

// Register all our routes
app.use(router);

// Start the server
app.listen(3000);

What we did here was to include the passport, express-session, and authController modules. After that we setup our Express application to use passport and passport session as middleware. Finally, we create two new endpoints responsible for logging users in via Twitter.

In order to test this, you will need to head to Twitter and register an application. You can do that here: https://apps.twitter.com/. Once you have an application, update the consumer key and secret inside secrets.js.

You can now test things out by making a request to http://localhost:3000/auth/twitter

Clean up our views

Before we update our views to support logging in and out, we need to clean up our views and some of the code behind it first.

Open up homeController.js and delete this line from the index action: res.locals.ip = req.ip;.

Open up home.jade and delete this line from the view: h2 You are visiting from #{ip}.

Allow users to login and logout

To know whether or not a user is currently logged in, we need to add a little code to our Express application. One of the nice things Passport provides is that it automatically adds a user object to the Express request object when someone is logged in. We can take advantage of this by passing it to our views. Open up server.js and update the code as follows right after we use passport.session.

1
2
3
4
5
// Setup objects needed by views
app.use(function(req, res, next) {
  res.locals.user = req.user;
  next();
});

What we are doing is adding the user object to the locals object in order to make it available in our views.

Next, we will want to update navigation.jade to show login or logout depending on the user’s state.

1
2
3
4
5
6
7
header
  div
    a(href='/') Twitatron
    if !user
      a(href="/auth/twitter") Login with Twitter
    else
      a(href="/auth/logout") Logout

The last thing we need to implement is a controller action for the route /auth/logout. Open up authController.js and add the following to the very end.

1
2
3
4
5
exports.logout = function(req, res) {
  req.logout();
  req.session.destroy();
  res.redirect('/');
};

Now, just define your route within server.js as follows.

1
2
3
4
5
// Auth routes
router.get('/auth/twitter', authController.twitter);
router.get('/auth/twitter/callback', authController.twitterCallback, function(req, res) {
  res.redirect(req.session.returnTo || '/');});
router.get('/auth/logout', authController.logout);

Go ahead and try things out. You should be able to click Login with Twitter, get redirected to Twitter, authorize access to your Twitter account, and have a User created or get logged in as an existing user.

Wrap up

We covered a lot of areas in this tutorial. First, we added a configuration module which allows easy configuration between development and production. Second, we learned about Mongoose and connected to MongoDB. Third, we created a Mongoose User model and created helper methods that allow us to encrypt and decrypt sensitive information such as access tokens and token secrets. Finally, we added the ability to log in with Twitter, have a user account created, and then log out.

If you found this article or others useful be sure to subscribe to my RSS feed or follow me on Twitter. Also, if there are certain topics you would like me to write on, feel free to leave comments and let me know.

Source code for this part can be found here on GitHub.