Making An App With PhoneGap & jQm Part III: A Stateless Authentication Layer 33

If you missed Part II: Loading JavaScript Properly, you may want to go there and grab the code we wrote so far so that you can follow more easily what we'll do in this part.

Today, we'll see how to implement a RESTful, stateless authentication layer in our application. Let's first talk a bit about REST; the acronym stands for "REpresentational State Transfer" and describes a way of handling client-server transactions in a uniform, layered, stateless, scalable and cacheable way. I know that's a lot of adjectives to process so I won't delve into much more details here. We'll focus on the stateless constraint, which requires that the exchanges have no memory per se, they always contain all the information that the other party has to know to process them correctly.

I have tried to simplify my code examples as much as possible. The goal being "stateless authentication", I have focused on that, which means that for now we will deal with "ugly" URLs like https://foo.bar/server.php?a=b&id=123; we'll see in a while how to do the same with "clean" URLs like https://foo.bar/users/bob

The REST philosophy is widely considered one of the best ways to solve communication challenges between a client and a server over the Internet. You may want to read the linked Wikipedia article for more details about the how and why.

Now what we're interested in is the design of a system in which we can "phone home" from our application to an online server. This is needed for applications which have to pull data from a remote host like news items or score boards, or on the contrary push data to a remote host like creating a new account or updating profile information. Obviously such a system must allow for authentication, or we could end up with users accessing or modifying other users' data at will!

Phoning Home (Cross-Domain) With jQuery

Phoning home is surprisingly easy with PhoneGap & jQuery, and at the same time it can prove hard to do it right. What I mean is that on the one hand jQuery provides us with powerful tools like jQuery.ajax(), which we can leverage in order to query our servers; on the other hand, there is a bunch of things we need to do so that our query is not blocked by the anti-cross-domain security features embedded in web browsers like Chrome.

Thanks to jQuery, the client code is rather straightforward; we simply need to add a function like this to our App namespace:

	"servers": {
		"public": {
			"URL": "https://foo.bar/public.server.php",
			"query": function (action, data, callback) {
				console.log("[public.query]");
				jQuery.ajax(App.servers.public.URL, {
					"type": "GET",
					"dataType": "json",
					"data": {"action": action, "data": data},
					"contentType": "application/json",
					"success": callback
				});
			}
		}
	}

As for the server, you can hack something quite quickly in pure PHP:

<?php
function successJSON ($content, $die=true) {
	wrapJSON('SUCCESS', $content, $die);
}
function errorJSON ($content, $die=true) {
	wrapJSON('ERROR', $content, $die);
}
function wrapJSON ($code, $content, $die=true) {
	// encode content as {foo:bar,foobar:baz}
	$content = json_encode((object)$content);
	// strip the first character '{' from the json encoded string
	$content = substr($content, 1, strlen($content)-1);
	// add the "code" part to the string
	$buffer = '{"code":"'.$code.'",'.$content;

	echo $buffer;
	if ($die) {
		die;
	}
}

// empty request?
if (empty($_REQUEST['action'])) {
	errorJSON(array("message" => "empty_request"));
}

switch ($_REQUEST['action']) {
	case 'test':
		$data = 'yeepee';
		if (!empty($_REQUEST['data']['foo'])) {
			$data = $_REQUEST['data']['foo'];
		}
		successJSON(array('foo' => $data));
		break;
	default:
		errorJSON(array("message" => "access_denied"));
		break;
}

Change the URL content in your client code and you can test the function from anywhere in your application:

App.servers.public.query("test", {"foo":"bar"}, function (response) {
if (response && response.code) {
	if (response.code === 'SUCCESS') {
		PGproxy.navigator.notification.alert(response.foo);
	} else {
		console.log("error while phoning home!");
	}
});
// should result in an alert box saying "bar"

I wrote "should" specifically because if you try it locally (on your MAMP/LAMP/WAMP stack) with a remote server, it's actually probable that it won't work as expected. Here's the thing: querying cross-domains from PhoneGap on a smartphone is typically not a problem, but doing so on your desktop system can be, due to browsers' security policies.

To fix this, we have to (1) make the server tell the client it allows it to perform queries from a remote origin, and (2) make jQuery and jQuery.Mobile know they are allowed to perform cross-domain calls.

First, simply add the following lines to the top of your PHP file:

header("Access-Control-Allow-Origin: *");
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Max-Age: 1000');
header('Access-Control-Allow-Headers: Content-Type');
header('Content-Type: application/javascript');
if ($_REQUEST['method'] === 'OPTIONS') {
	echo '1';
	die;
}

What does this code do? Basically, jQuery.ajax() will always start by pinging the remote URL with an HTTP OPTIONS request and checking the response headers; if it detects Access-Control-* headers and in particular Access-Control-Origin matches the source domain, then it will perform the "real" query. We die early if the request method is "OPTIONS" in order to prevent our script from processing the same data twice!

Then, make sure you have those lines in your jQuery.Mobile configuration code:

			jQuery.support.cors = true;
			jQuery.mobile.allowCrossDomainPages = true;

Now you should be able to run the example call successfully on both your desktop browser and your Android smartphone!

Stateless Authentication Using Public/Private Tokens

Now that's great and all, but how about sensible queries like updating a user's account information or posting things in his name? As I wrote earlier, surely we can't use this public server queries implementation, or we'd allow anyone to mess with our system!

In order to solve this problem, we'll use an additional server script named private.server.php, which will only accept authenticated queries. Users will authenticate themselves using an email/password, like they do on most websites. The REST philosophy then requires us to pass authenticating information inside all our requests, as opposed to, say, using session cookies. We'll use private/public tokens to securely authenticate the user across requests to our API without passing its login information, which is very sensible in essence, in every request. 

Here's how the login process works:

App: Present the user a login form
User enters his/her login info (email/pass, username/pass, etc.)
Send the login information to public.server.php
Server: Find user in the database based on the login info
Retrieve his/her public & private tokens
Send the two tokens back to the app
App: Store the two tokens locally for future reference (using localStorage)

Having those two tokens, we can authenticate every subsequent query that needs so:

App: Construct a hash from the private token and another value, say the timestamp
We'll use the sha1() algorithm: hash = sha1(private_token + timestamp)

App: Send a query to private.server.php passing additional variables
We'll need hash, timestamp and public_token added to action & data
We'll automate this using an additional query method: App.servers.private.query
Server: Find user in the database from the public token
We actually only need the private_token field from our users table
Construct the same hash from the (retrieved) private token and the (passed) timestamp
Compare (reconstructed) hash to (passed) hash. If the two match, the query is authenticated

Stateless Exchanges While Logged In

Let's assume we're logged in. As we've seen, this means that we have stored our public and private tokens on the mobile device. Now all we have to do is create a brand new App.servers.private.query() method, which we will use for every authenticated query. I've actually written a wrapper method called App.servers.query, which allows us to have cleaner code. Here is the whole thing; take your time reading it, it should be quite easy to understand but please do not hesitate to post a comment if you need me to explain anything:

"servers": {
	"query": function (url, type, data, callback) { console.log("[query "+url+"]");
		jQuery.ajax(url, {
			"type": type,
			"dataType": "json",
			"data": data,
			"contentType": (type==="GET" ? "application/json" : "application/x-www-form-urlencoded"),
			"success": callback
		});
	},
	"public": {
		"URL":  "https://foo.bar/public/",
		// perform unauthenticated query
		"query": function (action, data, callback) { console.log("[public.query]");
			App.servers.query(App.servers.public.URL+action, "GET", {"data": data}, callback);
		}
	},
	"private": {
		"URL": "https://foo.bar/private/",
		// perform authenticated query
		"query": function (action, data, callback) { console.log("[private.query]");
			if (!localStorage || !localStorage.getItem("token_public") || !localStorage.getItem("token_private")) {
				console.log("can't do private query: empty private/public tokens!");
				return;
			}
			// used as seed for hashing the private token
			var timestamp = Math.round(+new Date()/1000);
			App.servers.query(App.servers.private.URL+action, "POST", {
					"timestamp": timestamp,
					"token_public": localStorage.getItem("token_public"),
					"hash": sha1(timestamp+localStorage.getItem("token_private")),
					"data": data
			}, callback);
		}
	}
}

That's it, basically: once you're logged in, you can run whatever type of query you want in a very simple, unobstrusive way:

App.servers.public.query('deposit_cash', {"account":1,"amount":"€4,321.00"}, function (result) { /* ... */ });
App.servers.private.query('wire_money', {"account":2,"amount":"€1,234.00"}, function (result) { /* ... */ });

As for the private server, here's what you could have set up:

// reject incomplete requests
if (empty($_REQUEST['token_public']) || empty($_REQUEST['hash']) || empty($_REQUEST['timestamp'])) {
	errorJSON(array('message' => "empty_tokens"));
}

// try to find a user from the public token we've been sent (die if not found)
try {
	$User = User::find('first', array('conditions' => array('token_public = ?', $_REQUEST['token_public'])));
} catch (Exception $e) { errorJSON(array('message' => "invalid_tokens", "e" => $e->getMessage())); }
// make sure the hash hasn't been tampered
if ($_REQUEST['hash'] !== sha1($_REQUEST['timestamp'].$User->token_private)) {
	errorJSON(array('message' => "invalid_tokens"));
}

// try and parse any additional data we've been sent
if (!empty($_REQUEST['data'])) {
	parse_str($_REQUEST['data'], $_REQUEST['data']);
}

switch ($_REQUEST['action']) {
	case 'check_tokens':
		// if we've arrived this far, it's all good
		successJSON(array('message' => "all_good"));
		// shouldn't happen
		break;
	case 'wire_money':
		// [...]
		// [...]
		successJSON(array('message' => "tranfer_order_placed"));
		// shouldn't happen
		break;
	default:
		errorJSON(array('message' => "invalid_action"));
		// shouldn't happen
		break;
}

How To Authenticate For The First Time

Now how about logging in? Let's start slowly with the addition of two fields in the users table: public_token and private_token, both as VARCHAR(40), NOT NULL and with an index so that we can retrieve their associated record quickly later on. You will then have to generate the two tokens in a random way upon registration, and I encourage you to generate new ones regularly.

Here's a App.handleLogin function launched when the user submits the login form:

"handleLogin": function () { console.log("[handleLogin]");
	var m = jQuery("#input-Login_mail").val();
	var p = jQuery("#input-Login_pass").val();

	if (m.length === 0 || p.length === 0) {
		console.log("! empty fields");
		PGproxy.navigator.notification.alert("empty fields", function() {});
		return false;
	}
	App.servers.public.query("login", {
		"mail": m,
		"pass": p
	}, App.callbacks.handleLogin, true);
	return false;
}

… and finally the App.callbacks.handleLogin() callback:

"handleLogin": function(r) { console.log("[callback: handleLogin]");
	if (r && r.code && r.code === "SUCCESS" && r.token_private && r.token_public) {
		console.log("user is now logged in!");
		localStorage.setItem("token_private", r.token_private);
		localStorage.setItem("token_public", r.token_public);
		localStorage.setItem("id", r.id); // you can store whatever other data you want
		return;
	} else {
		PGproxy.navigator.notification.alert("login failed", function() {});
	}
}

Those functions were rather simple and intuitive, the most interesting part to my mind comes from the servers and their handling of the requests. Here's a rough example in PHP of the code could have in your public server:

switch ($_REQUEST['action'])
{
	case 'login':
		// exit early if obviously invalid fields
		if (empty($_REQUEST['data']['mail']) || empty($_REQUEST['data']['pass'])) {
			errorJSON(array('message' => "invalid_fields"));
		}
		// get corresponding user or exit early
		try {
			$User = User::find('first', array('conditions' => array(
				"email = ? AND password = ?", $_REQUEST['data']['mail'], sha1(SOME_SALT.$_REQUEST['data']['pass'])
			)));
		} catch (RecordNotFound $e) { }
		if (empty($User)) {
			errorJSON(array('message' => "invalid_credentials"));
		}

		// send the tokens back to the app & die
		successJSON(array(
			"token_private" => $User->token_private,
			"token_public" => $User->token_public,
			"id" => $User->id
		));
		// shouldn't reach this
		break;
	}
}
Note that here I'm using a very simple ORM called PHP ActiveRecord to manage my models and their relations. Feel free to use anything else, as long as it suits you and sends queries in a secure way!

Wrapping Up

Phew, that was a long post. I hope that these snippets and explanations will be somewhat useful to you; if you would like me to give more details on something specific please do tell me in the comments or send me a mail through the contact form, I'll try my best to reply. This public/private tokens system is quite simple to handle once you've grasped the concepts around it, it is reasonably secure and is completely platform independant. I wanted to include a section on how to add even more security features to it like using a user-specific salt instead of a global one or implementing a retry limit, however the article was becoming way too long so I'll talk about it in another part ;) Thanks for reading!

33 thoughts on “Making An App With PhoneGap & jQm Part III: A Stateless Authentication Layer

  1. Reply Marina Sep 2,2013 5:59 am

    Thank you x 10000000000000000000000!!!!!!

  2. Reply Marina Sep 2,2013 6:00 am

    Thank you!!!!

  3. Reply TuanPhan Nov 5,2013 5:42 pm

    Can you share me your source code.
    My email phanvantuan134@gmail.com
    thank you

  4. Reply antek Dec 8,2013 4:41 pm

    Nice tut! thanks a lot!
    It could be interesting effectively to share your source code with us.
    I have several errors in my log console and don't know how to go on.
    (awesome fonts do not appear, app function is not defined…)

  5. Reply kenny Jan 23,2014 3:57 am

    Excellent piece of work! Really appreciate it if you can publish the full working codes. Thank you very much again and keep it up!

    • Reply Thomas Jan 23,2014 8:51 am

      Hey kenny, thanks for the kind words 🙂

      I'm sorry I've been quite busy lately… I'll definitely try publishing a fully working code sample tonight; stay tuned!

  6. Reply Caio Feb 13,2014 6:45 pm

    Nice work, but I have a doubt: as you're using Local Storage to store the tokens, wouldn't it be available to other local apps? I mean, the domain used to scope the local storage is the localhost, so every other app running on the app would be able to see this info right?

    • Reply Thomas Feb 14,2014 5:49 pm

      Hello Caio, thanks for your comment!

      I have to admit, I sure was taken aback by your -excellent- remark for a second. Here's the answer.

      Unlike the now deprecated GlobalStorage, LocalStorage is designed from the bottom up to prevent data from leaking from one tab to another. The deciding criteria is a combination or the protocol, domain and port number (if not standard).

      Under those premises, our PhoneGap app would indeed be protected from leaking to other web-based apps but we could very well be wary of it leaking to other PhoneGap apps because all of them would use the http/localhost/80 combination. Luckily for us, the issue is non existent thanks to Android specifically not sharing localStorage data between distinct WebViews. Each PhoneGap app will necessarily use theirs, hence them being protected 🙂

      -Thomas

  7. Reply Mike Tommasi Feb 27,2014 3:24 pm

    Looks good, did you ever publish that source code? Would help…

    thx

  8. Pingback: #3: A REST Auth. Layer | Coding An App On Phone...

  9. Reply Syuhada Mar 7,2014 3:09 am

    Hii… thanks for tutorials,
    i've tried it, but i can't.
    can you share your source code?
    my email : ar.salman30@gmail.com

  10. Reply Nizar Mar 8,2014 7:30 pm

    Thanks! can you please share your code?

  11. Reply João Marcos Mar 15,2014 5:33 pm

    Hey buddy, all parts of the tutorial is great!
    Thanks for wasting time writing great things.

    But as you didn't make the code public, could you send me it?
    I am having some problems, and can't wait to make it right.

  12. Reply atabak Apr 26,2014 7:21 am

    Thanks! can you please share your code please?

  13. Reply Andy May 12,2014 9:30 am

    Hey Thomas,
    Thanks for the writing! It's all good and well, but I'm concerned about this part:

    "App: Present the user a login form
    User enters his/her login info (email/pass, username/pass, etc.)
    Send the login information to public.server.php
    Server: Find user in the database based on the login info
    Retrieve his/her public & private tokens
    Send the two tokens back to the app"

    If I get it right that data is sent as is in plain text. Are you using SSL for this? I mean what's the point in using public/private key system, if the initial process of getting the keys is insecure?

    • Reply Thomas May 12,2014 5:07 pm

      Hey Andy, thanks a lot for the kind words.

      You're absolutely right! Using an insecure, unencrypted connection to send the user's (login, password) tuple is problematic to say the least.

      The URLs that I've used in my code samples were only intended to serve as an illustration of the different types of scripts involved (public & private, respectively). The public.server.php script is definitely not to be understood as unsecured access to our backend, only unauthenticated access. I have updated the article to make it clear that the whole setup should use SSL-enabled URLs. 😉

      Happy coding!
      Thomas

      • Reply Andy May 14,2014 8:20 pm

        Looks better now 😉
        The process of user registration and authentication in mobile apps appears to be the most tricky part to handle. Your article has really helped me in this! Many thanks Thomas!

  14. Reply Arah May 20,2014 12:11 am

    Can you share me your source code?
    Thank you sir!

  15. Reply Bruno Jun 3,2014 3:44 pm

    Thanks for this post =)
    It was exactly what I was looking for!

  16. Reply Behi Jun 17,2014 11:48 pm

    Thank you so much for this clear presentation.
    Could you please share source code for this project?

  17. Reply Alex Jul 11,2014 12:02 pm

    Thanks a lot. This is very helpful.

  18. Reply Kevin Jul 17,2014 3:12 am

    Appreciate you can share the source. Thanks.

  19. Reply Michael Aug 13,2014 6:09 am

    Wow, just wow.

    Your code is so elegant haha, I understood every single bit, very nice coding decisions too.

    Thanks for this!

  20. Reply Michael Aug 13,2014 6:33 am

    Also I have existing accounts I want to integrate your system into, what would be the best way of generating public/private keys for these accounts?

    Can I just create a random algorithm for both public/private keys using some form of hashing based on their user ID or something unique?

  21. Reply Lofi Aug 21,2014 4:36 am

    Thank you very much for this excellent tutorial! I was just wondering why we have public and private keys on the client. Could an alternative be asymmetric encryption? Example: client has public key, server has private key. Client sends his id, a timestamp and a public key encrypted timestamp to the server. Server gets private key using the id, decrypts encrypted timestamp with private key and compares it with the client's timestamp. Or do you see a flaw in that? Thanks.

  22. Reply Thomas Sep 7,2014 7:42 am

    Thank you for the educational article.

    Wouldn't storing the public and private key locally on the mobile be a security risk?

    Also, you mentioned you were going to post your full source. Did you get a chance to post it?

  23. Pingback: Gregory Smith

  24. Reply Dries Oct 6,2014 2:47 pm

    Great article! I implemented it on a phonegap app with https php backend.

    It seems like a very safe way of authentication but is there still a way to hack this? (= impersonate another user) (except hacking the database or stealing the password of a user)

    I am just wondering since you say it is "reasonably secure".

  25. Pingback: Vanessa Smith

  26. Reply Luuk Barten Apr 15,2015 4:15 pm

    There are few details missing:
    1. The callbacks.handleLogin construction is missing in the example. It should start like this:

    "callbacks": {
    "handleLogin": function(r) { console.log("[callback: handleLogin]");
    ....

    2. Javascript doesn't have its own sha1 function. I used this one

    3. The servers.public and servers.private javascript functions won't correctly format the URL and action if you want to use a folder name as the destination page for your ajax call. I made the following change:
    .....app.servers.public.URL + '?action='+ action, "GET",....

    4. To test with a local browser you can start Chrome with the following flags to enable crossbrowser loading. (works on MacOs in terminal)
    open -a Google Chrome --args --disable-web-security

Leave a Reply

  

  

  

Time limit is exhausted. Please reload the CAPTCHA.