Aiming for elegance, one thought at a time

Adding a registration form (Secure SPA Part 2)

Posted: May 3rd, 2013 | Author: | Filed under: Uncategorized | 1 Comment »

This is the second part of a series of blog posts that will walk step-by-step through the process of creating a secure Single Page App (SPA) using node.js, passport.js, and Angular. We’ll be enforcing a minimum password strength (entropy) using zxcvbn, with checks on both the server and client.

If you’re new here, you should start at Part 1, where we set up Passport.js. Today we’re going to add in a signup page. We’ll be enforcing a minimum password strength (entropy) using zxcvbn, with checks on both the server and client (why I chose zxcvbn).

The complete series of posts will cover:

  • Getting started with passport.js
  • Adding in users
  • Handling login, logout, and registration from a single page app
  • Adding in robust remember-me functionality
  • Protecting against CSRF
  • Protecting against XSS
  • Adding a reasonably robust remember-me to passport js
  • Enhancing security by adding some simple HTTP headers
  • Setting up to always redirect to SSL
  • Pushing our app to Heroku
  • Throttling login attempts

Let’s get started

The code for this part is at:

git clone https://github.com/studds/secure-spa-part-2.git

Change in to the secure-spa-part-2 directory and run

npm install
grunt dbseed

See Part 1 for a detailed description
Now run:

node app.js

… and take a look at http://localhost:3000/signup
Passport-Local Example-Signup
Here’s out new signup screen. Let’s have a look at the code…

The create user function

A create user function has been added inside of config/pass.js. This function is where we enforce our password rules, and only then create the user in the database. Note that we require password1 and password2 in this function, the idea being that this forces anyone using this function to confirm the password with the user (or at least forces them to knowingly choose not to.)

On line 11, we define the MIN_PASSWORD_SCORE required. This is on a scale of 0 – 4 defined by zxcvbn. 2 here equates to a crack time of between 3 and 300 hours.

On line 18, we’ve got a simple check that the passwords provided are in fact the same, or otherwise we return an error. This error message will be displayed to the user.

On lines 20 and 21, we use zxcvbn to calculate the score. If the score is less than MIN_PASSWORD_SCORE, then we return an error (again, this will be displayed to the user.)

Finally, from line 22 on, we save the user to the database.

var passport = require('passport')
  , LocalStrategy = require('passport-local').Strategy
  , db = require('../config/dbschema')
    , zxcvbn = require("zxcvbn");

// Minimum password score based on scale from zxcvbn:
// [0,1,2,3,4] if crack time (in seconds) is less than
// [10**2, 10**4, 10**6, 10**8, Infinity].
// (useful for implementing a strength bar.)
const MIN_PASSWORD_SCORE = 2;

// snip

// Helper function to create a new user
passport.createUser = function(username, emailaddress, password1, password2, adm, done) {
    // convert adm string to bool

    if (password1 !== password2) return done(new Error("Passwords must match"));

    var result = zxcvbn(password1);
    if (result.score < MIN_PASSWORD_SCORE) return done(new Error("Password is too simple"));
    var user = new db.userModel({ username: username
        , email: emailaddress
        , password: password1
        , admin: adm });

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

};

Adding the routes

To get the signup work, we need to add two new routes. Firstly, a simple route to render the signup template (line 3). Secondly, a route to actually create the new user (line 7). At line 9 on, we’re pulling the parameters from the createUser function out of the request body. Note the two separate password fields. At line 15, we’re checking whether the error was a type 11000, which is the MongoDB error for a duplicate key (in this case, duplicate user), or otherwise passing the error message straight through to the user. In a production system, we’d probably want to be more sophisticated in our error handling.

// snip

exports.getsignup = function(req, res) {
    res.render('signup', { user: req.user, message: req.session.messages });
};

exports.signup = function (req, res) {
    var body = req.body;
    pass.createUser(
        body.username,
        body.email,
        body.password,
        body.password2,
        false,
        function (err, user) {
            if (err) return res.render('signup', {user: req.user, message: err.code === 11000 ? "User already exists" : err.message});
            req.login(user, function (err) {
                if (err) return next(err);
                // successful login
                res.redirect('/');
            })
        })
}
1

Then we add these routes to app.js.

1
// snip

app.use('/public/zxcvbn', express.static('node_modules/zxcvbn/zxcvbn'));

// snip

// Signup pages
app.get('/dmz/signup', user_routes.getsignup);
app.post('/dmz/signup', user_routes.signup);

// snip

Note at line 3 that we’re also adding in an additional static middleware to make the zxcvbn scripts available to the front-end. Which brings us to…

Client side password checks

A key goal is using the same code to do both the client and server password strength checks. This is why we’ve exposed the zxcvbn scripts above. At line 5, we’re including zxcvbn-async.js file, which will retrieve the main zxcvbn file – this is the recommended way of including zxcvbn.

<% include header %>
<% if (message) { %>
<p><%= message %></p>
<% } %>
<script src="/public/zxcvbn/zxcvbn-async.js"></script>
<form action="/dmz/signup" method="post">
	<div>
	<label>Username:</label>
	<input type="text" required name="username"/><br/>
	</div>
	<div>
	<label>Email:</label>
	<input type="email" required name="email"/><br/>
	</div>
	<div>
	<label>Password:</label>
	<input id="password" type="password" required name="password"/>
	<input id="strength" class="weak" type="text" value="weak" disabled />
	</div>
	<div>
	<label>Repeat password:</label>
	<input type="password" required name="password2"/>
	</div>
	<div>
	<input id="submit" type="submit" value="Submit" disabled />
	</div>
</form>
<script src="/public/javascript/check-password.js"></script>
<p><small>Hint - some strong password are correcthorsebatterystaple, rWibMFACxAUGZmxhVncy, Ba9ZyWABu99[BK#6MBgbH88Tofv)vs$w</small></p>
<% include footer %>

At line 28, we include check-password.js:

var password = document.getElementById("password"),
    strength = document.getElementById("strength"),
    submit = document.getElementById("submit");
password.addEventListener('keyup', function () {
    var score = zxcvbn(password.value).score;
    if (score < 2) {
        strength.value = strength.className = "weak";
        submit.disabled = true;
    }
    if (score === 2) {
        strength.value = strength.className = "so-so";
        submit.disabled = false;
    }
    if (score > 2) {
        strength.value = strength.className = "strong";
        submit.disabled = false;
    }
})

This defines a simple event listener that enables and disabled to submit button and changed the class and value of the strength indicator.

Wrapping up

Today, we’ve added a user signup page, complete with passport strength tests that are enforced both on the client and the server. Next up, we’ll change the front-end to an Angular SPA.


Sign up for danielstudds.com
* = required field

One Comment on “Adding a registration form (Secure SPA Part 2)”

  1. 1 Daniel Studds » Blog Archive » Basic Angular front-end for authentication (SPA Part 3) said at 11:03 pm on May 7th, 2013:

    [...] Adding in users [...]


Leave a Reply