I have a web application that uses form based authentication. if I go to a protected page for example: http://myhost/myapp/index.html then I get the authentication form: http://myhost/myapp/login.jsp I fill it up, and submit and I get authenticated and the page http://myhost/myapp/index.html is properly shown. However, if instead of trying to go to a protected resource, I try to go directly to the login.jsp page, and that is pretty common since some people like to bookmark the login page, then this is what happens: I go to the login page: http://myhost/myapp/login.jsp the login page gets displayed properly. but if I fill it up and submit, the browser gets redirected to this address: http://myhost/myapp/null and the following error is shown on the browser: HTTP Status 404 - /null The requested resource (/null) is not available. The behavior that I would like to see is that the default page for the web application be shown. I think this is what is happening: if I go to a protected resource the url gets saved somewhere in the session then after I submit the login information, the server redirects the browers to the saved location. But if I go directly to the login page, then there is no url that failed the security constraints, and nothing is saved. After I submit, it tries to go to whatever is saved (null in this case) and since there is no page named null an error is shown. What is needed is an extra check somewhere that says: if the saved location is null, then go to the default webapp page.
It is not valid to bookmark the form-based login page. You should consider that page to be part of the *container*, not part of the *application*. Users should be trained to bookmark the page they really want to see -- exactly as if you were using BASIC authentication instead. The login page will be presented by the container if necessary (i.e. if the user is not currently authenticated). Even if you figure out a way to do this that works in one servlet container, it is pretty much guaranteed not to be portable to any other.
ohh, come on, you are saying "the solution is to fix the people who use your web application". What am I supposed to do, put in the login page something like this:"Do not bookmark this page, consider it a part of the *container*, not part of the *application*" Some of the users don't even know there is such thing as a container, and I don't see a reason why they should know.I don't see why the users should be instructed at all.Well, that really does not make it transparent for the users. I used to use weblogic and they had the same problem. They did change it to go to the default page in the web application after we contacted support.Plus the page IS part of the application, it has to be placed inside the war file, it is different for every web application, it has to be specified inside web.xml which is part of the standard. Exactly what part of the servlet standard is broken by fixing this?What good is the default page for the web application if it doesn't get shown by default?????
The fact that you hd the same problems under WebLogic also should have given you a hint that you might be mis-using this functionality :-). Although the form login page (and form error page) are physically contained in your web application archive, they should not be hyperlinked to by any of your app's pages. Most particularly, it should *not* be your welcome page. If you (temporarily) switch your app to use BASIC authentication instead, it should still work correctly - and there is no possibility to bookmark the login page because there is no such thing. If your app doesn't work in this scenario, then you should modify it so that it can. If you don't, then you're going to be dependent on non-portable behavior of whatever container vendor happens to allow this technique to work - the spec doesn't require it.
*** Bug 4104 has been marked as a duplicate of this bug. ***
I still think this is a bug. If it is not allowable for the user to bookmark the login page then the container should never expose that login url to the user. In other words, Tomcat should not issue a browser redirect to the login url but instead should do the equivalent of a jsp forward and after authentication do a redirect back to the original url. Thus there is no possibility for the user to bookmark the page and get an ugly 400 error.
Add this to the beginning of your login page: <% if (request.getHeader("Referer") == null) { response.sendRedirect("index.jsp"); } %> Replacing "index.jsp" with whatever URL you feel you want to have your connection redirected to... For example, if "index.jsp" is one of several "protected" resources, then if your users bookmark the login page, what will happen is that when your users use the bookmark, they won't have a referer, not having a referer they will be redirected to what you think it is their "default" page (after they have logged in), this (since they're not logged in) will trigger a redirect again to the login page...
That looks like a valid WORKAROUND but I believe this is something that I as a developer shouldn't have to handle. This should all be handled seemlessly by the container.
That workaround is dangerous. In some browsers (like Opera) referrer logging can be disabled, in which case you have an endless loop... Two other suggestions for a workaround (but probably non-portable): - Test isNew() on the Session object. For form-based login, the container needs to store the context of the original call somewhere. I pretty sure that tomcat uses the session object for this (though it will be hidden from the webapp) - Put the login page itself in the protected area. I believe Tomcat 4 allows this. In the login page you can then put code to test if the user is already logged in. If he is, he got there because he bookmarked the login page, got the 'container' login page, logged in and was redirected to the 'application' page (that happens to be the same). In that case, redirect to the application default page.
Yes I just discovered this myself: Mozilla 1.0 does not seem to send a referer on a redirect so I got an infinite loop. Furthermore some proxies can be set to strip out the referer.
*** Bug 8976 has been marked as a duplicate of this bug. ***
There is a dirty hack way of solving this in TC4. I should probably be flogged in public for posting such a filthy hack, but anyway... I suspect it is not very portable. In web.xml define an error page for 400 such as 'error400.jsp': In that page do something along the lines of: <% String requestURI = (String)request.getAttribute( "javax.servlet.error.request_uri" ); boolean isLogin = requestURI.indexOf( "j_security_check" ) >= 0; if ( isLogin ) { String username = request.getParameter( "j_username" ); String password = request.getParameter( "j_password" ); session.setAttribute( "j_username", username ); session.setAttribute( "j_password", password ); response.sendRedirect( "/some/protected/resource" ); return; } %> // display error message. In the form based login page do something like: <% // j_username and j_password may be set in the error400.jsp page on a // direct reference to the j_security_check page. String username = (String)session.getAttribute( "j_username" ); String password = (String)session.getAttribute( "j_password" ); boolean autoLogin = username != null && password != null; if ( autoLogin ) { session.removeAttribute( "j_username" ); session.removeAttribute( "j_password" ); %> <form name="login" method="POST" action="j_security_check"> <input type="hidden" name="j_username" value="<%=username%>"/> <input type="hidden" name="j_password" value="<%=password%>"/> </form> <script language="JavaScript"> document.login.submit(); </script> %> // now let the browser auto submit the login. return } %> // Display your normal login page.
First of all, I know that this FORM based authentication problem has been raised many times, but I really think that I have come up with a solution that would not violate the sun spec. The key to solving this problem is not to expose the login page URL to the user so it can't be bookmarked (the same goes for the error page URL). First the current flow with form based authentication: 1. Call the protected resource. (this is where the user context is saved) 2. Automatic outer forward to the custom login page. (this is the place where the users bookmark it) 3. Submit the login via j_security_check. (lets say the password was incorrect) 4. Automatic outer forward to the custom error page. (this page can also be bookmarked)(the steps 3 and 4 can be repeated many times). 5. Submit the login via j_security_check. (lets say that everything was OK) 6. Restore the original URL, delete the user context and do an aotomatic outer forward. As everybody can see the problem lies in the outer forwards to the "must be hidden" login and error page. Now concider the next flow that uses inner forwards instead. 1. Call the protected resource. (this is where the user context is saved) 2. Inner forward to the custom login page so the original URL stays in the browser. 3. Submit the login via j_security_check. (lets say the password was incorrect in which case we will save a marker to know that there was an error) 4. Now temporary recreate the original URL and do an automatic outer forward to it. As we saved a marker we won't do an inner forward to the login page anymore but to the error page. Additionally we delete the marker so that no other request gets it. 5. Submit the login via j_security_check. (lets say that everything was OK) 6. Restore the original URL, delete the user context and do an aotomatic outer forward. This new proposed solution has only two weak spots but these are unsignificant ones. 1. If the user happens to preform another separate call to the same protected resource in between the marker saveing and automatic outer forward returning then the error page will go to the wrong window. I also think it's impossible to happen in a real case. 2. The second problem is a bit bigger one. If the user will do a refresh to the login error page he will be taken to the login page instead. Now this behaviour would disturb me a lot if it weren't for the fact that usually you can't refresh form submits either plus this kind of action could also be concidered as a new request for the protected resource that it really is. As I know you all are a bit like me and won't accept the second point too easely because the fact remains that a refresh changes the users page. In that case it's possible to leave the error page logic like it works right now and hope that users don't want to bookmark error pages ;) (I myself think the refresh problem to be to small to allow users to bookmark wrong pages). Now I have just two more things to say. 1. If anybody thinks the marker can be preserved longer to fix the second problem please stop. Because a new horrible problem will arise when the user leaves our application without authorizeing and returns later to find an error page staring in his face (sessions, with the help of cokies, last a long time). 2. If anybody finds error in my reasoning please let me know but I'm sure that Tomcat can be the first web container to resolv this problem gracefully.
As has alreday been stated in this report, tomcat is spec compliant in this regard so I am marking this report as an enhancement request. Given the lack of movement on this bug since it was raised, it is unlikely that anything will be done to address this enhancement unless someone wants to provide a patch. I have also corrected a few other parameters on the original report.
I've been tracing through the Tomcat source code because of an "Invalid direct reference to form login page" error our customers frequently get and here is what was found (for Tomcat 5 and 6): When you retrieve the login page for the first time, Tomcat initializes a field that is needed for the actual login (a session.note entry). Unfortunately that field is wiped out after the session expires or the server is rebooted. Also Tomcat confuses the get request for http://<server>/j_security_check with the actual login action (see further down for an explanation in extreme detail). As a workaround for this error, we implemented the following at the end of our login module (posted with permission from Organizational Strategies Inc): org.apache.catalina.connector.RequestFacade requestFacade = (RequestFacade)request; org.apache.catalina.connector.Request tomcatRequest = org.apache.catalina.connector.TomcatRequestFacadeAccessor.extractRequest(requestFacade); org.apache.catalina.Session tomcatSession = tomcatRequest.getSessionInternal(false); org.apache.catalina.authenticator.SavedRequest saved = (SavedRequest) tomcatSession.getNote(org.apache.catalina.authenticator.Constants.FORM_REQUEST_NOTE); if(saved==null){ saved = new SavedRequest(); saved.setRequestURI("/your/path/here"); tomcatSession.setNote(Constants.FORM_REQUEST_NOTE, saved); } You will also need to make a TomcatRequestFacadeAccessor class (in package org.apache.catalina.connector), jar it up and add that jar to Tomcat's classpath. That class contains this method public static org.apache.catalina.connector.Request extractRequest(RequestFacade facade){ return facade.request; } The above is just a quickly put-together workaround for the error. We're hoping someone will implement a permanent solution into the Tomcat codebase to address this issue. To assist with that below is an explanation of the error in extreme detail 1) After rebooting everything and going to the login page for the first time, the following code in org.apache.catalina.authenticator.FormAuthenticator.authenticate() gets executed: if (!loginAction) { session = request.getSessionInternal(true); if (log.isDebugEnabled()) log.debug("Save request in session '" + session.getIdInternal() + "'"); try { saveRequest(request, session); } catch (IOException ioe) { log.debug("Request body too big to save during authentication"); response.sendError(HttpServletResponse.SC_FORBIDDEN, sm.getString("authenticator.requestBodyTooBig")); return (false); } forwardToLoginPage(request, response, config); return (false); } The loginAction flag is true when you press the login button; however, the retrieving of the login page is NOT a loginAction so the above block executes 2) In that block of code, the call to saveRequest(request, session) eventually executes the following line: session.setNote(Constants.FORM_REQUEST_NOTE, saved); 3) Later on when you press the login button, this code in FormAuthenticator.java tries to use that session.note value: requestURI = savedRequestURL(session); and savedRequestURL returns the session.note value set when the login page was originally retrieved: session.getNote(Constants.FORM_REQUEST_NOTE); 4) Unfortunately if the block of code from 1) is not executed, then the session.note value won't be set, requestURI will be null and this will run, giving you the "Invalid direct reference to form login page" error: requestURI = savedRequestURL(session); if (requestURI == null) response.sendError(HttpServletResponse.SC_BAD_REQUEST, sm.getString("authenticator.formlogin")); 5) The block of code from 1) won't execute and you'll get the "Invalid direct reference to form login page" error in the following conditions: a) If the server is rebooted and the user already has a login window open b) If the user's session times out and they already have a login window open c) If they go directly to URL http://<yourserver>/your/path/here/j_security_check (many users may have this bookmarked because it's the browser URL you see after invalid logins) The reason this URL doesn't work is because the loginAction flag toggling the block of code from 1) is set like this: boolean loginAction = requestURI.startsWith(contextPath) && requestURI.endsWith(Constants.FORM_ACTION); //this is "/j_security_check" and going to http://<yourserver>/your/path/here/j_security_check will set loginAction to true (Tomcat thinks you clicked the login button) - one way to fix this is to make loginAction false for GET requests Many thanks in advance for whoever makes the FormAuthenticator changes to fix this. Thanks, Mark Morris.
I have implemented a work-around for this issue for Tomcat7 and it will be in 7.0.5 onwards. It is unlikely to be back-ported to earlier versions. The solution is based on Mark Morris's suggestion. It adds a landingPage attribute to the FormAuthenticatorValve that can be used to define where to send the user if they request the login page directly or take so long to log in the session expires.