Contents


Manage account approval in an OpenWhisk application

Minimize the chances for brute-force hacking attempts

Comments

In many circumstances, such as business-to-consumer applications, it is best to have users register on their own and then have an administrator approve or reject the account or specific account permissions. In this tutorial, I teach you how to implement such a system. This is primarily a means to prevent hackers from entering the system through brute force methods.

The application in this tutorial is based on OpenWhisk, IBM's serverless architecture. In contrast to normal Node.js applications, which require a constantly running process, a serverless architecture runs the process only when needed, resulting in a much lower cost.

What you'll need to build your application

Run the appGet the code

This tutorial shows a very simplistic application that allows users to register on their own and then have an administrator approve or reject their requests.

Step 1. The Cloudant database

The design of OpenWhisk assumes that any information that is required for the application is stored elsewhere, for example in a Cloudant database. Here are the steps to create a new database:

  1. Log on to the Bluemix console.
  2. Click the menu icon on the upper left and then click Data & Analytics.
  3. Click Create Data & Analytics service.
  4. Click Cloudant NoSQL DB.
  5. Name the service "Acct-approval-db" and click Create.
  6. Go to the service.
  7. Click Service credentials and then click New credential.
  8. Click Add.
  9. Click View credential and copy the URL value into a text file. The URL encodes the host name, port, user ID, and password.
  10. Click Manage and then click LAUNCH.
  11. Click Databases on the left (use the <-> icon to see the meanings of the various icons, if necessary).
  12. Click Create Database and name your database accounts.

Step 2. The self-registration actions

Most functions require two OpenWhisk actions. The first provides the user with a form to complete, and the second responds to that form after it is filled in. Here is how you do this:

  1. Click the menu icon on the upper left and then click Functions.
  2. Click Start Creating.
  3. Click Create Action.
  4. Click Create a new Package.
  5. Name the package "Acct-approval" and click Create.
  6. Name the action "self-reg-form" and click Create. Don't worry about enabling as a web action; the API definition will do that later.
  7. Replace the source code with the code from my self-registration form script.
  8. Click Manage in the upper left to return to the action management page.
  9. Click Create Action.
  10. Name the new action "self-reg-handler", select the package Acct-approval, and click Create.
  11. Replace the source code with the code from my request form handler script.
  12. Click Manage in the upper left to return to the action management page.
  13. Click APIs on the left sidebar and then click Create Managed API.
  14. Name the API "acct-approval". You don't need to worry about specifying a base path.
  15. Click Create operation.
  16. Create these two operations:
    ParameterFirst operationSecond operation
    Path /self_reg_form /self_reg_handler
    Verb GET POST
    Package containing action Acct-approval Acct-approval
    Action self-reg-form self-reg-handler
    Response content type text/html text/html
  17. Scroll down and click Save & expose.
  18. Copy the route for the API.
  19. Browse to <your URL>/self_reg_form. (View my form.)

The self-registration form

The JavaScript code for the self-registration form is pretty self-explanatory, although a couple of parts might require some discussion:

  • The action sends the HTML to OpenWhisk a parameter (named "html"). Here, this parameter is a template literal (see "Related topics" below), a string that starts and ends with a backtick (`). This kind of string can encompass multiple lines, which makes the HTML a lot more readable.
    	return { html: `
    	    <html>
    		.
    		.
    		.
    	    </html>
    	` };
  • The action uses Bootstrap 3.3 with the default theme for the look and feel. All the other actions in this tutorial also use it.
    <link rel="stylesheet" 
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>

The self-registration handler

The form handler is more complicated. First, it needs to have a database connection. You need to replace the cloudantUrl value with the one for your database.

// Replace with your value
var cloudantUrl = "https://<<redacted>>-b18c-93f1b9986506-bluemix.cloudant.com";

The commands to connect to the database are best placed outside the main function. After an action is executed, the OpenWhisk system keeps the process running for a few minutes until it needs the resources. If during that time another call comes, OpenWhisk just reruns main with new parameters. Instead of creating a new connection to the database each time, you can have the connection as a global variable and create it only once.

var cloudant = require("cloudant")(cloudantUrl);
var mydb = cloudant.db.use("accounts");

The main function receives the values from the form in the parameter params. But it cannot respond right away, because it needs to communicate with the database, which is an asynchronous process. When OpenWhisk actions need to perform such an asynchronous process, they return a Promise object (see "Related topics" below).

The constructor for a Promise object receives one parameter, a function. That function receives two callback functions, one to call if it is successful and another to call if it fails. This function is executed at some point asynchronously by the OpenWhisk system. At the end of some callback, it will call either the success function or the failure function, and the action will terminate.

return new Promise(function(success, failure) {
.
.
.

    // Return the response             
    success({html: reqStatus(body.status, params.uid)});
  });   // mydb.get
});   // Promise object

The first thing the Promise function does is check if there is already an account by that name. If so, the callback returns the appropriate response.

mydb.get(params.uid, function(err, body) {
.
.
.
            
// Return the response 
      success({html: reqStatus(body.status, params.uid)});
});    // mydb.get

The appropriate response is determined by the reqStatus function. This is a very simple function, which uses the statusResponse table to decide which response to provide. This function uses another feature of template literals. You can use the syntax ${<expression>} inside a template literal, and it is replaced with the expression result.

// Table of statuses and their messages
statusResponse = {
  "unapproved": "Your request is waiting, please have some patience",
  "approved": "Good news, we approved your request",
  "rejected": '<b style="color:red">Rejected </b>. ' + 
'We don\'t want your business',
  "submitted": "Your request has just been submitted," + 
" it is awaiting approval"
};

reqStatus = function(status, user) {
    return `
        <html>
.
.
.           <body>
                <p>Hello, ${user}. ${statusResponse[status]}</p>
            </body>
        </html>
    `;
}

There are two possible ways for mydb.get to return an error. The first is if there is no such account, which is what you'd expect. The other is if there really is an error condition. If there is an error with error code 404, it means no user has been found. In that case you create an entry for the new user. The identifier for the new entry is the user ID. The if statement ends with a return to get out of the function (that is necessary because the call to success is in a callback).

// No user yet, register the entry
if (err != null && err.statusCode == 404)  {
mydb.insert( {
     		_id: params.uid,
pwd: saltHashPassword(params.pwd),
           status: "unapproved"
},   // Data to insert
function() {
     		success({html: reqStatus("submitted", params.uid)})    
      });   // mydb.insert                
                return ;
}   // No user yet

It is bad practice to store unencrypted passwords. This action uses hashing with salt, which is the industry standard. You can read about it in "Salt Hash passwords using NodeJS crypto."

Step 3. The approval action

The next step is to create the action to approve users. Create a new action called admin-approval-form, using Create 03_approval.js. Create a new API operation called "/admin_approval_form" that responds to GET requests by running the action.

You can go to the URL to see this (see my version). You will see a list of UIDs and two buttons for each: one to approve and one to deny.

How does it work?

This functionality has only one action and one web page. If called with the proper parameters, it modifies the database entry to approve or deny a request. Either way, it responds with the new table of requests.

It is important that only authorized administrators be able to approve or deny requests. To simplify the software, I wrote it assuming that only a single administrator is active at a time, and that he takes only a few minutes to decide. This ensures that the table comes from the same run of the application as the one that receives the decision.

With those assumptions, it is enough to keep track of two keys. The old key is the one from the previous call to main, which appears in any authorized request. The new key is the one in the web page you're returning to the user, so it appears in authorized requests to the next call.

var oldKey, newKey = genRandomString(15);


function main(params) {
    oldKey = newKey;
    newKey = genRandomString(15);
.
.
.
}

This function can be used in a Promise object. It returns the web page with the new list of users.

var responseFunc = function(success, failure) {

The .find method lets you do a query on a Cloudant database. In this case, you select for documents whose status field is equal to unapproved. See the Cloudant Query section on the Bluemix Docs website for help with constructing more complicated queries.

mydb.find({
     		"selector": {
                "status": "unapproved"
           }
     },

The callback function receives two parameters, the error condition (if any) and a list of entries that match the selector. The user identifier is one of the parameters in the structures in the list, _id (that is the reserved name for the document identifier in Cloudant). The map method (see "Related topics" below) runs a function on every entry. The first call creates a list of user identifiers out of the data provided by mydb.find. The second one turns the list of user identifiers into a list of HTML table rows with the necessary buttons to approve and deny requests.

        function(err, data) {
           var userList = data.docs.map(function(item) {
return item["_id"]
});
           var rowsList = userList.map(function(user) {
               return `
                    <tr>
                        <td>${user}</td>

The buttons are really links to the same page, but with the parameters to approve or deny the request. There are three parameters:

  1. key: The new key right now (and will be the old key when the request is received).
  2. action: The action (either approve or reject).
  3. uid: The user identifier. This value comes from users, and therefore could include code injections. To make sure you don't have any such malicious users, you use the querystring module's querystring.escape(str)) function. (See "Related topics" below.)
<td>
<a href="admin_approval_form?key=${newKey}&action=approve&uid=${qs.escape(user)}">
<button class="btn btn-success">Approve</button>
</a>
</td>
<td>
<a href="admin_approval_form?key=${newKey}&action=reject&uid=${qs.escape(user)}">
<button class="btn btn-danger">Deny</button>
</a>
</td>
</tr> `
});

Finally, you check if there are any rows. If not, you return a message. If there are, you use the reduce method to concatenate them. (See "Related topics" below.)

var rows;
            
// No data is a special case
if (rowsList.length > 0) 
rows = rowsList.reduce(function(a, b) {return a+b;});
else
rows = '<tr><th colspan="3">No users to approve</th></tr>';

Now that you have all the rows as one string, you can embed that string in an HTML document and send it as the html parameter when calling the success function. All this happens in the callback of the mydb.find function inside the definition of responseFunc.

    var html = `<html>
.
.
.
    <table class="table">
        <tr>
            <th>User Name</th>
            <td colspan="2"></td>
        </tr>
	    ${rows}
    </table>
.
.
.
   </html> `;            
            
    success({html: html});
	});   // mydb.find

};   // responseFunc

If there is no key, or the key is not the correct old key, then there is no request to process. In that case, the request function you just defined is the correct function for the Promise object.

// If we are not authenticated, then there is no action to take
if (params.key != oldKey)
return new Promise(responseFunc);

If you get past this point, it means the key is accurate. In that case, it is necessary to fulfill the request. To do this, return a Promise object whose function first modifies the status and then runs the response function that returns the table with the pending requests and the buttons to handle them.

if (params.action == "approve")
return new Promise(function(success, failure) {
     		modifyStatus(params.uid, "approved", function() {
           	responseFunc(success, failure);
});   // modify status
      });   // return new Promise

The modifyStatus function retrieves the full entry for the uid, modifies the status, writes the new entry, and in the callback for after the new status is written calls the callback function that calls responseFunc.

var modifyStatus = function(uid, newStatus, callback) {
    mydb.get(uid, function(err, body) {
        body.status = newStatus;

        mydb.insert(body, function(err, data) {
            callback();
        });   // mydb.insert
    });   // mydb.get
};

Step 4. The login actions

Logging in is implemented using two actions, a form and a handler, similar to requests. Create these two:

Action nameAction codeAPI PathAPI Verb
login-form Here /login GET
login-form-handler Here /login_handler POST

The login form just returns a fixed HTML, so it is self explanatory. The login form handler is more complicated, but mostly uses the same constructs as the other actions. The only part that is new is checking the password, which is explained in "Salt Hash passwords using NodeJS crypto."

Conclusion

This tutorial shows a very simplistic application that allows users to register on their own and then have an administrator approve or reject their requests. In a real life application, users are going to be asked for additional information before they register, and possibly some of that information will be verified before ever being seen by an administrator. It is particularly important to get additional information that will allow for a password reset.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Security
ArticleID=1051406
ArticleTitle=Manage account approval in an OpenWhisk application
publish-date=10262017