I've searched and searched but can't seem to find the answer to what seems like a straightforward authentication scenario.
We have an existing Java web application that uses form-based authorization provided by Spring. We are attempting to access this application via our portal site without challenging the user to enter their credentials (SSO).
The portal has a credential vault and we can successfully access the secrets for the remote web application on the server side. We are using Apache's HTTP Components utility to post the login request to the j_spring_security_check and are successfully authenticating. The response to this post sends back a 302 redirect to the application home page and sets a cookie with a session id.
Now we have to somehow send this authenticated session back to the browser and this is where we are having trouble. Simply redirecting the browser to the home page doesn't work - it redirects us to the login page. Forwarding all of the response headers back to the browser exactly as received on the server-side doesn't work either - still returned to the login page.
So, how do we authenticate server-side and still be able to load the target page client-side?
I am relatively new to this so I apologize if this is a silly question. Any help or advice regarding an alternative approach is appreciated.
Notes:
HttpComponent Client code:
DefaultHttpClient httpclient = new DefaultHttpClient();
try {
// try to get the home page
HttpGet httpget = new HttpGet("http://<host>/<root>/home.action");
HttpResponse httpClientResponse = httpclient.execute(httpget);
HttpEntity entity = httpClientResponse.getEntity();
// check status and close entity stream
System.out.println("Login form get: " + httpClientResponse.getStatusLine());
EntityUtils.consume(entity);
// check cookies
System.out.println("Initial set of cookies:");
List<Cookie> cookies = httpclient.getCookieStore().getCookies();
printCookies(cookies);
/*** Login ***/
HttpPost httppost = new HttpPost("http://<host>/<root>/j_spring_security_check");
// Prepare post parameters
List <NameValuePair> nvps = new ArrayList <NameValuePair>();
nvps.add(new BasicNameValuePair("j_username", getUserFromVault()));
nvps.add(new BasicNameValuePair("j_password", getPasswordFromVault()));
httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
httpClientResponse = httpclient.execute(httppost);
// copy response headers and determine redirect location
Header[] allHeaders = httpClientResponse.getAllHeaders();
System.out.println("Headers: ");
String location = "";
for (Header header : allHeaders) {
System.out.println(header);
if("location".equalsIgnoreCase(header.getName())) location = header.getValue();
response.addHeader(header.getName(), header.getValue());
}
// check response body
entity = httpClientResponse.getEntity();
System.out.println("Response content: " + httpClientResponse.getStatusLine());
System.out.println(EntityUtils.toString(entity)); // always empty
EntityUtils.consume(entity);
// check cookies
System.out.println("Post logon cookies:");
cookies = httpclient.getCookieStore().getCookies();
printCookies(cookies);
// populate redirect information in response
System.out.println("Redirecting to: " + locationHeaderValue);
response.setStatus(httpClientResponse.getStatusLine().getStatusCode()); // 302
// test if server-side get works for home page at this point (it does)
httpget = new HttpGet(location);
httpClientResponse = httpclient.execute(httpget);
entity = httpClientResponse.getEntity();
// print response body (all home content is loaded)
System.out.println("home get: " + httpClientResponse.getStatusLine());
System.out.println("Response content: " + httpClientResponse.getStatusLine());
System.out.println(EntityUtils.toString(entity));
EntityUtils.consume(entity);
} finally {
httpclient.getConnectionManager().shutdown();
}
Headers returned from the successful login on the server side:
HTTP/1.1 302 Found
Date: Wed, 23 Feb 2011 22:09:03 GMT
Server: Apache/2.2.3 (CentOS)
Set-Cookie: JSESSIONID=6F98B0B9A65BA6AFA0472714A4C816E5; Path=<root>
Location: http://<host>/<root>/home.action
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
Via: 1.1 PPWebFilter.<host>:80 (IronPort-WSA/7.0.0-825)
Connection: keep-alive
Headers from the client side request and response:
Request:GET /<root>/home.action HTTP/1.1
Host: <host>
Connection: keep-alive
Referer: http://localhost:10039/SCMViewer/TestLoginServlet?launchScm=Launch+SCM+servlet
Accept:application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.98 Safari/534.13
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: JSESSIONID=FC8E823AB1A1545BE8518DB4D097E665
Response (redirect to login):
HTTP/1.1 302 Found
Date: Wed, 23 Feb 2011 22:09:03 GMT
Server: Apache/2.2.3 (CentOS)
Location: http://<host>/<root>/security/login.action
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
Via: 1.1 PPWebFilter.<host>:80 (IronPort-WSA/7.0.0-825)
Connection: keep-alive
As a test, we wrote a bit of a hack that seems to work, but is too insecure to be viable:
- Embedded a form on the jsp which will post the login credentials directly to the remote site's j_spring_security_check.
- Wrote a servlet method to retrieve the credentials from 开发者_运维知识库the vault.
- Filled the credentials on the client side into hidden form fields and submitted the form via javascript.
It is a bit hard to understand what your application is trying to do, but my best guess is that your 'portal' sits between the user's browser and the application, and you are trying to use the some stored credentials for the application to authenticate on behalf of the users.
There are two things you need to watch for / deal with.
The responses from the application will contain SetCookie
headers of some sort. The cookies need to be handled carefully. Depending on the security model you are using:
- They could be saved in the portal and used for future requests to the application.
- They could be relayed to the user's browser. The portal would also need to pass the cookies through in future requests to the application. (This approach needs to be handled carefully to deal with possible issues with session token leakage.)
Also, be aware that SpringSecurity changes the session cookie when login succeeds. If you don't capture the new session cookie and use them in follow on requests to the application, those requests won't be authenticated.
The application's login mechanism is clearly trying to redirect you (the portal) to the "default" place after logging in, and this is inappropriate. There are two simple fixes for this:
Have the portal detect the final redirect and treat it as an indication that you've successfully logged in. Then have the portal repeat the request for the page you were originally requesting from the application using the new cookie (see above).
IIRC, there's an extra parameter you can add to a j_spring_security_check request that tells the application where to return on successful login. I can't recall the details ...
I thought that forwarding the setCookie response header from the RA into the portal's response to the browser would be all that is needed to transfer the cookie/session id to the user's new browser window. Is that not correct?
That will cause the browser to set the RA's cookie for the portal context. That won't work unless the RA and portal are in the cookie's "scope" (for the want of a better word).
Question is, how do I display this on/through the portal? Do I just have to copy all the content over and map all the relative links accordingly? And, as you state, continue to proxy all requests to the app through the portal, passing the cookie each time? Is there any way to avoid copying/modifying the markup?
You do need to massage the markup. But exactly what massaging is required is not entirely clear. I think you'll need to map the relative links so that when the user's browser sees them they point to the portal. Then, arrange that the portal relays requests to the RA with the appropriate cookies.
One tool that you can use to deal with relative links is the HTML <base>
element. In fact, this potentially easier to deal with than absolute links ... if you map everything via the portal.
But beware that there are all sorts of things that can cause grief in this process. For example, you've got to beware of the "same source" restriction, and with javascript with embedded URLs for the RA.
In case anyone is interested, here's how everything turned out.
Once we realized the issue with setting foreign cookies, we decided we had a few options:
- Proxy - Tunnel through the portal to the remote application, using the portal as a proxy. This option is the most straightforward logically, but it has complications as mentioned above (i.e. you have to modify each request and each response - adding cookies and markup as necessary). This method turned out to be a pain point for us, not unrelated to our use of IBM WebSphere Portal 7.
- 3rd party SSO solution - Use CAS or Tivoli or some other enterprise solution. This is our ideal final solution, but it is still being researched to determine compatibility with our environment.
- Cookie Monster - Our interim solution, in order to get IBM portal out of the way as the middle man, was to deploy a small new remote application on the same server as our target app that simply accepts a cookie in JSON format and spits it back to the browser in a 302 redirect response.
The cookie monster solution works as follows: when the user clicks on the link in the portal, our portlet will internally lookup the user's credentials, authenticate to the remote application, and return the authentication cookie/token. We convert that (as well as the destination URL) to JSON and return it to the browser. The browser then posts this JSON to the remote cookie application in a new window. The cookie is reconstituted and placed in the response along with the 302 and the target location. Voila, the page redirects to the application homepage and the user is logged in. Yay!
Some notes for anyone using IBM WebSphere Portal:
- We handled the authentication via resource-serving portlet.
- Make sure the response from the resource-serving portlet is not cached (we made the cache expire immediately as we could not return no-cache)
- Make sure you ping the portal before making the ajax call as the session may be expired.
I'm sure there are other, more elegant solutions, but this is working for us until we get CAS/Tivoli up and running.
精彩评论