To maintain modularity and simplify the authentication process, we will create a separate module to validate the access privileges of a given user.
In your project directory, add the following file named authentication.js
. Open the file and insert the following:
var db = require('./database'); module.exports = { database: 'OrderBase', collection: 'AccessTokens', generateToken: function (user, callback) { var token = { userID: user._id } } // Persist and return the token db.insert(this.database, this.collection, token, function (err, res) { if (err) { callback(err, null); } else { callback(null, res); } }); }, authenticate: function (user, password, callback) { if (user.password ==== password) { // Create a new token for the user this.generateToken(user, function (err, res) { callback(null, res); });}); } else { callback({ error: 'Authentication error', message: 'Incorrect username or password' }, null); } } }
Next, import the module into your entry module, as follows:
var authentication = require('./authentication');
We will need to add endpoints to our API for the purpose of both creating and authenticating users who wish to interact with it. In light of what we have done thus far, this is easy to do.
We begin by adding a URL endpoint for adding users. This will be very familiar in terms of what we already did when creating the REST API in the previous chapter; all that we are going to do is create a POST
method for the user
collection. First, add the following utility method:
var insertUser = function (user, req, res) { insertResource('OrderBase', 'User', user, function (err, result) { res.writeHead(200, {"Content-Type": "application/json"}); res.end(JSON.stringify(result)); }); };
Next, modify your router to include the following case
statement:
case 'api/users/register': if (req.method === 'POST') { var body = ""; req.on('data', function (dataChunk) { body += dataChunk; }); req.on('end', function () { // Done pulling data from the POST body. // Turn it into JSON and proceed to store. var postJSON = JSON.parse(body); // validate that the required fields exist if (postJSON.email && postJSON.password && postJSON.firstName && postJSON.lastName) { insertUser(postJSON, req, res); } else { res.end('All mandatory fields must be provided'); } }); } break;
This is all we need to register users. Registrations can now be handled through a simple POST
request to the /api/users/register
endpoint.
To enable users to log in via our API, we will need to accomplish the following three things:
Luckily, all but the first of the preceding list are taken care of by the authentication module that we designed earlier. All that we need to do is plug it into our router. To do this, we will also need to design a new endpoint for the login part.
Add the following case
to your router configuration:
case 'api/users/login': if (req.method === 'POST') { var body = ""; req.on('data', function (dataChunk) { body += dataChunk; }); req.on('end', function () { var postJSON = JSON.parse(body); // make sure that email and password have been provided if (postJSON.email && postJSON.password) { findUserByEmail(postJSON.email, function (err, user) { if (err) { res.writeHead(404, {"Content-Type": "application/json"}); res.end({ error: "User not found", message: "No user found for the specified email" }); } else { // Authenticate the user authenticator.authenticate( user, postJSON.password, function(err, token) { if(err) { res.end({ error: "Authentication failure", message: "User email and password do not match" }); } else { res.writeHead(200, {"Content-Type": "application/json"}); res.end(JSON.stringify(token)); } }); } }); }); } else { res.end('All mandatory fields must be provided'); } }); } break;
In the preceding code, we added the following simple method in order to handle the looking up of a user by e-mail:
var findUserByEmail = function (email, callback) { database.find('OrderBase', 'User', {email: email}, function (err, user) { if (err) { callback(err, null); } else { callback(null, user); } }); };
That's all we need as far as user management is concerned for now. Now, let's add the finishing touch and set up the actual security for our endpoints.
We are now ready to modify our API in order to add the authentication features that we have developed so far. First, let's determine exactly how the access policies should work:
insert
) orders and retrieve (get
) information about products and nothing elseWe will accomplish this by placing a simple token and role check on each endpoint. The check will simply verify the following:
To start, we will add a new function to the authentication
module, which will be responsible for checking whether a given token is associated with a given role:
tokenOwnerHasRole: function (token, roleName, callback) { var database = this.database; db.find(database, 'User', {_id: token.userID}, function (err, user) { db.find(database, 'Role', {_id: user.roleID}, function (err, role) { if(err){ callback(err, false); } else if (role.name ==== roleName) { callback(null, true); } else { callback(null, false); } }); }); }
This method is all that we need to verify the roles for the token provided (implicitly checking whether the user who owns the token has the specified role).
Next, we simply need to make use of this in our router. For example, let's secure the POST
endpoint for our product API. Make it look like the following:
case '/api/products': if (req.method === 'GET') { // Find and return the product with the given id if (parsedUrl.query.id) { findProductById(id, req, res); } // There is no id specified, return all products else { findAllProducts(req, res); } } else if (req.method === 'POST') { var body = ""; req.on('data', function (dataChunk) { body += dataChunk; }); req.on('end', function () { var postJSON = JSON.parse(body); // Verify access rights getTokenById(postJSON.token, function (err, token) { authenticator.tokenOwnerHasRole(token, 'PRODUCER', function (err, result) { if (result) { insertProduct(postJSON, req, res); } else { res.writeHead(403, {"Content-Type": "application/json"}); res.end({ error: "Authentication failure", message: "You do not have permission to perform that action" }); } }); }); }); } break;
That's it! Implementation for the other endpoints is the same, and we will provide you with the full example source code for them.
Though I have covered some basics here, security remains one of the largest and most diverse areas of contemporary software development. We believe that token-based authentication will address a majority of the cases that you are bound to come across in your career. I would like to offer some suggestions for future study as well as complements to the topics that you have studied here.
One of the most common authentication standards offered by modern web apps is OAuth (Open Authentication Standard), its second version (OAuth2) in particular. OAuth makes heavy use of access tokens and is used by (among others) Facebook, Google, Twitter, Reddit, and StackOverflow. Part of what makes the standard powerful is that it allows users to sign in with their Google or Facebook accounts, or even some other account that supports OAuth2, when using your services.
There are several mature NPM packages for using OAuth2 with Node.js. In particular, we recommend you to study the node-oauth2-server package (https://github.com/thomseddon/node-oauth2-server).
To keep things simple and focus on the main concepts, we have allowed our access tokens in this example to be permanent. This is a very bad security practice since tokens, like passwords, can be compromised and used to grant unauthorized users access to the system.
A common way to reduce this danger is to impose a Time To Live (TTL) value on each access token, indicating how long the token can be used until the user has to authenticate themselves again in order to get a new token.
For the sake of simplicity, we allowed passwords in this example to be stored and retrieved as plain text. Needless to say, this is an abysmal security practice and nothing that you should ever do on a production server. Mature Node.js frameworks such as Express.js provide built-in mechanisms for hashing passwords, and you should always choose those when available. In the event that you need to hash passwords on your own, choose the bcrypt
module in order to both hash and compare. Here's an example of the same:
var bcrypt = require('bcrypt'); var userPlaintextPassword = "ISecretlyLoveUnicorns"; var userHashedPassword = ""; // First generate a salt value to hash the password with bcrypt.genSalt(10, function(err, salt) { // Hash the password using the salt value bcrypt.hash(userPlaintextPassword, salt, function(err, hashedPassword) { // We now have a fully hashed password userHashedPassword = hashedPassword; }); }); // Use the same module to compare the hashed password with potential //matches. bcrypt.compare("ISecretlyLoveUnicorns", userHashedPassword, function(err, result) { // Result will simply be true if hashing succeeded. }); bcrypt.compare("ISecretlyHateUnicorns", userHashedPassword, function(err, result) { // result will be false if the comparison fails });
3.21.246.223