Authentication

Up to this point, each of the web apps we’ve built has a singular flaw - they are completely accessible to anyone who can reach their address. Any use can modify any of our data using the methods we’ve provided. What we need is an authentication system that can determine what users are allowed to do.

HTTP Authentication

The original method for authentication is known as http authentication (because it is provided for in the HTTP standard), and expects a password and username to be submitted as part of the HTTP request header, which can be in one of two forms: Basic or Digest.

Basic Authentication

For Basic Authentication, the client includes in its request headers Authorization: Basic followed by a Base64 encoded string in the form username:password. If no authorization is provided, the server can challenge the browser to provide http authentication by sending a 401 “Unauthorized” status code and a WWW-Authenticate field. In Node, this would look like:

// A simple Basic Authentication server example function handleRequest(req, res) { // Check for authentication var auth = req.headers.authorization; if(auth) { var b = new Buffer(auth.split(' ')[1], 'base64'), s = b.toString(), credentials = s.split(':'), username = credentials[0], password = credentials[1]; // TODO: check username/password pair, and if valid allow access to resource console.log(username, password) res.writeHead(200, {"Content-Type":"text/html"}); res.end("Welcome " + username + "!"); return; } // If no authentication was provided, serve a 401 error and request Basic Authentication res.writeHead(401, {'WWW-Authenticate': 'Basic'}); res.end() } new require(‘http’).Server(handleRequest).listen(80);

If the client does not send a username and password, then the 401 status message with a WWW-Authenticate header is served. In most browsers, this triggers a dialog prompting the user for a username and password, which is then re-submitted. This pair is added to the header of the request, and can be parsed out server-side by decoding the Authentication string from base64. The unencoded string should be in the form username:password, and we can then check this pair against a server-side list of allowed users.

We can also skip the challenge step by providing the username and password directly in the URL, as: http://username:password@localhost (or appropriate host name).

Security Concerns

Basic authentication sends our username and password as plain text! (Encoding it as base64 doesn’t fool anyone - if they’re sophisticated enough to be sniffing packets, they understand basic authentication and can easily decode your username/password pairs). You should therefore only use basic authentication in https. Also, browsers typically cache username/password pairs used in basic authentication, to prevent the user from needing to resubmit every time they make a request. There is no logout option for saved credentials. While the browser cache can be flushed, it is unlikely all your users will understand how to do so.

Digest Authentication

In digest authentication, the client sends a cryptographically hashed username/password pair, again in the form username:realm:password (realm is provided by the server as part of its 401 ‘WWW-Authenticate’ header. The server also provides a nonce value, a unique string generated each time the server responds with a 401 status. Full details can be found in rfc2617.

While digest authentication no longer sends plaintext passwords, it can still be captured and used for man-in-the-middle attacks, and is subject to other security critiques as well. Given these clear drawbacks, why would we want to use Basic or Digest Authentication? Plainly put, it is the simplest option that fits within the “stateless” nature of HTTP.

Bringing State to a Stateless Protocol

As the Internet grew, the limitations of its “stateless” design became apparent. Authentication is one very clear example of this limitation - to overcome the lack of state, HTTP authentication requires the client to send their credentials every time a request is made! (even if the browser saves the username and password for you, it is being sent with every request).

Similarly, there are other cases you may want to keep some idea of state between requests. Consider a website that allows you to place items in a shopping cart. How could you keep track of the contents of the cart without state?

The only real answer is to keep sending the full list of items in the shopping cart to the server with every new request, and have the server return the list of items in every response.

Enter Cookies

This is exactly what the technology referred to as “cookies” does. A cookie is small bit of data the server sets on a client with a Set-Cookie header in the response. The client then is expected to save the cookie locally, and send it back in any successive requests (provided the browser supports cookies and they are turned on).

A cookie consists of three parts - a name, a value, and zero or more attributes. Cookies can be set in Node by passing them to the writeHead method as part of the Set-Cookie key (for multiple cookies, the value of Set-Cookie should be an array of cookie strings):

function handleRequest(req, res) { console.log(req.headers.cookie) res.writeHead(200, { "Content-Type":"text/html", "Set-Cookie": [ "quote=cookies%20are%20for%20me;", "sessionid=212;Expires=Wed, 09 Jun 2021 10:18:14 GMT", "safe=value;HttpOnly" ] }); res.write('<img src="https://muppetmindset.files.wordpress.com/2010/12/cb45d-cookiesitting.jpg" alt="C is for cookie" />') res.end(req.headers.cookie) } new require('http').Server(handleRequest).listen(80);

Setting the attributes of a cookie are powerful ways of controlling their behavior. Domain is used to identify the domain associated with the cookie, i.e.

Set-Cookie: Domain=.cis.ksu.edu

Will instruct the browser to only send this cookie to servers in the cis domain. This is especially important to set in production environments. Path works similarly, but for particular subpaths within a domain.

Expires sets a time at which the cookie should be flushed from the browser, specified in the form Wdy, DD, Mon YYYY HH:MM:SS GMT. Max-Age is similar, but specifies a time in seconds since the cookie was received. Cookies without expiration times are considered session cookies, and should be deleted by the browser when it is closed.

Finally, Secure lets the browser know that the cookie should only be sent over secure channels, and HttpOnly indicates that the cookie should not be accessible via non-http functions (i.e. client-side JavaScript). Also, Secure and HttpOnly do not have associated values - the presence of the key is all that is needed.

Limits of Cookies

Cookies clearly do not work if the browser does not support cookies or cookies are turned off (both are unusual today). Also, cookie-supporting browsers are only asked to support cookies of up to 4,096 bytes and up to 50 per domain. Cookies also represent additional data being sent back and forth from the browser, so keeping them manageable is often a priority.

Also, most browsers cache cookies as plain-text files. For this reason, it is recommended that sensitive data not be included in cookies, or that if it is included, it be done in an encrypted form.

Encryption

Before we go too much farther into authentication, we need to spend some time talking about encryption - applying mathematical functions to our data in an effort to protect it from malicious users and hackers. There are many different styles of encryption, and many different algorithms, but they are all based on the idea of a transformation that is difficult and cost-prohibitive to undo. The two forms we’ll deal with today are digests and ciphers.

Digest

A digest (or cryptographic hash) is a one-way function - you put in the text (or other data) you want to encrypt, and you get out a cryptographic string. There is no (easy) way to retrieve the original data from the cryptographic string - and it is a lossy process, so much of the data may be discarded - hence the name ‘digest’. Digests are primarily used in two ways. First, they are used to verify data (such as an email body or an executable file). The user sending the data also creates a digest and sends it as well. The receiver can take the received data, run the same hashing function on it, and compare the results to the provided digest. If they match, the original data has not been altered. If not, the original data may have been corrupted or tampered with. Secondly, digests are used to store passwords in a secure form. A system can check a password’s validity by hashing it and comparing the result with the stored, encrypted password.

Ciphers

Ciphers, on the other hand, are reversible functions that take a key value. You can encrypt a string or other data using a cipher function with a specific key, and then decipher it using the same key. This is the basis of ssl communication - the client and server share a key that is used to encrypt and decrypt https requests and responses (The challenge is, of course, establishing the shared key, ensuring the key does not get intercepted, and making certain your client is talking to the right server - all of which is handled through a handshake process and ssl certificates).

Encryption Module

We’ll need to use both methods in our authentication system. Thankfully, Node provides a powerful cryptographic library, crypto. But like most of Node, it has more power than we really need, so we’ll wrap it in a module that provides the more basic functionality we want - creating digests, ciphering and deciphering, and creating random salt tokens. This also provides us with a single location for storing keys and secrets (though in production code we’d want to move these into a configuration file).

// encryption.js "use strict" // A randomly-generated secret string const secret = '93apiepovineaoi309uuaphgoanlane4o;aij4o8*(%WP*(P(NUV)))' // The algorithm to use in cyphering const algorithm = 'aes-256-ctr' const crypto = require('crypto'); class Encryption { // Creates a random value for use as salt salt() { return crypto.randomBytes(32).toString('hex').slice(0,32); } // Creates a cryptographic hash of the provided // plaintext, with additional salt using a module // specific secret digest(plaintext) { const hash = crypto.createHash('sha256'); hash.update(plaintext); hash.update(secret); return hash.digest('hex'); } encipher(plaintext) { const cipher = crypto.createCipher(algorithm, secret); var encrypted = cipher.update(plaintext, 'utf8', 'hex'); encrypted += cipher.final('hex'); return encrypted; } decipher(crypttext) { const decipher = crypto.createCipher(algorithm, secret); var deciphered = decipher.update(crypttext, 'hex', 'utf8'); deciphered += decipher.final('utf8'); return deciphered; } } module.exports = exports = new Encryption();

Cookies allow us to overcome the biggest weakness of stateless protocols in authentication - the need to send credentials with every request. Instead, once the user has been authenticated, we can send a cookie representing their login, and check it on every successive access.

The User

We first need a way of keeping track of our users. If our website is going to be any size, it makes sense to store users in our database, just like any other resource. A User table will typically have a username and a password field, as well as any role information we need to store (such as a boolean value for admin, or possibly a number or string representing the roles held by the user.

However, we should never store passwords as plain text - if we do, and our database is ever breached, then we’ve just given up all our users’ passwords. And as many users re-use usernames and passwords, this can be a very serious issue. Therefore, we store passwords as an encrypted hash - applying a one-way encryption process to the password to create a token that cannot (or at least, is very difficult) to decrypt.

To make this even more secure, we can go a step father and concatenate a second, random string to the password before we encrypt it, known as a salt. This helps defend against dictionary attacks (rainbow tables), but we’ll need to know the salt in order to recreate the encrypted password. For this reason, we’ll store it in the user table as well:

CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT UNIQUE, admin BOOLEAN, crypted_passowrd TEXT, salt TEXT)

We can add even greater level of protection by using a two-part salt; one part is specified in the code (and the same for all passwords) and the other is stored in the user database on a per-user basis. This way, an attacker needs to obtain both the database and the source code to have a chance at recreating the code. Both the digest function and a salt-generation function are defined in our encryption module, so we can use it to create a default user in our database:

// in seeds.js var encryption = require(‘../encryption’); // Create a default user var salt = encryption.salt(); db.run("INSERT INTO users (username, admin, crypted_password, salt) values (?,?,?,?)", 'admin', true, encryption.digest('insecurepassword' + salt), salt ); // Log contents of the user table to the console db.each("SELECT * FROM users", function(err, row){ if(err) return console.error(err); console.log(row); });

When we run the seed file, you should see printed to the console the user structure, with crypted password and salt fields as strings of apparent gibberish:

{ id: 1, username: 'admin', admin: 1, crypted_password: '7eeb8859bc15c216f94c08124fb86c5f35ed8d96ea243a13a0a3f8478c175f76', salt: 'b2a83d7630f62ac53ea4a6faf9c1c68b' }

This is exactly what a hacker who manages to get a copy of our database will be faced with. Much more secure than storing passwords in plaintext. One final possible improvement is the addition of a pepper; like a salt it is randomly generated on a per-password basis. Unlike salt, though, it is not stored, so it must be re-found via iterating through all the possible values and re-hashing, looking for a match (thus, the pepper should be a small range of possible values). While pepper adds a deeper level of security, it is inappropriate for most web applications due to the increased time complexity.

We’ll eventually want to create a User controller and restful routes, but for now let’s move on to authenticating our user.

Session Management

We’ll want to create a login and logout route, a corresponding session controller, and templates. For now, let’s create a simple login form in templates/session:

<!-- /templates/session/new.html --> <form action="/login" method="POST"> <div id="message"> <%= (params.message) ? params.message : “” %> </div> <label for="username"> Username: <input type="text" name="username"/> <br/> </label> <label for="password"> Password: <input type="password" name="password"/> <br/> </label> <input type="submit" value="Sign In"/> </form>

Note that we dynamically add a message to the page in the message div - if that is undefined, then the empty string is used (and the div is for all intents and purposes, invisible to the user). This will be used by our controller.

We need to create a session controller to manage our sessions. We can consider our session to be a CRUD resource, needing a new, create, and destroy action, so let’s use that structure. The new action is easy - just render our template:

// in controllers/session.js // Render the login form new(req, res) { res.writeHead(200, {"Content-Type":"text/html"}); res.end(view.render('session/new')); }

But create is a bit more involved - we need to parse the submitted form, authenticate by hashing the provided password with our stored salt, and then comparing it with our saved password. If the two match, we log the user in by creating a session cookie containing their id. Because cookies are often stored as plain-text files by the browser, we’ll want to take an additional step and encrypt the value we store. And, if anywhere in the process this goes wrong, we’ll want to serve the login page again, with the message that logging in was unsuccessful (unlike most aspects of user interface design, we’ll be deliberately vague as to why it failed here, as such information can be immensely useful for a hacker).

// Attempt to login the user create(req, res) { // Helper error function for responding to the client function respondWithError() { res.writeHead(403, {"Content-Type":"text/html"}); res.end(view.render('session/new', {message: "Incorrect username/password. Please try again."})) } // Parse the incoming form var form = new formidable.IncomingForm(); form.parse(req, function(err, fields, files) { // Find the supplied username in the database db.get("SELECT * FROM users WHERE username=?", fields.username, function(err, user) { if(err) { console.error(err); return respondWithError(); } if(!user) { console.log("Username " + fields.username + " not found in db"); return respondWithError(); } if(user.crypted_password == encryption.digest(fields.password + user.salt)) { // password digests match! Create the session // by storing the user's id in a session cookie var sessionData = {user_id: user.id}; var cookieData = encryption.encipher(JSON.stringify(sessionData)); res.writeHead(200, {"Content-Type":"text/html", "Set-Cookie":"session=" + cookieData + "; httpOnly" }); res.end("Logged In Successfully"); } else { // Login failed because username/password was not a match return respondWithError(); } }); }); }

Note that our cookie holds our session data as an encrypted JSON string. This helps protect the security of our site, as cookies can be captured on client machines, and are often stored by browsers as plain-text files. We’ll need to both decrypt and parse the JSON string when it returns to us in future requests (we’ll cover that in a bit).

Finally, we’ll need to flush the cookie when the user logs out, by setting it to the empty string:

destroy(req, res) { response.writeHead(200, {"Content-Type":"text/html", "Set-Cookie":"session=; httpOnly" }); response.end("Logged Out Successfully"); }

And the last step is setting up our login/logout routes in app.js:

// in app.js // Add routes for logging in and out var session = require('./controllers/session'); router.addRoute('/login', 'GET', session.new); router.addRoute('/login', 'POST', session.create); router.addRoute('/logout', 'GET', session.destroy);

Now we can run our server, and even log in and out. However, outside of our login and logout methods, the session cookie is unused. Accessing the Session on the Server What we would really like is to load the session cookie, decipher it, parse it, and provide it as an additional argument to our controllers - much like the params argument that we parse out of our request url. Thus, the router is probably the appropriate place for tackling this issue. Let’s add a getSession() function to our router.js file:

function loadSession(request) { var cookie = request.headers.cookie; // If there is no cookie, there is no session yet if(!cookie) return {}; // If there is a cookie, grab the session cookie data var match = /session=(.+)/.exec(request.headers.cookie) if(match) { console.log('cookie', cookie, 'match', match[1], 'deciphered', encryption.decipher(match[1])); return JSON.parse(encryption.decipher(match[1])); } return {} }

This function takes the request object, checks for the session cookie, and if it finds one, extracts it and transforms it back into a JavaScript object. We could modify our route() function next to capture this change, and pass it as an extra argument to our route callback:

// Routes a request to the matching callback function // using the mappings stored in the route table function route(request, response) { var verb = request.method.toLowerCase(); var path = request.url.split("?")[0]; // Iterate through the routeMap looking for a matching route for(var i = 0; i < routeMap[verb].length; i++) { // Use the route's regular expression to pattern match // against the path var match = routeMap[verb][i].regexp.exec(path); if(match) { // A match was found, so populate the params object by // iterating over the match's capture groups and assigning // them to the corresponding key (correspondence is by order) var params = {}; for(var j = 0; j < routeMap[verb][i].keys.length; j++) { var key = routeMap[verb][i].keys[j] params[key] = match[j+1]; } // We also want to load the session data as a object var session = loadSession(request); // Exit control flow and trigger the callback, passing // on our request, response, and params objects. Also, // use *call()* to pass on the 'this' context, which is // custom-populated by http.Server return routeMap[verb][i].callback.call(this, request, response, params, session); } } // If we reach this point in our program execution, it means // that there was no match for our route. Respond with a 404. response.writeHead(404, {'Content-Type':'text/html'}); response.end("<h1>Resource Not Found</h1>"); }

Now our session will be loaded and available in every response handler called by the router.

This is enough to get authentication up and running, and to provide a very simple session implementation; but it has several drawbacks. First, we can only add a user_id to the session - we can’t currently use it to store anything else without first repackaging our session as a cookie and sending it with a request. Second, we probably don’t want to load a user_id as much as we’d like to retrieve the actual user from the database. This can be done with another function in our router, but it would pollute what is essentially framework code with application-specific code (unless you think all web applications need users). And finally, we are doing nothing to control access to any of our pages - regardless if a user is logged in or not, though we could add that kind of code to each and every request handler... but that is back to a lot of boilerplate. In fact, what we would like to see is more of a pipeline approach where we can specify a series of functions flowing into one another as a route, which we’ll tackle next time.