PDA

View Full Version : Ext.ux.J2EEAuth extension



Marte
15 Feb 2008, 6:08 AM
Hi everybody :)

This is a new extension to catch and handle the J2EE FORM authentication mechanism inside Ext.Ajax requests, to avoid page reloads.

I tested it against the embedded Tomcat in JBoss v4.2.0-GA application servers. Tested browsers include Firefox v2.0.0.11 and IE6. If you find any bug don't hesitate to contact me.

See the README file inside for details.

coderobo
24 Jun 2008, 7:16 PM
Thanks Marte -

This looks good, I just downloaded and browsed the sources. I am in the same camp trying to get J2EE Form Auth to behave with the Ajax world. I will try this tomorrow.

Regards

Marte
25 Jun 2008, 5:49 AM
Hi, coderobo.

I am actively using the extension, so feel free to contact me if you need to. :)

Cheers!

coderobo
25 Jun 2008, 8:27 AM
Hi Marte -

I just tested the extension and did not have the beginner's luck the first time through :(

When the browser send an AJAX POST request from an extJS store load:



POST /webmin/jsp/itables/usr-json.jsp HTTP/1.1
...
X-Requested-With: XMLHttpRequest

I can see the XHR header being sent by the browser. However as soon as the J2EE server on the backend notices that the request is unauthenticated, it looks at the form login descriptor in web.xml



<form-login-page>/auth</form-login-page>
and issues a redirect (302) to the /auth page. At that point the browser follows the redirect and sends an /auth request to the J2EE server and drops the XHR header! Now the J2EEAuthServlet does not have any way of knowing that the request was an Ajax request so it sends the ordinary login.jsp page back instead of a JSON authRequest. That messes up the JSON request royally.

My question is without modifying the guts of the J2EE server's form auth mechanism how can we avoid the 302 redirect trap ? Since the redirection to /auth page is coming from within J2EE server, its too late for J2EEAuthServlet to intercept the Ajax X-Requested-With header.

How did this work for you?

Regards

Marte
25 Jun 2008, 9:54 AM
Hi, coderobo

What J2EE server are you using? It might need special treatment, since I don't recall to get a redirect, but just a normal response from the "/auth" servlet.

By the way, please check that "/auth" is correctly mapped in the web.xml file to a valid servlet. Something like in the packaged example:

<!-- Your security servlet -->
<servlet>
<servlet-name>J2EEAuthServlet</servlet-name>
<servlet-class>examples.J2EEAuthServlet</servlet-class>
</servlet>

<!-- The mapping. This URL has to be publicly accessible, not under the restricted area -->
<servlet-mapping>
<servlet-name>J2EEAuthServlet</servlet-name>
<url-pattern>/auth</url-pattern>
</servlet-mapping>

Otherwise it could be being understood as a normal web resource, leading to the 302 redirect.

coderobo
25 Jun 2008, 11:06 AM
Hi Marte - The Form based Auth redirects on encountering a page that is not authenticated. That seems to be part of the Servlet specs - See http://java.sun.com/j2ee/1.4/docs/tutorial/doc/Security5.html#wp483393 I use the Jetty 6.1.11 as my servlet container. Having actually implemented several servlet containers at Sun & AOL, I know most of them implement the Form Auth the way the specs require. I suppose some container may use the servlet forward'ing mechanism instead of issuing a redirect to the form-login-page and thus preserving the XHR header. If I do not use the std J2EE form auth then I could have a LoginServlet check the XHR header. The J2EEAuthServlet gets invoked correctly I can see that in the debugger. Any page defined as a form-login-page in web.xml is treated as unprotected by the container so that isn't an issue. If you have the time could you trace the path of a JSON post followed by auth step in your env. You could use FireBug or LiveHttpHeaders for instance to track the Http requests ? Regards

coderobo
25 Jun 2008, 5:39 PM
Marte - The AJAX authRequest works as expected, the login screen is popped up when your interceptor determines the ajax call is unauthenticated. However the existing Ajax call returns as soon as it gets the authRequest JSON response back instead of waiting for auth to finish. The redirect is still an issue though. Regards

Marte
26 Jun 2008, 12:16 AM
Hi, coderobo

I captured a TCP stream of my login procedure with wireshark. Please take a look at it to see if the behavior is the same as in your container (which I assume it's not). Can you send me a trace of yours? The stream reflects the following:

a) I try to get the root of the secured web site, at context "regent".
b) Status code "200" is returned (instead of redirect) along with the login page.
c) The login page triggers the AJAX auth.
d) Server detects the unauthorized request and returns an invalid login. Now the popup shows.
e) I use wrong credentials "test":"test", and post to "j_security_check". The resulting JSON is a denied login, with status code "200".
f) Now I use valid credentials. Status code "302" is returned, and the extension executes again the original AJAX, the one triggered in step c).
g) The login page gets its response and does a "document.location = 'index.jsp';"

This stream is only valid on startup, since you have no other option to bootstrap the AJAX login. While inside a running app, if your session expired, any attempt made from, let's say, a grid, will act as if you started at step c), but ending at step f), so the grid would get its data.

Regards.

Marte
26 Jun 2008, 12:21 AM
Coderobo, I didn't understand the sentence "However the existing Ajax call returns as soon as it gets the authRequest JSON response back instead of waiting for auth to finish."

Do you mean that the code actually runs but the resulting data is not forwarded to the original AJAX call? Or that the AJAX call returns with the authRequest JSON as a (wrongly) valid response, interrupting the rest of the login procedure?

Can you send me a trace? The one made with wireshark would be perfect. Just apply a filter like "port 8080" (or the one you're using for web) and once the conversation is captured, click on the original request (GET or POST) and select "Follow TCP stream".

Thanks!

coderobo
26 Jun 2008, 9:25 AM
Hi Marte -

Attached is the trace off a FireFox 2.0.0.14 browser session. You can see at step (b)
a GET to the /auth is being generated by the browser in response to a previous redirect 302 from server. In my case the main index page has no security constraint. The user can
click on any link/menu and if the URL is protected security would be triggered. In the
attached trace the user is trying to actually do a JSON request via login/getuser.jsp.

The /auth request unfortunately doesn't have an XHR header and ends up fetching the
normal login page in step (c) instead of the authRequest JSON response.

Interestingly on IE7, at step (b) the browser is sending an XHR header after the redirect!!
So on IE7 it works fine as the J2EEAuthServlet is able to see it as an AJAX request!

From your trace I can see no 302 redirect, seems like Tomcat is just doing an internal forward and sending the appropriate login page.

Question now is if this is a FF bug - not putting all the headers in the redirected request.
IE7 appear sto be doing the right thing.

Regards


Hi, coderobo

I captured a TCP stream of my login procedure with wireshark. Please take a look at it to see if the behavior is the same as in your container (which I assume it's not). Can you send me a trace of yours? The stream reflects the following:

a) I try to get the root of the secured web site, at context "regent".
b) Status code "200" is returned (instead of redirect) along with the login page.
c) The login page triggers the AJAX auth.
d) Server detects the unauthorized request and returns an invalid login. Now the popup shows.
e) I use wrong credentials "test":"test", and post to "j_security_check". The resulting JSON is a denied login, with status code "200".
f) Now I use valid credentials. Status code "302" is returned, and the extension executes again the original AJAX, the one triggered in step c).
g) The login page gets its response and does a "document.location = 'index.jsp';"

This stream is only valid on startup, since you have no other option to bootstrap the AJAX login. While inside a running app, if your session expired, any attempt made from, let's say, a grid, will act as if you started at step c), but ending at step f), so the grid would get its data.

Regards.

coderobo
26 Jun 2008, 9:29 AM
Coderobo, I didn't understand the sentence "However the existing Ajax call returns as soon as it gets the authRequest JSON response back instead of waiting for auth to finish."

Do you mean that the code actually runs but the resulting data is not forwarded to the original AJAX call? Or that the AJAX call returns with the authRequest JSON as a (wrongly) valid response, interrupting the rest of the login procedure?


Ignore my rambling, that was a snafu on my end. Your code to queue pending AJAX request till login completes is working fine. My only issue as I noted in my last reply is with FF2 not putting an XHR header in the /auth request.



Can you send me a trace? The one made with wireshark would be perfect. Just apply a filter like "port 8080" (or the one you're using for web) and once the conversation is captured, click on the original request (GET or POST) and select "Follow TCP stream".

Thanks!

See my previous post for the trace.

Marte
26 Jun 2008, 10:37 AM
Hi, coderobo

Checking the "X-Requested-With" header in my project is a must, because I don't allow the user to navigate since there is no public area. So, for the login procedure to actually work, I need to submit a HTML fragment to allow the user to type in the credentials. This is the reason why my servlet checks if the request is made within an already loaded page or it comes from the initial GET.

Since you already have that HTML in the browser, you can safely remove the check for "X-Requested-With" header in the servlet. All your requests to the security servlet are actually AJAX requests, so you don't need to provide HTML. Just return the proper JSON fragment.

Let me know if it worked! :)

Regards.

coderobo
26 Jun 2008, 11:38 AM
Hi Marte -

Thanks, that works for all requests that are pure Ajax. However I do load via "autoLoad: url"
external HTML directly into some tab panels in my viewport. Using pure Ajax auth in the server breaks those pages.

Is there a way in extJs to make an Ajax call to fetch an HTML page and then have the responseText be rendered into a specific panel ? If I could do that then I can safely use the Ajax auth mode always.

I see couple of other approaches:

1. Intercept Ajax calls and put an extra param in the URL like isAjax=true that way even after a redirect the server can use the param instead of the XHR header to figure out the Ajax request. I am thinking it should be possible to put that in th emaskedrequest that you use to overwrite Ajax.request.

2. The other possibility is to modify the embedded Jetty servlet container I am using to store the Ajax XHR header in a session before it redirects to login page. That session attribute would be available to the J2EEAuthservlet even after a redirect

Regards

hendricd
26 Jun 2008, 12:08 PM
@coderobo -- Been following your progress. ;)

RE: option 1 -- You can set:


Ext.Ajax.extraParams = {isAJax:true};and that would get sent on every request, but using that approach would require POST/HEAD... scrutiny for that value as well.

Marte
26 Jun 2008, 12:45 PM
@coderobo
@hendricd

All your approaches are pretty valid. The point is to let the server know where to send real HTML and where to send an authrequest ticket.

The extension uses the minimum possible parameters, in order to keep communications intact for the most possible applications. Your mileage may vary. In fact, I was able to distinguish the HTML from the auth by using a unique flag I found valid for my project (the X-Requested-With). As you discovered, this flag is not valid for your project, but any other indicator would work.

As hendricd say, adding the extra param in the global "Ext.Ajax.extraParams" is certainly good. I would not add it into the extension itself because other apps might not need it (or it could cause side effects, which is worse).

Please note the paragraph in the TabPanel docs, about autoLoad: "A valid url spec according to the Updater Ext.Updater.update method."
The specs at http://extjs.com/deploy/dev/docs/output/Ext.Updater.html say the following about update: "Performs an asynchronous request, updating this element with the response. If params are specified it uses POST, otherwise it uses GET."

Summarizing:

1.- Parameters can be added just in the concrete places that need to tell the server not to emit JSON, but HTML.
2.- autoLoad is asynchronous, which makes me think it uses AJAX anyway. The extension *should* catch the JSON response from the server and then popup the login form. Once authenticated, the proper HTML should be obtained.

I have not yet tested this latter case. To be honest, I never used autoLoad before. If you have a simple testcase for me to try and debug, it is welcome! :-)

Regards

coderobo
26 Jun 2008, 12:59 PM
Thanks Doug -

That breaks the whole app I am afraid, as all the requests are turned into POST :D
There got to be a way to encode the param in the URL

hendricd
26 Jun 2008, 1:03 PM
Well....

I suppose I could add a .urlParams member to basex 3.0. ;)

coderobo
26 Jun 2008, 1:19 PM
@coderobo
2.- autoLoad is asynchronous, which makes me think it uses AJAX anyway. The extension *should* catch the JSON response from the server and then popup the login form. Once authenticated, the proper HTML should be obtained.

I have not yet tested this latter case. To be honest, I never used autoLoad before. If you have a simple testcase for me to try and debug, it is welcome! :-)

Regards

While I am trying the flag-in-param approach just wanted to let you know (2) doesn't work. I do get the Login window (so its using Ajax to do autoLoad) but after the auth is finished, the autoLoad never completes just hangs trying to load the HTML.

The autoLoad coul dbe done in any panel I am doing it in the TabPanel, don't have a isolated testcase but here is some code snippet :



function createHtmlPanel(title,url) {
return new Ext.Panel({
title: title,
frame: true,
autoLoad: url
});
}

var tabPanel = new Ext.TabPanel({
id: 'tabs',
region: 'center',
border: false,
margins:'3 3 3 0',
activeTab: 0,
deferredRender:false,
enableTabScroll:true,
monitorResize: true,
items: [
createHtmlPanel('Admin Home Page','../html/adminstart.html')
]
});

coderobo
26 Jun 2008, 2:13 PM
Here is the error I get when using autoLoad after the login completes successfully. The page keeps loading.


response.argument has no properties
http://localhost:8443/js/extjs/ext-all-debug.js
Line 5426

response.argument has no properties
processSuccess(Object tId=10 status=200 statusText=OK)ext-all-debug.js (line 5426)
apply(function(), Object el=Object defaultUrl=../jsp/test.jsp events=Object, [Object tId=10 status=200 statusText=OK, Object url=../jsp/test.jsp scope=Object timeout=30000], undefined)ext-base.js (line 9)
callback(Object url=../jsp/test.jsp scope=Object timeout=30000, true, Object tId=10 status=200 statusText=OK)j2eeauth-src.js (line 263)
apply(callback(options, success, response), Object form=Object options=Object type=submit, [Object scope=Object timeout=30000, true, Object tId=10 status=200 statusText=OK], undefined)ext-base.js (line 9)
handleResponse(Object tId=10 status=200 statusText=OK)ext-all-debug.js (line 5136)
getViewWidth(Object conn=XMLHttpRequest tId=10, Object scope=Object argument=Object timeout=30000, undefined)ext-base.js (line 10)
getViewWidth()ext-base.js (line 10)

chrome://firebug/content/blank.gif if(response.argument.form && response.argument.reset){

coderobo
26 Jun 2008, 3:34 PM
More debugging for the autoLoad case -

The maskedRequest method is blowing up in

Ext.callback(options.success, options.scope, [response, options]);All the params to the call are well defined except the response object has undefined arguments i.e. response.arguments is undefined. The callback eventually lands inside


Line 5426 : if(response.argument.form && response.argument.reset){in ext-all-debug.js where the if statements throws an exception due to response.argument being undefined. I don't know enough of ext yet to be able to say why an HTML response would cause this.

The other problem with autoLoad is when the Login screen is raised, the JSON authRequest response is loaded into the panel as html. See attached screen shot. In other words the pending mechanism that you have built only defers the JSON data load but doesn't prevent the rendering of HTML. Even with pure JSON scenarios like grid loading, I can see the outline of the grid being rendered while the login screen is getting popped. Its some screw up with the Ext.callback I think.

Marte
27 Jun 2008, 12:43 AM
@coderobo

I created a testcase by copy&pasting your snippet. I got the error you mentioned before, the one saying "argument is undefined". So it has to be something special with the autoload feature. I will focus on this error just now. :)

But I don't get the JSON rendered on screen. What version of Ext are you using? I attach a snapshot of the page as it's rendered without my intervention (i.e., no login attempts). After a wrong login, the screen keeps the same. The error shows when the login succeeds.

Marte
27 Jun 2008, 12:49 AM
@coderobo

... interesting... with the minified version (j2eeauth.src), it works until reaching the "argument is undefined" error... but with the non-minified version (j2eeauth-src.js) it produces the JSON dump you got before... :-?

Marte
27 Jun 2008, 3:26 AM
Hehehe, please forget my last post, it was my fault (I was needing a coffee)...

@coderobo
Please check the following in your j2eeauth-src.js file. Go to line 269, the one that does:



...
/* Restore the original options object. */
options = savedOptions.removeKey(options.maskedAjaxId);
...


and add the following:



...
/* Restore the original options object. */
options = savedOptions.removeKey(options.maskedAjaxId);
if (!response.argument) {
response.argument = options.argument;
}
...


It should make your code work. I forgot to restore the response's argument to match the original request.

If it all goes Ok, I'll prepare a new package. :)

coderobo
27 Jun 2008, 5:56 AM
YES it works finally. =;

Bravo Marte! Thanks for your persistence in helping out. This opens the way for J2EE applications to integrate authentication nicely with Ext. I am sure many will benefit from reading this thread.

The JSON display problem was also solved - while debugging I was messing around with j2eeauth-src.js and had commented out the 'delete o.success' line. FYI, I am using the latest ext ver 2.1.



/* Intercept the original callback handler. */
o.callback = callback;
//delete o.success;
delete o.failure;
ajaxRequest.call(Ext.Ajax, o);
Once I put your fix in and uncommented the 'delete' line that resolved the issue. It appears the default success handler was rendering the JSON content to the panel.


The reason I wanted to comment out the 'delete' call was to be able to attach my own custom login form success handler. I need to fire a handler after a successful login to dynamically update one of my panels.

After commenting our the delete call I also modified the j2eeauth-src to try adding a 'onSignIn' property to the Login config:



Ext.namespace('Ext.ux.J2EEAuth');
Ext.apply(Ext.ux.J2EEAuth, {
loginActionConfig: {
text: 'Login',
tooltip: {
.....
,onSignIn: Ext.emptyfn
,onSignInFailure: Ext.emptyfn

Ext.ux.J2EEAuth.LoginWindow = function() {
..
loginForm.form.submit({
url: Ext.ux.J2EEAuth.formAuthenticationURL,
success: Ext.ux.J2EEAuth.onSignIn,
failure: Ext.ux.J2EEAuth.onSignInFailure,

In my code I then set


Ext.ux.J2EEAuth.onSignIn = mySignInHandlerMy handler does fire but by then the JSON response also gets rendered. Probably the default handler is getting chained somehow.

Regards

Marte
27 Jun 2008, 8:18 AM
@coderobo

Happy to read that! :)

I attach the new version v1.1 with the bugfix and longer explanation about the "X-Requested-With" trick.

Regarding your custom success handler, there were already a built-in one. Just attach a callback handler in Ext.ux.J2EEAuth.successCallback and you're done. :) This handler will be called after the ones attached to the request itself, and only if the request needed authorization *and* it was successful (no matter how many attempts it required). So, unauthorized requests will not call this handler, nor any other requests made while being authenticated (since they don't trigger the extension).

The handler syntax is:



Ext.ux.J2EEAuth.successCallback = {
fn: yourfunction,
scope: yourscope
};


You can see all valid config options in the j2eeauth-src.js file, just at the beginning. Under the namespace declaration you'll find the defaults applied. :)

Cheers!

coderobo
27 Jun 2008, 9:02 AM
You rock!

Everything is working like a charm, downloaded the latest version. I am using the successCallback to update the panels.

I deal with the HTML versus Ajax issue by putting a 'noajax' url param for pure HTML requests and have the modified J2EEAuthServlet check for XHR hdr as well as the 'noajax' param. Since direct HTML access for my app is rare this is ok model to us.

Big thank You, if you are ever around the SF Bay area I will buy you a drink/lunch :)

Marte
27 Jun 2008, 11:09 AM
Hehehe :)
Thank you for your help in finding that bug. The wiki page at http://extjs.com/learn/Extension:J2EE_FORM_Authentication_Hook is now updated with the changelog and new download.

Cheers!

pavi
9 Sep 2008, 10:28 AM
I am having issues downloading the Ext.ux.J2EEAuth (http://extjs.com/forum/attachment.php?attachmentid=7798&d=1214583273) library. Can some one please upload it again.

hendricd
9 Sep 2008, 12:06 PM
Try downloading it with any browser except IE. vBulletin gzips zip files /:) and IE hates that.

pavi
10 Sep 2008, 5:02 AM
Thank you.

archmisha
19 Nov 2008, 10:58 AM
In your readme you state "you should make sure that a different user with different rights is not being POSTed, or the
complete custom-generated page would become incorrect in the best case and compromised in terms of security in the worst"

How can i make sure that?

Marte
19 Nov 2008, 3:37 PM
There are many ways to achieve it. An easy way to make sure a different user is not being POSTed is by disabling the username field of the login window at the first successful login. This way, once the session expired the only editable field would be the password field. For the user to change its username, he/she has to reload the page and therefore your custom code has a chance to be re-generated for this specific user. This applies only if you are generating different javascript code depending on the user who initially logged into the system, a design approach that is somehow difficult to defend.

Of course, you should never rely on client code for security. The client code can be patched quite easily. Always enforce the security in the server-side. Making checks in client-side makes sense, for example, if you want to alleviate the go-and-back of requests that you know in advance they won't succeed.

What I use to see is privilege broadcasting on every successful login: every top-level container is responsible of enabling/disabling sensitive controls (i.e., the "delete" button, the "superuser" tab, and so on). All client-side code is the same, no matter what user logs in (I mean, no per-user JS is generated). When the session expires and a (possibly different) user logs in, his/her privileges are received in the client-side and all containers are informed of the situation. The security is enforced in the server. If anybody patched the client code, he/she would be able to interact with the controls (as they are enabled), but the server would refuse to send/receive data as long as the user is not allowed to do so.

What I wanted to mean in the README is that it might not be a good idea to make per-user generated JS code as long as it involves security, so you better keep an eye on this to avoid side-effects. Your mileage may vary, of course. :-)

Cheers!

archmisha
25 Nov 2008, 2:04 PM
I am getting a strange error when the login screen appears and i press login i get:
illegal character: (GIF89a
pointing with firebug to the line:
authAttempt = o.scope.options.params._auth_;
in function maskedRequest(o)

ANy idea what is going on?

Marte
25 Nov 2008, 2:56 PM
Honestly, not a clue, given that information. :-?

Please check the server's response. Also make sure you've not typed a bad character into any of the sourcecode files by mistake. Note that the first six bytes of the enhanced GIF file format are "GIF89a". You might be trying to include a GIF file as a javascript file.

Regards.

archmisha
26 Nov 2008, 1:30 PM
Hmm, can you guide me on how to check server's real response?
Cuz the result is not from any servlet, its the servers form authentication mechanism..

I checked and i havent added some character on the file or something like that.

And it is not always this gifs chars. Other times i just get illigal character with this message:
"authAttempt = o.scope.options.params._auth_;"

And it happens only when i type the correct password. If for example i type a wrong one then it works fine (not authenticates, but no errors - as expected)

vdextjs2009
2 Jun 2009, 11:14 AM
Hi,

I am using the extension but I am getting options as "undefined" at o.scope.options.params._auth_. In the firebug I don't see options under the object (o) scope. Not sure what I am missing. Can you please help me out.

bareflix
30 Apr 2010, 12:31 PM
Is anyone working on porting this to Ext.direct? This kind of functionality really needs to be in the core library because the user could get logged out at any time.