Client-side authentication & session management via Backbone.js and Node.js

The rise in client-side apps has created the need for a secure auth workflow via AJAX.  While the authentication handshake isn’t one-size-fits all given the variety in client-side frameworks and server-side APIs, there are certain best practices which can harden the communication and protect your fancy, single-page apps from potential snoopers.  In this post I will review the following practices and implement them in a demo app built using Backbone.JS and Node.JS:

  • CSRF headers in authentication requests to prevent forgery

  • Auth state persistence through signed cookies

  • Global (singleton) session model in the client whose state changes can be listened to

  • Client-side + Server-side model validations

  • Salt/hashing of passwords for back-end storage

 

Demo App Overview

The demo app consists of both client-side & server-side components.

The server-layer is a lightweight, single-file Express.js HTTP server and SQLLite3 db.  It responds to a number of typical authentication API routes, as well as renders the initial index.html page wrapper to plant the session CSRF token.  While it’s a good practice to have an isolated User/Auth model in the server which handles validations and database interaction, for simplicity this functionality is encapsulated inside API handlers (a.k.a. at the controller-level).

The client-side is structured using Backbone.js (probably the most common client-side MV* framework) and Require.js (a popular, dependency-based module framework).  It is also rather bare with only a couple views, a pushState-enabled router, and models for a User and a Session.  The client communicates with the server via Backbone.sync methods, which essentially just wrap standard AJAX CRUD operations.

 

CSRF

A CSRF token is essentially a means for the server to recognize the identity of the requesting client.  This identity detection is used to prevent XSS (Cross-Site Scripting) from other scripts which may also be running on the page.  The randomized token is generated and planted by the server in the client’s HTML meta tags (which should not be refreshed in a single-page app) or inside of a hidden form element.  The client can then choose to include this token in the headers (more specifically a “X-CSRF-Token” header) of AJAX requests as a means of identification & authorization.

Most large, server-side frameworks in-use today provide a means of generating and validating a CSRF token.  In the demo app, we use the CSRF Express.js middleware, and the Express.js Handlebars templating engine to render the initial HTML wrapper with this token.

In the client SessionModel, we extract this token from the meta content using jQuery, and include it in our authentication POST headers.  Our server then can validate the client’s identity when an authentication request is received.

 

For a deeper dive into CSRF, I recommend consulting the in-depth Ruby on Rails security guide.



Signed cookies

Persistent, cookie-based sessions are becoming more and more common among single-page apps today.  Who wants to have to log back into a site every time they re-open it in their browser?  Not me.

Cookies are simply key-value pairs stored in the browser with domain-level specificity and expiration.  Persistent auth is often achieved through cookies because they can be read (in the request) and written (in the response) by the owning domain’s server.  Similar to CSRF, cookies can act to identify a client based on their value.  Because cookies can be read by client scripts, including scripts from external sources, these must be encrypted/decrypted (signed/unsigned) by the server using a special key.  Encryption is not always necessary for basic state-representative cookies, but it surely is when using the cookie value as a means of authenticating a client.

Almost all server-side frameworks in use today have a means of creating and parsing signed cookies, but it is largely up to the server-side developer with how to store and manage their lifecycle.  The back-end creation and fetching of client sessions can get more complex when a single user can have multiple active, persistent sessions.  This creates the need for separating Authentications from Users in the back-end, relationally linking the two via a foreign ID.

For simplicity in our demo app, however, we store this signed authentication token inside of the User table, from which we can update and query.  The Express.js cookie parser middleware takes care of the signing for us if we pass in a secret key.



Global, client-side session model

Building a modular, client-side app you quickly begin to realize how many views and underlying controller functionality depends on a user’s session – specifically a user’s logged-in state and certain logged-in user attributes.  Global variables aren’t often a good practice in JavaScript, but having a singleton SessionModel which houses this information and can be accessed throughout your app is downright awesome.  We sacrifice the performance in the  memory-management of a global object, but make up for it with having a single-source of truth for client session retrieval.  It also enables us to listen for changes in the model and render/react accordingly.

The demo app uses a persistent global Backbone.js SessionModel to manage state. Views and other models all can:

  • subscribe to changes in session state

ex/  app.session.on("change:logged_in", this.onLoginStatusChange);

  • extract current user data

ex/  "Logged in as "+app.session.user.get("username")

  • trigger auth events (login, logout, etc)

ex/  app.session.logout({});

There are a couple of common security pitfalls when constructing a global Session Model worth bringing to light.

1)  Do not let the client model persist sensitive information.  Backbone.js really likes to persist as much model data in the client as it can.  This is often helpful to carry over previously set properties on a model, extending any newly fetched or updated properties onto itself.  If you are letting a Session Model handle authentication, it’s important to immediately purge an auth token, password, or any sensitive information immediately after it is used in a request.  Or simply avoid setting sensitive properties on the model itself, and abstracting these out to a pass-thru function in the model.

2)  Ensure all session communication with the server is via HTTPS.  Your API might not be designed to handle HTTPS everywhere, nor may it be very performant that way, but it is absolutely critical when POSTing passwords and other sensitive information (email, address, etc.) in plaintext over the wire.  Since cookies are tied to a domain and not a protocol, they can be set and parsed by the server interchangeably.  HTTPS can be set in the model-specific  default url, or you can simply set this conditionally using standard AJAX in the model instead of Backbone.sync.

3)  Wrap the Session Model in a closure, only allowing access to your other views/models inside the closure.  An app ‘global’ does not mean it is accessible through the window.  Using a modular dependency framework like Require.js lets you explicitly define which of your modules need access to the session, and will wrap everything up for you upon compilation.  In our demo app, we include ‘app’ inside our module definitions when needed.



Client-side & server-side validations

As the client should not ever be completely trusted by the server, the ultimate responsibility of XSS cleaning and validating user/authentication data lies with the server.  The server is the gatekeeper to data that can enter your database.

That being said, the client can and should validate form data before attempting communication with the server’s API.  Why send unnecessary requests to the API?  The crux of single-page apps is offloading as much responsibility to the client as is practical, and things like validating input length and pattern-matching can be handled easily and performantly in JavaScript.  Although the validations need to occur in both places, it’s more responsive and less load on the server to validate forms in the client first.

One library which I’ve found is particularly useful for this is Parsley.js.  Parsley takes care of a slew of common validations such as input length, pattern-matching, and content equality in very little lines of code.  The validations can even reside in the DOM inside the form input tags themselves with ‘data-’ tags.  One call to $('form').parsley('validate') and you now have a list of any failed validations visible in the DOM.  Too easy.



Salting/hashing of passwords

The last practice of password encryption is not client-side responsibility, but definitely worth reviewing.  If the client is sending passwords as plaintext inside of POST requests’ JSON payloads to the server, the server must take the appropriate measures to encrypt those passwords before inserting into the database.

Although there are many encryption algorithms and practices for our disposal, a safe and common method is to irreversibly salt & hash the password so its final value can be read and compared but not returned into the raw password.  A salt can sit separate from the hash in the database or alongside it (or not sit in the database at all), and one salt can be used for multiple hashes.  As long as both can be retrieved, then they can be compared for equality to a raw password which is the boolean answer needed for the authentication handshake.

In our demo app we use the bcrypt Node.js module for the salt & hash generation and comparison.

 

 

While you can go down the rabbit hole of client-server security, we have covered the basic foundations and general best practices of securing modern web app authentication flow.  Check out the github repository and live demo for reference and application of the above concepts.  Appreciate your comments/feedback/questions!