Automated Login for Portals using Http POST

Filed under: JSR168 Portals, SSO — lars @ 11:24:01 am

I've worked on alot of portal projects over the last couple of years, and single-sign-on (SSO) has been a common requirement for them. Often this is achieved by using Web Application Firewalls like WebSEAL or SiteMinder, sitting in front of a j2ee application server configured to trust the credentials it sends. However I recently had to find a way to make a portal on a stand-alone WebSphere server provide an automatic login to a Apache/PHP forum application that provided only a web-based login form. The forum application was hosted by a 3rd party and we couldn't modify it - that login page was the only authentication interface we could use.

I prototyped two solutions for this. Both involved creating a HTTP POST request containing the user's username and password to log the user into the PHP application. The session cookies returned from the Http Response were extracted and set in the browser so that it could continue the authenticated user session. One option involved using server-side java code to perform the login POST, the other using an ajax-style asynchronous request (XmlHttpRequests).

But first, we had to solve a problem with browser restrictions. Web Browsers prohibit the use of Cookies and XmlHttpRequests between domains/host names - which is a problem for both solutions. Therefore, it was necessary to make both of our applications appear to the browser to come from the one domain - even though they were hosted on different servers.


Using Apache's MOD_PROXY to resolve cross-domain issues

My development machine runs windows, and I used WAMP (www.wampserver.com), a simple installer for Apache, PHP and MySQL as a quick way of getting Apache up and running on my machine. Once you have Apache running, the setup of the reverse proxy is simple, simply add the following lines to Apache's httpd.conf:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

...

ProxyPass /login http://localhost:10038/AutoLoginWAR
ProxyPassReverse /login http://localhost:10038/AutoLoginWAR

ProxyPass /forum http://www.forumApplication.com/forum
ProxyPassReverse /forum http://www.forumApplication.com/forum

The above lines cause any requests received by Apache for /login (eg: http://localhost/login) to be proxied to the URL http://localhost:10038/AutoLoginWAR. Likewise, Any requests received for /forum are proxied to the server running the forum software. Therefore, the browser can access both of these applications running on different servers using the urls http://localhost/login and http://localhost/forum - and hence cookies and javascript can be used without restrictions betwee these two URLs.

Easy! Note that "AutoLoginWAR" is just the name of the WAR I used for the next step, and 10038 is the port that my local WebSphere Application Server instance is listening to.

Its also important to realise that the reverse proxy doesn't do any modification of the HTML returned by the proxied application. Thankfully the forum software I had to integrate with seems to use relative links. If it used absolute links that included the full domain name (eg: http://not.my.server.com/viewForumPost.php) then the above solution would not work, as the user would wind up accessing the application directly, not via the reverse proxy, and because the domain name changed the browser would stop sending the Cookies we had set for the reverse proxy URL.


Method 1 - Server-side login

For this method, I made use of the commons-httpclient project, also from Apache. Note that this also depends on the commons-codec project.

For the test, I created a Web project in Eclipse called AutoLoginWAR, which contained a single JSP that performed the auto-login. The session cookies returned by the application are sent to the users browser, and they are then free to be redirected to the forum to continue their logged in session:

<%@page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1" autoFlush="false" buffer="80kb" %>
<%@page import="org.apache.commons.httpclient.methods.GetMethod"%>
<%@page import="org.apache.commons.httpclient.NameValuePair"%>
<%@page import="org.apache.commons.httpclient.methods.PostMethod"%>
<%@page import="org.apache.commons.httpclient.HttpState"%>
<%@page import="org.apache.commons.httpclient.Cookie"%>
<%@page import="org.apache.commons.httpclient.Header"%>
<%@page import="org.apache.commons.httpclient.Credentials"%>
<%@page import="org.apache.commons.httpclient.UsernamePasswordCredentials"%>
<%@page import="org.apache.commons.httpclient.HttpClient"%>
<%@page import="org.apache.commons.httpclient.params.HttpClientParams"%>
<%@page import="org.apache.commons.httpclient.cookie.CookiePolicy"%>
<%
try {

//Initialise the HttpClient. The test instance of vBulletin we are using
//required HTTP authentication so this is being set up too.
//
//The below code also sets the CookiePolicy to ignore cookies. This
//means that HttpClient will ignore cookies and it is up to us to
//manually maintain their state. I also had success using RFC_2109,
//which which HttpClient will automatically pick up Cookies sent by the
//server, make them available in the HttpState object, and resend them to
//the server with every subsequent request. The only reason I chose to
//manually handle setting cookies was because I noticed that HttpClient's
//RFC_2109 method formatted the cookies in the request headers differently
//to firefox, and I wanted to keep the requests as similar as possible.
//This may not have been necessary.
HttpClient client = new HttpClient();
HttpState state = client.getState();
HttpClientParams params = client.getParams();
params.setAuthenticationPreemptive(true);
params.setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
Credentials credentials = new UsernamePasswordCredentials("testUser", "testPassword");
state.setCredentials(null, null, credentials);







//Perform the actual Login POST with username and password.
//Note that it is important to use the User-Agent value of the client
//browser. Otherwise, the HttpClient User-Agent and the browser
//User-Agent will be different, causing vBulletin (and possibly other
//applications) not recognise requests from the browser as part of
//the same session - even though the sessionId cookies are the same.
PostMethod post = new PostMethod("http://www.forumApplication.com/forum/login.php?do=login");
out.println("<br/>" + post.getURI());

NameValuePair[] data = {
new NameValuePair("login_username", "FredUser"),
new NameValuePair("login_password", "FredsPassword"),
};
post.setRequestBody(data);

Header userAgentHeader = new Header();
userAgentHeader.setName("User-Agent");
userAgentHeader.setValue(request.getHeader("User-Agent"));
post.addRequestHeader(userAgentHeader);

client.executeMethod( post );

String bbsessionhash = "";
String bblastvisit = "";
String bblastactivity = "";

Header[] headers = post.getRequestHeaders();
for (int i=0; i < headers.length; i++) {
out.print("<br/>Request Header - " + headers[i]);
}
headers = post.getResponseHeaders();
for (int i=0; i < headers.length; i++) {
out.print("<br/>Response Header - " + headers[i]);
if (headers[i].getName().equals("Set-Cookie")) {
out.print("(Cookie)");
String val = headers[i].getValue();
String name = val.substring(0, val.indexOf("="));
val = val.substring(val.indexOf("=")+1, val.indexOf(";"));
out.print( name + " - ");
out.print( val );

if (name.equals("bbsessionhash")) {
bbsessionhash = val;
} else if (name.equals("bblastvisit")) {
bblastvisit = val;
} else if (name.equals("bblastactivity")) {
bblastactivity = val;
}

}
}
Cookie[] cookies = client.getState().getCookies();
for (int i=0; i < cookies.length; i++) {
out.print("<br/>Cookie: " + cookies[i]);
/* Use this block if the CookiePolicy is set to RFC_2109
if (cookies[i].getName().equals("bbsessionhash")) {
bbsessionhash = cookies[i].getValue();
} else if (cookies[i].getName().equals("bblastvisit")) {
bblastvisit = cookies[i].getValue();
} else if (cookies[i].getName().equals("bblastactivity")) {
bblastactivity = cookies[i].getValue();
}
*/
}

//Check the response page to confirm that the login was successful
String responseString = post.getResponseBodyAsString();
if (responseString.indexOf("Thank you for logging in") > 0) {
out.print("<h4>Login Successful!</h4>");
} else {
out.print("<h4>Login Failed!</h4>");
}
post.releaseConnection();

out.print("<hr>");







//The two Subsequent GETs are probably not required and should be removed
//before this code is used in a production environment. They are present
//simply to verify that subsequent requests by HttpClient will be
//treated by the server as authenticated.
GetMethod get = new GetMethod("http://www.forumApplication.com/forum/");
out.println("<br/>" + get.getURI());

Header cookieHeader = new Header();
cookieHeader.setName("Cookie");
cookieHeader.setValue("bbsessionhash=" + bbsessionhash + "; bblastvisit=" + bblastvisit + "; bblastactivity=" + bblastactivity + "");
get.addRequestHeader(cookieHeader);

userAgentHeader = new Header();
userAgentHeader.setName("User-Agent");
userAgentHeader.setValue(request.getHeader("User-Agent"));
get.addRequestHeader(userAgentHeader);

client.executeMethod( get );

headers = get.getRequestHeaders();
for (int i=0; i < headers.length; i++) {
out.print("<br/>Request Header - " + headers[i]);
}
headers = get.getResponseHeaders();
for (int i=0; i < headers.length; i++) {
out.print("<br/>Response Header - " + headers[i]);
}
cookies = state.getCookies();
for (int i=0; i < cookies.length; i++) {
out.print("Cookie: " + cookies[i]);
}

out.print(get.getResponseBodyAsString());

get.releaseConnection();
out.print("<hr>");






get = new GetMethod("http://www.forumApplication.com/forum/forumdisplay.php?f=22");
out.println("<br/>" + get.getURI());

cookieHeader = new Header();
cookieHeader.setName("Cookie");
cookieHeader.setValue("bbsessionhash=" + bbsessionhash + "; bblastvisit=" + bblastvisit + "; bblastactivity=" + bblastactivity + "");
get.addRequestHeader(cookieHeader);

userAgentHeader = new Header();
userAgentHeader.setName("User-Agent");
userAgentHeader.setValue(request.getHeader("User-Agent"));
get.addRequestHeader(userAgentHeader);

client.executeMethod( get );

headers = get.getRequestHeaders();
for (int i=0; i < headers.length; i++) {
out.print("<br/>Request Header - " + headers[i]);
}
headers = get.getResponseHeaders();
for (int i=0; i < headers.length; i++) {
out.print("<br/>Response Header - " + headers[i]);
}
cookies = state.getCookies();
for (int i=0; i < cookies.length; i++) {
out.print("Cookie: " + cookies[i]);
}

out.print(get.getResponseBodyAsString());

get.releaseConnection();
out.print("<hr>");






//Now we need to take the session cookies sent by vBulletin and send
//them to the user's browser, so that they can continue the logged in
//session that we have created.
javax.servlet.http.Cookie sessionCookie = new javax.servlet.http.Cookie("bbsessionhash", bbsessionhash);
sessionCookie.setPath("/");
response.addCookie(sessionCookie);

javax.servlet.http.Cookie lastVisitCookie = new javax.servlet.http.Cookie("bblastvisit", bblastvisit);
sessionCookie.setPath("/");
response.addCookie(lastVisitCookie);

javax.servlet.http.Cookie lastActivityCookie = new javax.servlet.http.Cookie("bblastactivity", bblastactivity);
sessionCookie.setPath("/");
response.addCookie(lastActivityCookie);




//Last of all, we provide a link for users to continue on to vBulletin.
//You may prefer to change this to an automatic redirect.
%>
<a href="http://localhost/forum/">Go to forum</a>
<%
} finally {
}
%>

So the above code, with a bit of modification, should be able to establish an authenticated user session in alot of 3rd party app that use a web-form login, and then allow the user to be redirected to this application (via the Apache reverse proxy) and continue this logged in Session.


Method 2 - Ajax/XmlHttpRequest login

There is an alternate method that I prototyped with vBulletin. It works the same way in principal, but uses javascript to create the login requests. Lets have a look at this code:

<script language="javascript">
//Method to return an XmlHttpRequest in IE and FireFox
function getXmlHttpRequest() {
var ua = navigator.userAgent.toLowerCase();
if (!window.ActiveXObject)
return new XMLHttpRequest();
else if (ua.indexOf('msie 5') == -1)
return new ActiveXObject("Msxml2.XMLHTTP");
else
return new ActiveXObject("Microsoft.XMLHTTP");
}

// Retrieve the value of the cookie with the specified name.
function getCookie(sName, sAllCookies) {
if (!sAllCookies) {
return false;
}
// cookies are separated by semicolons
var aCookie = sAllCookies.split("; ");
for (var i=0; i < aCookie.length; i++) {
// a group of name/value pairs can be separated by an ampersand
var aCrumb = aCookie[i].split("&");
for (var j=0; j < aCrumb.length; j++) {
// a name/value pair is separated by an equal sign
aEntry = aCrumb[j].split("=");
if (sName == aEntry[0])
return unescape(aEntry[1]);
}
}


// a cookie with the requested name does not exist
return null;
}

// Set the value of a cookie with the specified name
function setCookie(sName, sValue, dExpirey) {
//Expire any current value of the cookie (IE seems to require this so that the subsequent line works)
document.cookie = sName + "=" + escape(sValue) + "; expires=Fri, 31 Dec 1999 23:59:59 GMT;";
document.cookie = sName + "=" + escape(sValue) + "; expires=" + dExpirey.toString();
}




//Performs an initial request to the forum application to get the session cookies
//(Possibly this initial request is not needed and the session cookies could be
//from the next POST request).
//Note also that we are appending a query-string parameter of Math.random() to the
//end of the URL. This is to get around a problem in IE6 where requests for URLs
//that were already in the browser cache seemed to be ignored.
function vBulletinLoginStep1() {
var loginGet = getXmlHttpRequest();
loginGet.open("GET", "http://localhost/forum/login.php?unique="+Math.random());
//loginGet.open("GET", "/index.php?unique="+Math.random());
loginGet.send(null); //if using POST, params array goes to this method
//loginGet.onreadystatechange=function() { if (loginGet.readyState == 4 && loginGet.status == 200) { alert(loginGet.responseText); } }
loginGet.onreadystatechange=function() { if (loginGet.readyState == 4 && loginGet.status == 200) { vBulletinLoginStep2(loginGet); } }
}

//Extract the session cookies from the first request and set them in the browser
//using the document.cookie object so they will be sent on every subsequent
//request. Then POST the username and password to the login URL of the
//forum application.
function vBulletinLoginStep2(loginRequest) {
var loginCookie = loginRequest.getResponseHeader("Set-Cookie");

//Extract session cookies from request
var bbsessionhash = getCookie("bbsessionhash", loginCookie);
var bblastvisit = getCookie("bblastvisit", loginCookie);
var bblastactivity = getCookie("bblastactivity", loginCookie);

if (bbsessionhash)
setCookie("bbsessionhash", bbsessionhash, "");
if (bblastvisit)
setCookie("bblastvisit", bblastvisit, "Fri, 31 Dec 2030 23:59:59 GMT");
if (bblastactivity)
setCookie("bblastactivity", bblastactivity, "Fri, 31 Dec 2030 23:59:59 GMT");

var loginPost = getXmlHttpRequest();
loginPost.open("POST", "http://localhost/forum/login.php?unique="+Math.random());
//Required for POST!
loginPost.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
//Use only if we for some reason don't want to add the cookies to document.cookie
//if (sessionId) {
// loginPost.setRequestHeader("Cookie", "JSESSIONID="+sessionId);
//}
loginPost.send("login_username=FredUser&login_password=FredPassword"); //POST params
loginPost.onreadystatechange=function() { if (loginPost.readyState == 4) { vBulletinLoginStep3(loginPost); } }
}

//Great! User is logged in! Redirect the user to the forum application!
function vBulletinLoginStep3(loginRequest) {
location.href="http://localhost/forum/?unique="+Math.random();
}
</script>

Hope this code can be useful to somebody out there!

Comments

  • Tonia
    Hello, I was exploring your code to see if I could find something useful and found this line:
    userAgentHeader.setValue(request.getHeader("User-Agent"));
    Just one question: where do you define 'request'? I can't find it on this page.

    Comment by Tonia [Visitor] · http://websworld.org/tonia — 06/20/08 @ 04:44

  • lars
    Hi Tonia,
    That example code was initially written/tested inside a JSP. The request is an implicit object that is always available when you're in a JSP without having to be defined. If you wanted to move this code into a method in a java class you'd probably need to pass it the request (or the user-agent string).

    Comment by lars [Member] — 06/20/08 @ 08:31

Leave a comment

Allowed XHTML tags: <p, ul, ol, li, dl, dt, dd, address, blockquote, ins, del, span, bdo, br, em, strong, dfn, code, samp, kdb, var, cite, abbr, acronym, q, sub, sup, tt, i, b, big, small>


Options:
(Line breaks become <br />)
(Set cookies for name, email & url)




powered by  b2evolution