I've been thinking lately about how to add authentication to a jQuery Mobile site. I whipped up a quick example that works, but I'm definitely open to suggestions on how this could be done better. My intent with calling this entry the "first round" is to make it clear that there are other ways of doing this and probably better ways of doing it. Hopefully this example will help others and - admittedly - flesh out some improvements from my readers.
I began by creating a very simple page with two links. In my application login is not required for everything, but only for one link.
<div data-role="page"> <div data-role="header">
<h1>Secure Site Test</h1>
</div> <div data-role="content">
<ul data-role="listview" data-inset="true">
<li data-role="list-divider">Options</li>
<li><a href="page1.cfm">Non-Secure page</a></li>
<li><a href="page2.cfm">Secure page</a></li>
</ul>
</div> <div data-role="footer">
<h4>Page Footer</h4>
</div> </div> </body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.0a4/jquery.mobile-1.0a4.min.css" />
<script src="http://code.jquery.com/jquery-1.5.2.min.js"></script>
<script src="http://code.jquery.com/mobile/1.0a4/jquery.mobile-1.0a4.min.js"></script>
</head>
<body>
So I won't go over the basics - I've blogged about jQuery Mobile quite a bit already - but you can see I've created a basic list page with two links. One to an open page and one to a secure page. I then created page1.cfm:
<div data-role="header">
<h1>Non-Secure page</h1>
<a href="index.cfm" data-icon="home" class="ui-btn-right">Home</a>
</div> <div data-role="content">
<p>This page is not secure.</p>
</div> <div data-role="footer">
<h4>Page Footer</h4>
</div> </div>
<div data-role="page">
And then page2.cfm - note this page assumes that session.username exists.
<div data-role="header">
<h1>Secure page</h1>
<a href="index.cfm" data-icon="home" class="ui-btn-right">Home</a>
</div> <div data-role="content">
<cfoutput>
<p>This page is secure. You should only see it if you are logged in: #session.username#</p>
</cfoutput>
</div> <div data-role="footer">
<h4>Page Footer</h4>
</div> </div>
<div data-role="page">
So - how to handle the security? I decided to add a simple Application.cfc page. This will run with every request and I could use it to lock down requests for page2.cfm. Remember that jQuery Mobile will load page2.cfm via an Ajax call, but it's still just a request to ColdFusion.
public boolean function onApplicationStart() {
return true;
} public boolean function onRequestStart(string req) {
//logic to determine if a page is secured should probably be elsewhere...
if(listLast(arguments.req, "/") == "page2.cfm" && !structKeyExists(session, "username")) {
location(url="login.cfm",addToken=false);
}
return true;
} }
component {
this.name="jqmwithlogin_rev1";
this.sessionManagement="true";
You can see where I do my security check. I wouldn't normally write the code so statically, but for a simple example it works ok. My app only has one secure page so the logic isn't too complex. Anything more than one though would require a bit more thought here. So if the request is for my secured page and I am not logged in, I use a location call to push the user to the login page. Let's look at that.
<div data-role="page"> <div data-role="header">
<h1>Login</h1>
</div> <div data-role="content">
<cfif not structKeyExists(variables, "badFlag")>
<p>Please login...</p>
<cfelse>
<p>Your login was incorrect - try again - try harder...</p>
</cfif>
<p>
<form action="login.cfm" method="post"> <div data-role="fieldcontain">
<label for="username">Username:</label>
<input type="text" name="username" id="username" />
</div> <div data-role="fieldcontain">
<label for="password">Password:</label>
<input type="password" name="password" id="password" />
</div> <input type="submit" name="login" value="Login" />
</form> </div> <div data-role="footer">
<h4>Page Footer</h4>
</div> </div>
<cfif structKeyExists(form, "login")>
<cfif form.username is "admin" and form.password is "admin">
<cfset session.username = "admin">
<cflocation url="page2.cfm" addToken="false">
<cfelse>
<cfset badFlag = true>
</cfif>
</cfif>
So - this page is a bit bigger, but not really that complex. The form makes use of a divs with the role fieldcontain. This is standard jqm form rendering style. My form posts to itself and you can see logic on top of the file to handle that. Note the use of a static username and password. Again - this isn't something you would do normally. If the login is ok, once again a location is called to send you to the secure page.
So - how horrible is this? It seems to work ok. You can try this yourself by running the demo below.
Archived Comments
May not be the best practice but I email my trusted users a long key appended to my mobile url like..
mobilesite.com/index.cfm?k=...
I then give instructions on adding this url to their home screen and provide a custom icon and no url bar.
Works for me.
then i authenticate on that.
ok - have seen nicer code :-) but thought i would give it a run for its money anyway...
what happened with me is that first time logon works and i get to see page2.cfm, when i return "home" form that page and try to hit page2 again i get the logon-form (prefilled - but presumably with the wrong data) but hitting Login results in the "login-error" message.
Is the session lost somehow?
I had a mobile 'login' on my list today - so maybe more insights later on.
@Bart: no, the session is still there. First time round it gives you the login screen even though the hashtag says page2 because there has been a server side redirection. The browser is simply returning cached content and doesn't request the page again from the server. Refreshing the page shows page2 as expected.
@Bart: Wow, "nicer code"? Come on now - you can't say that w/o some constructive criticism about what you would change. ;)
Based on what @MrBester says - it sounds like caching. What browser do you use and what's your caching setting? I'm testing with Chrome. Are you using IE perhaps?
It _does_ kind of make sense that it would be cached. This is _exactly_ the kind of issue I was hoping would be fleshed out by testers, so thanks guys.
@Ray you said yourself "how horrible is this" :-)
I tested it in mobile Safari and Firefox caches the loginpage aswell (when you access page2.cfm for the second time). Refresh does work.
Will test it out during the day. Thanks.
Bam, confirmed it right away with Firefox. Ok, now I've got something to work on.
So I made a new copy and removed any idea of security. Instead, I simply made page 2 output a random number server side. I quickly see the error in Firefox _and_ Chrome in that version. For some reason, Chrome seems to work perfectly on the security version.
Either way - this is now a good more generic issue. It is a _good_ thing that JQM caches page requests, but how would we easily disable that for cases like this? Going to dig into that next.
Check out http://www.coldfusionjedi.c...
and be sure to view source. On the secure link I used data-ajax="false". This creates a full page reload which meant I needed to include 'full' HTML for login.cfm and page2.cfm. This seems to work better, but I noticed I ended up on login.cfm as a URL after a successful login. I seemed to be able to browse just fine, but, meh.
I'm thinking making I should make the FORM action also use data-ajax="false".
Check out http://www.coldfusionjedi.c...
I added data-ajax=false to my form submit and end up with page2.cfm in the URL. I think I'd feel better if it was index.cfm#page2 or some such.
But... getting there I guess. Any new comments?
The first iteration was correct from an old school (with jQM being the new school) paradigm; it is sufficient to block access to a protected page. So the downlevel browsers are catered for. Now it needs enhancing for the "I've just converted it into a sort of web application" that jQM provides.
What is possibly needed is:
Logged in state is established and maintained on the client. Whether this be cookies, localStorage or a vanilla JavaScript variable this is available before the user can select any page, be it protected or not. A hook into the pagechange event can check this value and decide whether to go to login or not. An AJAX return (1 or 0 would be sufficient) would update the logged in status accordingly and show error messages / go to originally requested page. This would make the hashtags correct as well.
Bottom line: jQM should control page redirection, not the server.
Logged in state cannot be maintained _just_ on the client. To do so would be a security issue. It _can_ be copied there of course. I did envision having to do more client side to make thing work better and looks like I might have to.
"Bottom line: jQM should control page redirection, not the server"
True - but again - and this may be me being picky - we need to be real clear that it's not just jQM, but jQM + the Server. Wouldn't you agree?
It does sound strange and unwanted(?) to need client-stuff to secure a mobile app. It might be me having to adjust to a new paradigm, but i dont know.
I just managed to open up an existing application on the mobile with a normal cflogin check/login in application.cfc. - somehow that seems more 'foolproof'.
What i found is that in my Login form i needed an absolute URL, because otherwise it would be fetched by Ajax and i started getting URLs like index.cfm#index.cfm - which, with any other 'internal' hash-links would return an error.
Any reasons not to use the 'normal' security?
@Bart: Sorry - what do you mean by 'normal' ?
As to what you say about the absolute URL, I believe that is expected behavior with jqm and links. If you check the docs you can confirm, but they discuss by what rules a resource is loaded via Ajax or not. You have multiple ways to control it.
@Ray with 'normal' i mean the cflogin-check in the onRequestStart and cfinclude of the login.cfm when the check fails
As you only have the login for certain templates there would be the addition of the conditional based on the requesting template...
I don't see (yet?) why we need a different way of authorisation for our mobile apps....
So you used cflogin to do an include and did NOT see the caching issue reported in Firefox?
right, no caching issues whatsever. I havent tried the exact same setup as you have though - will do that now.
Just checked - no problem to mix the non-secure pages and secure pages and then get the redirect to login.cfm (when hitting a secure page) and after logon seeing the correct version of that secured page. (BTW, only did a quick test in FF)
I have a site with a fairly robust cflogin-based wrapper, and have added a jqm site as a folder within it.
My login page to which logged out users get sent already had a "stopper" for when it is called by an Ajax within-page load (for example, in an autosuggest) that tells folks that their session is expired and to click the Home button for a fresh login screen.
I found I had to add a cfelseif to it that takes in the case of a page called by jqm as an Ajax page from a link, and have within that cfelseif block the data-role, like this:<div data-role="page" id="Login">, so that jqm would run the page. That block altogether looks something like this:
<cfif cgi.PATH_INFO CONTAINS "TheSnippedYouNeedToIdentifyThisProperly">
<div data-role="page" id="Login">
<div data-role="header" style="background-color:#cc0000;">
<h1><img src="images/SomeLogo.jpg"> Page title</h1>
</div><!-- /header -->
<div data-role="content">
YOUR LOGIN HAS EXPIRED<br />
<a href="index.cfm" rel="external">Click here</a><br />
for fresh login screen
</div>
</div>
<cfabort>
<cfelse THE NEXT PART AND WHATEVER ELSE YOU ARE DOING>
BLah blah
</cfif>
So, is there a zip with the code tghat works? first code examples dont, and I keet getting a yellow ERROR msg when i click link2 (protected)
So when you click the Demo button you get a yellow error? What is that - IE?
No, when I run the code on this page... i get a yellow alert error. but if I run login.cfm directly works fine.
/rev4/ seems to work fine... but the code currently in page isnt. but is rev4 same as whats on this page?
rev4? I have no idea what you mean.
Oh sorry - one of the comments. So no, rev4 is a newer version of the code. Just view source to see the front end HTML/JS.
Ray, nice post as always..
Can you provide a few tips/pointers or link on integrating your login with an actual dynamic table of Users/Passwords where password is encrypted?
- I've added your login to a jquery mobile site, works fine
- Just not sure about changing it from a Static UN/PW to actual UN/PW's in my database
- Using CF9/DW5.5 w/MySQL5.5 backend
Basically line 2 in the last template would become a query. You would write SQL to check if the username and password match up to a row in your user table. Something along the lines of this pseudo-code:
<cfquery name="checkauth" datasource="foo">
select userid
from users
where username = <cfqueryparam cfsqltype="cf_sql_varchar" value="#form.username#">
and password = <cfqueryparam cfsqltype="cf_sql_varchar" value="#form.password#">
</cfquery>
<cfif checkauth.recordcount is 1>
all is good
</cfif>
As for handling encryption, it depends on where you did it. If you used CF's encryption tools, then you would encrypt form.password.
I am a bit of a noob and I have no experience with Coldfusion. So when i open a CFC page.. how should it look?
Right now I have done it like this:
<cfcomponent>
component {
this.name="jqmwithlogin_rev1";
this.sessionManagement="true";
public boolean function onApplicationStart() {
return true;
}
public boolean function onRequestStart(string req) {
//logic to determine if a page is secured should probably be elsewhere...
if(listLast(arguments.req, "/") == "checklist.cfm" && !structKeyExists(session, "username")) {
location(url="login.cfm",addToken=false);
}
return true;
}
}
</cfcomponent>
Obviously this doens't work or something else doens't, because when I click on the icon which should login i get "error loading page"
You need to remove the cfcomponent tags on top/bottom.
Hi Ray,
Well I jumped into jquery mobile code with both feet thanks to your 2nd edition book.
My first webapp is here: www.tigerlilyquiltco.com
Now I'm attempting to convert a portal site at work to mobile. The portal is all cfm pages with lots ajax calls to cfc's. I use jqgrid on many pages. This will take sometime but i figure it will be a good learning experince.
My question for you today is about cf session vars which I use to hold lots of data about the current person logged in. Each person has a parameter record which I load into session vars on login then reference in the code to enable/disable section of code.
Can I just do this in the javascript after I verify the login successful:
var myparm1 = session.myparm1;
thanks,
Jim
Yes and no. So, when you use CFM to request soandso.cfm, you could obviously output dynamic JS:
<cfoutput>var foo = #session.foo#</cfoutput>
When rendered, the browser sees:
<cfoutput>var foo = 2;</cfoutput>
or somesuch. You can also use AJAX from JS to call a CFC and get the session variable as well.
But to be clear, it isn't JS 'reading' the session var - not directly I mean. Client Side is separated from Server Side.