Contents


Configure your IBM Cloud Node.js authorization proxy to communicate with the user

Know the cause for negative decisions and reverse them

Comments

In a previous article ("Add your own authorization proxy to a third-party app"), you learned how to add an authorization proxy between the user's browser and a third-party application that you don't have control over. One problem with that proxy is that it does not have a way to communicate with the user. Therefore, when the user is not authorized to perform a certain action, the attempt fails without informing the user what went wrong and how to get the action approved.

In this article, you learn how to solve this problem. I show you how to send the user a small script that asks the proxy every second if there are any messages for it. When the proxy receives a message, the message covers the screen until it is acknowledged.

What you'll need to build your application

  • An IBM Cloud account. (Try IBM Cloud for free.)
  • Knowledge of HTML and JavaScript.
  • Knowledge of the MEAN application stack (at least Node.js and Express).
  • A development environment that can upload a Node.js application to IBM Cloud, such as Eclipse.

Run the AppGet the code at JazzHub

In this article, you learn how a proxy can produce pop-up messages to communicate with the user. You send the user a small script that asks the proxy every second if there are any messages for it. When it gets a message, it covers the screen until the message is acknowledged.

Sending messages to users

Sending messages to users requires some code on both the server side and the client side. To follow along with the following steps below, browse to https://github.com/qbzzt/authz-proxy2/blob/master/app.js to read the modified (from the previous article) app.js.

Server side

Several changes are needed in the app.js application, which are for the cookie session identifier, where you store messages, and in sending messages.

Cookie session identifier

  1. On the server side, the first requirement is to be able to distinguish different sessions. The easiest way to distinguish sessions is with a randomly generated cookie. The name of the cookie is a constant, so I am following the convention of naming it in all caps.
    // The name of the cookie to use for sessions
    var SESS_COOKIE = "authproxy_session_cookie";
  2. Storing session IDs in a cookie requires two packages:
    • cookie-parser, which is used to understand cookies coming back from the browser.
    • node-uuid, which creates unique identifiers.
    // Need UUIDs for session identification
    var uuid = require("node-uuid");

    Both packages need to be added to packages.json.
  3. For demonstration purposes, the application sends a message every 10 seconds to the last session that was created. To do this, it is necessary to keep track of the last session cookie that the application sent.
    var lastUuid = "0";
    
    // if you get getMsg.html, set a cookie
    app.get('/getMsg.html', /* @callback */ function (req, res, next) {
    	lastUuid = uuid.v4();
  4. In contrast to reading cookie values, setting a cookie does not require any special package.
        res.cookie(SESS_COOKIE, lastUuid);
  5. The file getMsg.html is stored in the static directory and this call is followed by the call that specifies that directory's files, and are to be served verbatim. The call to next() tells the framework to continue the processing, so it will be served from the directory, but with the cookie you just set.
        next();
    });

Storing messages

  1. When the application needs to send a message to the user, the message has to wait somewhere until the browser code asks for it. Messages are stored in a hash array with the session ID as the key.
    // The messages to send sessions
    var sessionMsg = {};
    
    
    var sendSessionMsg = function(id, html) {
    	sessionMsg[id] = {text: html};
    };
  2. For demonstration purposes, it is useful to send a time-stamped message every few seconds. This call does it every 10 seconds.
    setInterval(function() {sendSessionMsg(lastUuid, "M<b>ess</b>age " + new Date());}, 10000);

Sending messages

Finally, when the browser asks for the message (if any), read the cookie to identify the session, and respond. Delete the message because you need to send it only once.

// If there is a message for the session, respond with it
app.get("/msg", require("cookie-parser")(), function (req, res) {
	var id = req.cookies[SESS_COOKIE];
	var msg = sessionMsg[id];
	if (typeof msg === "undefined") {
		res.send(""); // No message
	} else {
		sessionMsg[id] = undefined;
		res.send(msg.text);
	}
});

Note that this technique blocks some URLs that would otherwise be proxied to the server. Because you are adding authorization for a specific service, it is easy to make sure the URLs you block are not in use by that application. A more generic proxy, on the other hand, would have to have a more sophisticated solution.

Client side

On the client side, you can see a demonstration at public/getMsg.html. This demo uses the Bootstrap and Angular libraries. It includes two parts: the pop-up message (shown only when there is a message), and an iframe, which displays the actual proxy.

Display getMsg.html on GitHub and follow along.

The pop-up message

  1. The ng-show attribute defines the condition for showing the tag. In this case, the condition is that the message not be empty. The ng-click attribute is the command to execute when the tag (in this case, the entire division showing the message) is clicked. The browser deletes the message, which then returns to the hidden state.
     	<div class="fill-window overlay" ng-show="msg !== ''" ng-click="msg = ''">
  2. The messages are provided by the server as HTML, to allow for links and other forms of rich content. However, displaying HTML that you got from the Internet is risky. This is how you get cross-site scripting attacks. Angular allows you to do this, but you have to be very explicit and perform these steps (as shown in the source code):
    1. Import the sanitize library.
          	<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-sanitize.js"></script>
    2. Declare that the module uses that library.
      var myApp = angular.module("myApp", ["ngSanitize"]);
    3. Add $sce as a parameter to the controller function.
      myApp.controller("myCtrl", function($scope, $http, $sce) {
    4. Create a scope variable for $sce.trustAsHtml. This step allows $sce.trustAsHtml to be called from outside the controller creation function.
      $scope.trust = $sce.trustAsHtml;
    5. Use that scope variable to create a value to pass to the ng-bind-html attribute. This is the only HTML inside the pop-up div element.
      		<p ng-bind-html="trust(msg)"></p>
      	</div>

The proxy iframe

The proxy is extremely simple. It's just an iframe element that embeds the actual proxy. Because the proxy is part of the same application and comes from the same domain, there is no need for cross-domain requests.

 	<iframe src="./index.html" class="fill-window">
 	</iframe>

CSS classes

The stylesheet includes two classes: fill-window and overlay.

  1. The fill-window class specifies that the tag should fill the entire window. The position is absolute within the window, and specifies a location at the top left corner. The height and width specify that the content is to fill the entire window.
    .fill-window {
        	position: absolute;
         	top: 0;
         	left: 0;
         	height: 100%;
         	width: 100%;
         	display: inline-block;
    }
  2. The overlay class specifies that the pop-up message is to be mostly opaque (60%), and is to show large white letters on a black background. The z-index attribute puts the overlay in front of most other content.
    .overlay {
         	background-color: rgba(0, 0, 0, 0.6);
         	z-index: 9999;
         	color: white;
         	font-size: 50px;
         	display: inline-block;
         	text-align: center;
    }

Getting messages

  1. All the above is useless unless you can actually retrieve messages from the server. To get them, the browser-side code uses the $http Angular function.
    $scope.getMsg = function() {
  2. Call the $http function with the parameters to get /msg from our server. There is no need to include anything for the cookie, as it is sent automatically.
    	$http({
    		method: "GET",
    		url: "/msg"
  3. The $http function returns an object that has a then function. This function takes two functions, one to call when the operation is successful, and the other to call when it is not.
    	}).then(
    			function(response) {
  4. If the HTTP request is successful, and you got a real message (not just an empty string), add it and a new line to the message variable. This allows you to display multiple messages if you get a new one before an old one is dismissed.
    				if (response.data !== "")
    					$scope.msg += response.data + "<br />";
    			},
  5. The error function simply shows the response text for debugging. In a production application, this would probably be replaced by a number of retries, and then a user friendly error message.
    			function(response) { console.log("Error " + response.statusText); }
    		);
    	};
  6. The setInterval function runs a function, in this case the one that gets the message, repeatedly every thousand milliseconds (in other words, every second). This is how the browser code actually checks for messages.
    // Look for a message every second
    setInterval($scope.getMsg, 1000);

Integration with the proxy

To integrate with the proxy, modify the call that checks authorization.

  1. First, make sure the cookies are parsed before the function itself by adding:
    require("cookie-parser")(),
    // Authorize some requests, reject others
    app.get("/transaction/:amt/:debit/:credit", require("cookie-parser")(), function(req, res, next) {
  2. Then, when authorization is denied, use sendSessionMsg to send a message and then use res.send to respond to the HTTP request.
        if (amt >= 10000) {
        	// Send the user a message that this is unauthorized
        	sendSessionMsg(req.cookies[SESS_COOKIE], "Over the authorized limit");
        	
        	// res.send closes the connection
            res.send("");
        }
  3. You can see the behavior of the proxy at this point at http://authz-proxy2.mybluemix.net/proxyMsg.html. After you try a forbidden transfer, the application does not show any accounts. Try to transfer 10,000 and see that you get the following message of "Over the authorized limit." Screenshot showing message     example
    Screenshot showing message example
  4. Try to run a transaction directly (http://authz-proxy2.mybluemix.net/transaction/10/bank-account/bank-account, for example) to see the reason. The server sends back a new list of accounts, types, and balances. However, when the proxy responds with an authorization failure, it just sends out an empty string. To verify the authorization failure, notice that this transaction for US$9,999 returns a list of accounts, while a transaction for US$10,000 does not. (Note: These two transaction links might not work as expected from a browser that is already logged on to the application.)
  5. There are several possible solutions to this problem, but athe easiest is to redirect the browser to a transaction that does nothing.
    res.redirect("/transaction/0/a/a");

    For example, you might move US$0 from a nonexistent account to the same account. You can see this functionality at http://authz-proxy2.mybluemix.net/proxyMsg2.html. To show the problem and the solution, the application has two proxyMsg files. To make the two distinguishable, the session cookie for the second one is longer:

        	if (req.cookies[SESS_COOKIE].length === 36)
        		// res.send closes the connection
              	res.send("");
            else
    	       res.redirect("/transaction/0/a/a");

Sophisticated authorization policies

The messages the user gets are in HTML, which is a deliberate choice. With HTML, you can include links to an explanation of the process and why something was not permitted. You can also include a link to a user interface to kick off an approval process, either for the action itself or for allowing the user to commit similar actions in the future without having to go through the approval process.

Additional features

This article shows a sample program designed to showcase a particular technique. There are some features that would improve this technique when used in a production environment.

Application cookies

One feature that is missing from the proxy is support for cookies. There are two ways to support cookies. One is to simply relay them, sending the cookies from server HTTP responses to the browser and those from browser requests to the server. The other is to store them in a "cookie jar" on the proxy and never deliver them to the browser. The second approach requires less bandwidth, but fails if JavaScript code on the browser needs to access the cookies.

Support for absolute links

In most cases, the proxy does not have to change any of the links in the HTML. One exception is when the links are absolute and encode the name of the original server. Usually, you can modify the function in the proxy to do a search and replace on the HTML (and possibly JavaScript).

In more complicated cases, or if there is a need to change directory names, you can use the same techniques that are used by IBM Security Access Manager for Web. Those techniques are explained (under the product's old name) in "IBM Tivoli® Access Manager for e-business: Junctions and Links," an IBM Redpaper.

Forcing use of the proxy

Currently, users can get around the authorization by going directly to the backend service. In a third-party application, you can disable the backend service by following these steps:

  1. Change the user passwords on the application to random strings and hold the username to new mapping in a database table.
  2. Authenticate users when they access the proxy (as explained in "Add Google reCAPTCHA to your IBM Cloud Node.js application"; see the section entitled "Combine reCAPTCHA with the IBM Cloud single sign-on service.")
  3. When the user logs on to the proxy, log the user on to the application by using the password in the database. This way, users don't know their passwords and are therefore unable to log on themselves without the proxy. However, you will also need to disable the password reset mechanism.

Conclusion

With this article, along with "Add your own authorization proxy to a third-party app," you can now build an authorization proxy that communicates with the user, explain the reasons for negative authorization decisions, and how to reverse them.


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=1034755
ArticleTitle=Configure your IBM Cloud Node.js authorization proxy to communicate with the user
publish-date=07112016