Bug 3839

Summary: Problem bookmarking login page
Product: Tomcat 7 Reporter: Paul Pacheco <paul.pacheco>
Component: CatalinaAssignee: Tomcat Developers Mailing List <dev>
Status: RESOLVED FIXED    
Severity: enhancement CC: carlson, cpjunk, vicentesalvador
Priority: P5    
Version: trunk   
Target Milestone: ---   
Hardware: All   
OS: All   

Description Paul Pacheco 2001-09-26 09:13:50 UTC
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.
Comment 1 Craig McClanahan 2001-10-03 11:15:48 UTC
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.
Comment 2 Paul Pacheco 2001-10-03 14:11:04 UTC
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?????
Comment 3 Craig McClanahan 2001-10-03 15:23:08 UTC
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.
Comment 4 Remy Maucherat 2001-10-11 09:06:18 UTC
*** Bug 4104 has been marked as a duplicate of this bug. ***
Comment 5 Corey Puffalt 2002-06-11 19:40:48 UTC
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.

Comment 6 Pier Fumagalli 2002-06-11 20:06:56 UTC
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...
Comment 7 Corey Puffalt 2002-06-12 14:13:34 UTC
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.
Comment 8 Luc Vanlerberghe 2002-06-14 07:51:04 UTC
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.
Comment 9 Corey Puffalt 2002-06-14 15:10:43 UTC
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.
Comment 10 Remy Maucherat 2003-05-02 17:25:02 UTC
*** Bug 8976 has been marked as a duplicate of this bug. ***
Comment 11 Martin Algesten 2003-08-28 17:54:51 UTC
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.
Comment 12 Kahro Raie 2004-06-06 12:57:30 UTC
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.
Comment 13 Mark Thomas 2004-12-24 18:00:36 UTC
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.
Comment 14 Mark Morris 2010-10-06 15:29:58 UTC
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.
Comment 15 Mark Thomas 2010-10-20 17:57:59 UTC
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.