I've got a couple of entries going so far in my HTML/Adobe AIR series. I'm taking a step "out" of that course to address some questions I've gotten recently via IM. (Hence the 'Diversion' title above.) Today's question deals with handling server based authentication. In other words, how do you build a client side application that authenticates against some central server? Before I get started - let me send a quick shout out to Jason Dean and Andy Matthews for helping me get this together.
Ok, so before we get started - let's cover a few important things. I've yet to talk about network calls and Adobe AIR applications. I still plan to and have a few cool demos - but for now the important thing to know is that an AIR application can easily request data from remote domains. So while this would be a problem in a normal web app:
$.get("http://www.remotesomethingoranother.com/some.cfc?method=bringthefunk")
In Adobe AIR it works just fine. So right away, we now know that we can build a CFC to handle logins and other secured functions on some central server and our AIR applications will have no problems hitting it.
The second problem we have is handling the authentication after you have logged in. You aren't "on" the server - you are running a client side application. Sessions can't work, right? Well it turns out that your Ajax requests from your AIR application will easily accept ColdFusion's (or any other platform) session cookies. (Jason has an article that talks about this more in depth.) So that is no longer a problem. Enough chatter, let's look at some code.
I'm going to begin with my server code. In this example I'll keep it very simple. I've got a ColdFusion site that has one CFC. This CFC has a login method and a random number generator:
remote boolean function login(string username, string password) {
if(arguments.username == "admin" && arguments.password == "password") {
session.loggedin = true;
return true;
} else return false;
} remote numeric function randomNumber() {
return randRange(1,10000);
} }
component {
In front of this I have an Application.cfc file that will handle my security. To be clear - this is one way of handling it. My service CFC above could also handle it within itself.
public any function onCFCRequest(string cfcname, string method, struct args) {
if(!structKeyExists(session, "loggedin") and arguments.method is not "login") throw("NotAuthorized");
var comp = createObject("component", arguments.cfcname);
var result = evaluate("comp.#arguments.method#(argumentCollection=arguments.args)");
if(!isNull(result)) return result;
} }
component {
this.name="authtest";
this.sessionManagement="true";
this.sessiontimeout = createTimeSpan(0,0,1,30);
So this component only has one method defined: onCFCRequest. This will handle all CFC requests to the application. I begin with a security check. If the session variable, "loggedin" doesn't exist, I'm going to throw an error if they currently aren't trying to login. After this, I create an instance of the CFC and run the method. Using onCFCRequest means you have to be a bit more manual with your CFC requests. The basic gist of this is - if you try to run any CFC method and aren't authorized, you will get an error unless you are trying to login. I want to repeat - there are many other ways you could do this. Consider this just one simple example.
Alright - so we've got our server setup. What's next? Well the front end client of course. This isn't a huge file so I'll paste in the entire thing. I'll then go over the bits and explain what's happening where.
<html>
<head>
<title>New Adobe AIR Project</title>
<script type="text/javascript" src="lib/air/AIRAliases.js"></script>
<script src="lib/jquery/jquery-1.4.2.min.js"></script>
<script>
var serviceURL = "http://127.0.0.1/testingzone/authtest/service.cfc?returnFormat=json"; $(document).ready(function() { //set up login div
$("#loginDiv").show(); $("#loginButton").click(function() {
var u = $("#username").val();
var p = $("#password").val();
//normally you would validate here
$.post(serviceURL, {"method":"login","username":u,"password":p}, function(res,status) {
res = $.trim(res);
if(res == 'false') {
$("#result").html("<b>Your credentials did not pass. Use admin for username and password for password.</b>");
} else {
$("#loginDiv").hide();
$("#mainApp").show();
}
}); }); $("#getRandomButton").click(function() {
$.get(serviceURL, {"method":"randomNumber"}, function(res,status) {
res = $.trim(res);
$("#randomResult").html("<b>Your random number is "+res + "</b>");
});
}); });
</script>
</head>
<body> <!-- Login Panel -->
<div id="loginDiv" style="display:none">
<div id="result"></div>
Before you use the app, you must login.
<form id="loginForm">
username: <input type="text" id="username" value="admin"><br/>
password: <input type="password" id="password" value="password"><br/>
<input type="button" value="Login" id="loginButton">
</form>
</div> <!-- Main App -->
<div id="mainApp" style="display:none">
<h2>Random CF Number Generator</h2>
Click the button to get a random number: <input type="button" id="getRandomButton" value="Get Random!">
<div id="randomResult"></div>
</div> </body>
</html>
Let's start at the bottom. My application has two views. A login view and a 'Get a random number' view. I'm not quite sure what the best way to handle this so far. For this application I simply hid them both and will use visibility to enable the views. I plan to come back to this later with a more intelligent solution. Scroll on up a bit to the document.ready block. We begin by showing the login div. In theory it's possible that when you start this application, you have an existing session on the server. But we are going to always force you to login. Notice my loginButton handler. It picks up the username/password values and then sends it to my CFC defined above. If the value is bad - tell the user. If good - hide the login div and show the main application div.
Finally - I've got a simple handler for the random number generator. On click I fire off the request and display the response. Here is the beautiful app in action.
Sexy, isn't it? This is why my job never lets me do design work. Ok, so this works - but - what happens when the session times out? If you look at my Application.cfc above you can see that I've got a real short timeout. Unfortunately, right now my code does... nothing! The error isn't handled in my code so the user never gets a response and has no idea that something has gone wrong. Luckily jQuery makes this pretty simple for us. We can register an error handler for the individual data call we are doing. We can also register a global error for Ajax issues. I chose a global handler. We could argue which is better - and to be honest - I think probably the more specific one. But for now, let's keep this simple. Here is the handler I added:
$.ajaxSetup({
error: function(x,e){
//for now, assume error was a login error
$("#mainApp").hide();
$("#loginDiv").show();
alert("I'm sorry, but your authentication has timed out.\nPlease login again.");
}
});
Right now the handler is a bit dumb. It assumes any error is an authorization error. Again though I wanted to keep things simple. Here is an example of the error being handled:
And that's basically it. I've included the entire HTML below. You can download the AIR application as well and hit it. It will run against my own server so be sure to click the frack out of that button.
I want to make one quick point. You may notice I'm sending my username and password over the air. (Heh, get it, air?) That's bad. This application could fix that by simply calling an HTTPS URL instead of an insecure HTTP URL. Obviously for a demo it works, but, keep it in mind if you go forward with a real application. (And again I'll push folks to Jason Dean. He's done multiple presentations on security and AIR applications. Check em out. He doesn't bite. Normally.)
Archived Comments
Thank you Ray, I am going to play with this, this should be a good start! I appreciate the help and look forward to more articles in the air/cf/js/jq series!
Can't the AIR app send the username/password every time? Why do we need Session at all on a stateful AIR app?
Could we have used http(s) basic access authentication?
"Can't the AIR app send the username/password every time? Why do we need Session at all on a stateful AIR app?"
In a Flex app, you can do it with Flash Remoting.
"Could we have used http(s) basic access authentication?"
I don't know. Ok - I checked the jQuery docs: http://api.jquery.com/jQuer...
And it does look possible. CF's cflogin framework supports picking up on these credentials automatically, so that too is an option.
Why would we want to pass the user info each time? If the app depends on the internet to work, assuming it's an app that needs to be constantly online, than i prefer to have the session set one time and be done with it. If you have to pass the login info each time that would mean each request would have to be authed every single time.
@Hatem, you are sending CFID and CFSESSIONID every time via cookie, so the network payload is about the same. You're correct that authentication will need to be done every time, but the server can scale better without session management. Furthermore, the client does not have to handle session timeout.
"Why do we need Session at all on a stateful AIR app?"
The real reason is that while our client is stateful the server is not. The session is still about letting the server keep track of you and letting it know when to not let a user have access anymore.
You will want to avoid sending login credentials over the wire every time. This is essentially how http-basic-auth works (although they are hashed) with most public API's (google, yahoo, twitter). All of these companies have started to move to OAuth (or similar) which allows you to send that information over the wire once and then you receive a session key for your troubles. Then you send that session key with each of your requests.
Why bother with that when AIR maintains state with CF so well? Because while the browser in AIR does maintain cookies from the CF server, not all clients do. And using the OAuth mechanism allows many different clients to connect and access the stateless server without having to support cookies.
And that is my long and over informative answer to a very simple question.
:)
Hi Ray.
@Simeon
"You will want to avoid sending login credentials over the wire every time."
why? How is OAuth that much better? (OAuth sucks for mobile, and OAuth2 is still in draft). And isn't OAuth for transferring capabilities, not for user authentication only?
My understanding of OAuth is that it's not about avoiding sending credentials over the wire, but is about not giving your (say) Twitter credentials to a Twitter client / Twitter accessing website. The client redirects to a page from Twitter where you enter your credentials to give the client a token it can use to access Twitter as you without ever knowing your credentials. You still need to give your credentials to Twitter, at which point they go over the wire.
The other benefit of OAuth is that you can then access the Twitter site and see which applications have been given tokens and potentially revoke them.
BTW - In case it wasn't clear, I'm not saying OAuth is only for Twitter, it was just an example.. :)
I think OAuth is irrelevant here. The app in question is just consuming its own services, not 3rd party services.
@Sim: Hi SIm! (For folks who don't know it, Sim is one of like 2 'go to' guys for my Flex/AIR questions. Surprised he showed up here though since the topic is HTML. :P
@Marcin: Twitter is fine. As it stands, 99% of all Twitter clients are written in AIR. :)