× {{alert.msg}} Never ask again
Get notified about new tutorials RECEIVE NEW TUTORIALS

Sharing Passport.Js Sessions With Both Express And Socket.Io

Scott Hasbrouck
Mar 21, 2016
<p>Want more JavaScript tips from me? <a href="http://scotthasbrouck.com">Visit my blog for all of my posts and book</a>!</p> <p>Authentication and session management is an integral part of any Node.js app. As a continuation from last week's post about using <a href="http://socket.io/">Socket.io</a> and <a href="http://rethinkdb.com">RethinkDB</a> change feeds together to build "Meteor-like" reactivity, I'm going to share the simple strategy I've used to use the same <a href="#">Passport.js</a> session for both Express and Socket.io. If you're unfamiliar with it, Socket.io is a realtime messaging library backed by Websockets, with a fallback to vanilla XMLHttpRequests and polling, making it extremley useful for reactive web apps.</p> <p>Prior to integrating Socket.io for opening websockets to emit and subscribe to events, we'll first need to use Express to serve the client-side files (HTML, CSS, and JS), via HTTP or HTTPS. In fact, the strategy I take is to not open a websocket to the client until they authenticate. Every single socket you open with a client will stay 'pending', and any data will be sent as 'frames' of that pending connection - you can see this in the Network tab of Chrome web tools. This means that unlike using AJAX, which is built on XMLHttpRequest, the connections stay open and you could reach a maximum concurrent connection limit much sooner than you would with Express. Remember that when an HTTP request is made to your server, your app responds to that request, and then it is closed. Given all that, I only give clients the privilege of opening a websocket once they authenticate. You may find your web app should implement websockets right away - perhaps you're building an app that has a public activity stream (<em>perfect</em> use case for Socket.io). It's up to you, but if you want my thoughts, <a href="https://twitter.com/scotthasbrouck">tweet me</a> or <a href="mailto:hello@scotthasbrouck.com">email me</a>!</p> <p>For this short tutorial, we'll assume that you already have a Node.js app with Express implemented, and start by implementing Passport.js and a Redis session store. If you don't have Redis, grab it <a href="http://redis.io/topics/quickstart">here</a> and install it. In reality, you don't even need to understand how Redis works to use it for session. But since we're <strong>good engineers</strong>, and we like to understand the <em>why</em> behind the reasons for using a certain technology, I'll enlighten you (skip to the next paragraph if you're yawning). In short, Redis is an "in-memory" key value store - that's right, the Redis database is stored in volatile memory. Why the hell would you do that? Well, it's fast, and is perfect for storing simple key-value data, like sessions! Redis also has on-disk persistence features, so if your server restarts, Redis will write it's current state to the disk, and replay it to its last state when your server boots up.</p> <h2>Boilerplate Node.Js App</h2> <p>Ok, so here's our simple Node.js app, which serves a single index.html file and public directory with all of the JS, Image, and CSS goodies.</p> <pre><code class="language-javascript">// index.js var express = require('express'); var http = require('http'); var app = express(); var server = http.Server(app); app.use(express.static(__dirname + '/../public')); app.get('*', function(req, res) { res.sendFile('index.html'); }); server.listen(9000);</code></pre> <h2>Setting Up Database Model For Users</h2> <p>Let's get started with Passport.js! If you haven't done so already, install Passport, along with an authentication strategy:</p> <pre><code class="language-bash">$ npm install -s passport passport-local</code></pre> <p> </p> <p>You need to pick one or more "strategies," of which there a whopping 307 at the time of this writing. The most common of course being passport-local, just a vanilla username and password authentication. Others include Facebook, Twitter, OAuth, and Github, but we're just going to stick with passport-local for this tutorial. Once you have sessions wired up, you can use any authentication strategy you like.</p> <p>We'll also be saving our User accounts in RethinkDB for this tutorial. You're of course welcome to use any database of your choice, such as MongoDB. Of course, if you do use a different datastore, you'll need to modify how your users are retrieved and stored. In addition to RethinkDB, we'll use <a href="https://thinky.io/">Thinky.io</a>, an ORM for RethinkDB:</p> <pre><code class="language-javascript">$ npm install -s rethinkdb thinky</code></pre> <p><span style="background-color:rgb(255, 255, 255); color:rgb(54, 54, 54)">The last set of libraries will need (we'll explain them as we get to them):</span></p> <pre><code class="language-bash">$ npm install -s body-parser bcrypt connect-redis express-session redis-url</code></pre> <p> </p> <p><span style="background-color:rgb(255, 255, 255); color:rgb(54, 54, 54)">Let's first make a Thinky model for our User account, which includes an email and password hash. Of course, if you are using MongoDB, you could just as easily setup a Mongoose model to represent your users.</span></p> <pre><code class="language-javascript">// models/user.js var thinky = require('thinky')({ db: 'myapp', host: 'localhost' }); var type = thinky.type; var bcrypt = require('bcrypt'); var User = thinky.createModel('User', { email: type.string(), hash: thyp.string(), attempts: type.number().default(0) }); User.pre('save', function(next) { this.email = this.email.toLowerCase(); var password = this.password || ''; delete this.password; if (!this.hash &amp;&amp; validatorMessages.isGoodPassword(password)) { this.hash = bcrypt.hashSync(password, 10); } return next(); }); User.define('public', function() { delete this.hash; return this; }); User.define('authenticate', function(password) { if (bcrypt.compareSync(password, this.hash) &amp;&amp; this.attempts &lt; 20) { this.attempts = 0; this.save(); delete this.hash; return this; } else { this.attempts++; this.save(); return false; } }); module.exports = User;</code></pre> <p> </p> <p>Besides just defining the email and hash properties, we've also defined a pre-save event which removes the raw text password and saves the bcrypt hash, and also ensures the email is saved in lower case. There are also two model methods - a public() method which can be used to strip any private information from the user instance, and an authenticate() method used to check the password against the bcrypt hash. The authenticate method also increments an <em>attempts </em>integer, and does not allow the user to login if more than 10 failed attempts are made. This is to prevent brute force attacks. In a production app, you will likely want to make this more sophisticated by automatically sending a password reset email after 10 attempts are made. Once the user authenticates, the attempts is reset to 0 and the user instance is saved. It is important to note that you should delete the user.hash <em>after</em> saving the user, otherwise you will delete the password hash when the user logs in.</p> <h2>Setting Up Passport.Js</h2> <p>Now let's setup Passport.js back in our index.js file:</p> <pre><code class="language-javascript">// index.js var express = require('express'); var http = require('http'); var app = express(); var server = http.Server(app); // import necessary modules for Passport var passport = require('passport'); var session = require('express-session'); // Sets up a session store with Redis var sessionStore = new RedisStore({ client: redisUrl.connect(process.env.REDIS_URL) }); // Express session middleware app.use(session({ store: sessionStore, resave: false, saveUninitialized: false, cookie: { secure: process.env.ENVIRONMENT !== 'development' &amp;&amp; process.env.ENVIRONMENT !== 'test', maxAge: 2419200000 }, secret: process.env.SECRET_KEY_BASE })); // Initialize Passport session app.use(passport.initialize()); app.use(passport.session()); app.use(express.static(__dirname + '/../public')); app.get('*', function(req, res) { res.sendFile('index.html'); }); server.listen(9000);</code></pre> <p> </p> <p>Let's walk through what we're doing here. First, we need to import Passport.js, and the express-session middleware. Once that is imported, we need to setup the express-session middleware, this is done by passing an object literal of properties in as the only argument to session(). The explanation of all of these properties can be found in the <a href="https://github.com/expressjs/session">express-session README</a>. Finally, we initialize Passport.js, and the Passport.js session.</p> <h2>Serializing And Deserializing The User</h2> <p>Passport.js is agnostic as to how you store, retrieve, and in the case of passport-local, confirm the supplied password is correct. This is why we defined a Thinky (or Mongoose) model, and a model method which compares the password with the stored hash using bcrypt. So now we need to tell Passport.js how to read the User from the database. Let's specify this just below the express-session middleware:</p> <pre><code class="language-javascript">// Import this at the top of index.js var User = require('./models/user.js'); // Below express-session middleware // Pass just the user id to the passport middleware passport.serializeUser(function(user, done) { done(null, user.id); }); // Reading your user base ont he user.id passport.deserializeUser(function(id, done) { User.get(id).run().then(function(user) { done(null, user.public()); }); });</code></pre> <p> </p> <p><span style="background-color:rgb(255, 255, 255); color:rgb(54, 54, 54)">Notice that when we retrieve the User from the database, we invoke the User.public() model method - which filters any properties we do not want to be made public. In this case, the password hash. Now we're ready to setup our Passport strategy: passport-local.</span></p> <pre><code class="language-javascript">passport.use(new LocalStrategy({ usernameField: 'email', passwordField: 'password' }, function(email, password, done) { User.getAll(email.toLowerCase(), { index: 'email' }).run() .then(function(users) { // Was a user found? if (users.length) { // Attempt authenticating with the supplied password if (users[0].authenticate(password)) { done(null, user.public()); } // Supplied password incorrect else { setTimeout(function() { done("Sorry, your password is incorrect", false); }, 3000); } } // No user was found else { setTimeout(function() { return done("Sorry, no account was found for that email", false); }, 3000); } }); }));</code></pre> <p> </p> <p>Passport.js accepts the various strategies as middleware, so we instantiate a new LocalStrategy, which accepts two arguments: the first is an object literal that specifies the names of the email and password fields, and the second is a callback that is invoked when an authentication is attempted with this strategy.  The callback here is pretty simple. First, we attempt to find a user with that email (lower case). If no user is found, we return an error message after 3 seconds - this delay is to prevent rapid brute force attacks. Once a user is found, we assume the first user in the array is the only user - since we're enforcing uniqueness on the email address - then check the password. If the password is correct, the user is allowed to login. Otherwise, we again show an error message after 3 seconds.</p> <h2>Login And Signup Routes</h2> <p>The last step to implement Passport.js with just Express, is to create POST routes to Login and Signup users. Going back to the index.js file, let's put these just above the app.get('*'):</p> <pre><code class="language-javascript">app.post('/signup', function(req, res) { User.filter({ email: req.body.email.toLowerCase() || '' }).count().run() .delay(3000) .then(function(count) { if (count === 0) { return true; } throw new Error("A user is already registered with that email address"); }) .then(function() { var user = new User(req.body); return user.save(); }) .then(function() { req.login(req.body, function(err) { if (err) { throw new Error(err); } res.send({ loggedin: true }); }); }) .catch(function(err) { res.json({ 'error': err.message }); }); }); app.post('login', passport.authenticate('local'), function(req, res) { res.json({ loggedin: true }); });</code></pre> <p> </p> <p>The signup POST route accepts JSON in the format of { email: '', password: '' }, as defined in the passport-local strategy. Once we check to make sure no one has already registered with that email address, the save just the req.body to the database. Finally, we can authenticate the user and use Passport.js to save the session by calling req.login (this is a method that Passport.js will attach to all requests). Once the user is logged in, it is up to you what response you want to be sent to your front end - in this case we're just sending an object with a single property, 'loggedin', set to true. The login POST route is very simple. we use the passport.authenticate() middleware, then respond to the request.</p> <h2>Integrating Passport.Js With Socket.Io</h2> <p>This completes everything you need to use Passport.js with Express! From here, sharing this same session with Socket.io, and opening a socket upon login is fairly straightforward. We'll need two more modules to make this work, a Passport.js / Socket.io middleware, and a cookie parser for Socket.io to read the cookies used by Passport.js:</p> <pre><code class="language-bash">$ npm install --save passport.socketio cookie-parser</code></pre> <pre><code class="language-javascript">// index.js var express = require('express'); var http = require('http'); var socketio = require('socket.io'); var passportSocketIo = require('passport.socketio'); var cookieParser = require('cookie-parser'); var app = express(); var server = http.Server(app); var io = socketio(server);</code></pre> <p><span style="background-color:rgb(255, 255, 255); color:rgb(54, 54, 54)">Next, below the express-session middleware, let's setup the Socket.io middleware for the session:</span></p> <pre><code class="language-javascript">io.use(passportSocketIo.authorize({ key: 'connect.sid', secret: process.env.SECRET_KEY_BASE, store: sessionStore, passport: passport, cookieParser: cookieParser }));</code></pre> <p><span style="background-color:rgb(255, 255, 255); color:rgb(54, 54, 54)">That's all there is to making your Passport.js sessions available from Socket.io! Let's setup an example socket to see how this works (this could be done in index.js, better yet, make a new file for your sockets and import it to index.js:</span></p> <pre><code class="language-javascript">var eventSocket = io.of('/events'); // on connection event eventSocket.on('connection', function(socket) { // example 'event1', with an object. Could be triggered by socket.io from the front end socket.on('event1', function(eventData) { // user data from the socket.io passport middleware if (socket.request.user &amp;&amp; socket.request.user.logged_in) { console.log(socket.request.user); } }); });</code></pre> <p>The Socket.io / Passport.js middle ties the session request object to 'socket.request', which in turn has the 'user' attached to it. Additionally, it attaches a boolean to the user called 'logged_in', that is true if the user successfully authenticated.</p> <p>That completes all that is needed to share your Passport.js you use with Express with Socket.io! You'll notice most of this is setting up a boilerplate Express/Passport.js session, so if you already have that in your app, adding Socket.io and accessing the session is a very simple addition.</p>
comments powered by Disqus