<img height="1" width="1" style="display:none;" alt="" src="https://px.ads.linkedin.com/collect/?pid=299788&amp;fmt=gif">

Portal Applications: Single Sign On with Portlets

Portal, Software Solutions

Introduction

This blog will cover at a minimum what you need to do for a simple form-based Single Sign On (SSO) in the context of a portlet, though parts of it could be applicable for any application. This scenario will come up when you have two applications that are incompatible because they may have different SSO solutions in place. So for IBM solutions like Websphere Portal, its LTPA token will allow you to have SSO easy enough for an associated program like Lotus Quickr server. But another application by another vendor will not be able to use LTPA tokens, thus you'll need to roll your own solution.

When I was programming this solution for a client, I had to hunt around for each piece of information separately. My intention with this blog is to collect all the information in one place to make it a bit easier to see the whole picture and help you get your solution in place quicker.

We're assuming in this scenario that the user logs into the portal and is going to a page that either has links in it to click that only work in the context of being signed into another web application or seeing a viewer portlet for a web application—let's say a web clipping or simple iFrame, so that you are already logged in when you on the home page.

 

Authentication on Behalf of the User

Normally when a user logs into a web application with cookie-based sessions, they go to a login screen and submit a form with their username and password. When authenticated, the application sends back session cookie(s) in the header response which are placed in the user's browser. When the user makes additional requests to that web server, the browser automatically sends the applicable cookies along with the request. The web application seeing the new request recognizes the session cookies submitted and is able to validate you as a valid logged-in user.

What we're achieving with this new piece of code is getting the portal to log into the web application on behalf of the user. As part of this, the portlet will submit a POST request with the username and password (matching the field names of the login page) and then pass back the session cookies that are retrieved from the web application to the user's browser. With those cookies in the user's browser, it's as if the user logged in himself. There are a few other changes to make this work, though.

 

Portlet configuration

For the portlet, you must turn on two-phase portlet render mode in order to pass back the cookies in the header response.  

Two phase is turned on by placing the following snippet into the portlet.xml configuration file:

<portlet>
…
<!— turn on two phase render to enable sending cookies to browser —>
<container-runtime-option>
<name>javax.portlet.renderHeaders</name>
<value>true</value>
</container-runtime-option>
</portlet>


What two phase render does is allow setting of the render headers in the first phase, and then render markup in the second. Without this configured, it won't matter if your code is set to return a cookie—it will go nowhere.


For a standard JSR portlet, you get an extra method to handle this:

protected void doHeaders(RenderRequest renderRequest, RenderResponse renderResponse) {
// do some header cookie code
}


This method will be called first, followed by the 'doView' method. But, let's say you want to use a portlet filter or a SpringMVC controller's view method instead of using a standard portlet?

Well, what happens is those methods are called twice during the render phase. In order to avoid calling the same code twice, you must figure out which phase you're in by checking the request attributes.

  • PortletRequest.RENDER_PART - name of the request attribute to retrieve which phase you're in.
  • PortletRequest.RENDER_HEADERS - name of the first phase - render headers for placing in the cookies
  • PortletRequest.RENDER_MARKUP - name of the second phase - render the markup of the portlet.

You can then execute code based on the value of phase.

For example, this would check to see if you're in the header phase:

if (PortletRequest.RENDER_HEADERS.equals(renderRequest.getAttribute(PortletRequest.RENDER_PART))) {
// do some header cookie code
}

 

Scope of session cookie's domain attribute

When a browser receives a cookie from a web application, it will pass that cookie back with each request. It only does this, however, for the site it matches (the domain attribute on the cookie matches the site). For example, the session cookie for your gmail login won't be sent on requests to your yahoo mail on the other browser tab.

A browser won't accept a cookie from a site if it doesn't match the scope of that domain.

Let's take a look at the URLs for our given scenario:

  • Portal site: myportal.mydomain.com
  • Web application: myapp.mydomain.com

Normally, if the domain attribute isn't explicitly set on the session cookie for a web application, it will be set to the full name shown in the URL. In the case of the web application above, that's myapp.mydomain.com. We can see that we'll have a problem setting the web application cookie on the user's browser since our portal URL doesn't match. We can't even change the domain in our code to match it as the browser will just reject it.

no-domain-cookie

So, how do we get past this problem? Configure it to have a common 'wildcarded' domain attribute with a leading period:  .mydomain.com
both-domain-cookie
With this domain attribute set on the session cookies for both sites, the cookies are allowed to be set by either site and both sets of cookies will be submitted by the browser to either application. Pay attention to whether more than one cookie is used for sessions. In the case of Websphere Portal, both the JSESSIONID cookie (under the application server's web container settings) and LTPA domain (under security settings) both need to be set.

When choosing the shared domain attribute, it doesn't have to be the domain name only.  If you follow a convention for the naming of your application servers you can add onto it.  For instance, if all your QA instances follow the dot notation of adding 'qa', then you could set the domain attribute to:  .qa.mydomain.com

This would further limit the scope of the cookies.


Please note: If you're only performing SSO in one direction, then you only have to set up the shared domain attribute on the remote server that your code will be signing into on behalf of the user.  So in our case, we don't have to set it up on the portal server, only the remote web application server.  If SSO was to be performed in the remote application to log us into the portal, then the domain attribute would have to be set in the portal.
When testing out the domain attribute change, be sure to use a browser that makes viewing cookies relatively easy.

First, clear all the cookies (so you won't see old junk) and log into each site independently to verify that the cookies have their domain set properly. Once you see that they're handled properly, you can test the SSO solution after. Browser tools like the Web Developer Extension for Firefox can allow you to view cookies in the context of what would be submitted for that site. You can then verify that both sets of cookies are shown regardless if you're on a browser tab with the portal or a tab with the web application.

 

Other considerations

Since this is a simple scenario, we're not covering anything like making sure the user is still logged into the second web application after some time has passed or whether they're already logged in before they get there. Those scenarios are specific to the web application and you should be able to put together some code to test any of those given scenarios.

 

SSO Code

 

Placement of code

You can place this SSO code into the portlet with the needed content.  However, if it's a portlet you can't modify, like a viewer, then you can create a new portlet without any viewable information and place it anywhere on the page. It will be processed before the dependent content is shown.

Depending on how you're using your own portlet, you can place the code in either the portlet or a portlet filter. The filter is a nice option as you won't have to see the code in the portlet where view code is handled. Also, you can code the filter to show an error message to the user if the web application is out of service. This way you'll know that if your portlet view code is reached, you can assume a valid session for the other web application.

 

Credentials to use

Something you'll also need to figure out on your own is how you'll get the user's credentials to use for the web application. One way is to present a page to the user the first time they navigate to the portlet asking them for their credentials. Those credentials can then be stored in a database or portlet preferences. If the credentials are the exact same between both applications, then you can find a way to share them between the login portlet and your SSO portlet. For Websphere Portal, a solution can be created using a custom login portlet that stores the username and password into a shared credential vault slot. Then in the SSO portlet, those credentials can be retrieved and used.

 

Main SSO Snippet

This code is using the Apache HttpComponent's HttpClient classes in order to post the login form and retrieve the session cookies from the web application. I didn't see a clear and easy way of transforming the HttpClient cookies to the standard servlet HTTP cookies, so I just set them directly and used a little math to set the max age that I found (link in resources). If I find a simpler solution, I'll re-edit this blog.

Once the code is executed and the session cookies have been put into place, we're just setting a simple boolean on the session to indicate that we logged in so the code won't execute on every request. You may ultimately choose another mechanism for tracking this, especially if you consider needing to re-log into the web application after timeout.

Other considerations may need to be made when you're doing your login.  For example, if there are any parameters or cookies you'd receive from landing on the home page needed to submit in your login post request, you may need to first perform a 'get' on that page first.

This sample is using the portlet's doView method but can be placed in a portlet filter or Spring MVC Controller.

@Override
protected void doView(RenderRequest request, RenderResponse response) throws PortletException, IOException {
// we only want to perform this particular function during the render headers phase
// so that we can place the cookies in and it isn't executed twice
if (PortletRequest.RENDER_HEADERS.equals(request.getAttribute(PortletRequest.RENDER_PART))) {
PortletSession session = null;
try {
session = request.getPortletSession();
         login(session, request, response);
      } catch (Throwable t) {
log.error("Exception received while trying to authenticate for SharePoint", t);
} finally {
         // not authenticating to web app is not a deal breaker
         // at this point of time -- prevent the filter from trying again
         if (session != null) {
          session.setAttribute(SESSION_KEY, true);
         }
}
}
}
public void login(PortletSession session, PortletRequest request, PortletResponse response, FilterChain chain)
throws IOException, PortletException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, URISyntaxException {
// A value for SESSION_KEY indicates that we already checked in
if (session.getAttribute(SESSION_KEY) == null) {
// some sample data - you'll need to retrieve your credentials to use
String myUserName = "myUserName";
String myPassword = "myPassword";
log.info(String.format("Starting the login process for: %s", myUserName));
// this portion is for convenience only to trust self signed certificates and
// should only be used for testing purposes. If a self signed cert is used,
// it should be stored in the portal's truststore. Then you won't have to use
// this code and pass SSLConnectionSocketFactory into the http client.
SSLContextBuilder builder = new SSLContextBuilder();
builder.loadTrustMaterial(null, new TrustSelfSignedStrategy());
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(builder.build());
BasicCookieStore cookieStore = new BasicCookieStore();
CloseableHttpClient httpclient = HttpClients.custom()
.setSSLSocketFactory(sslsf)
.setDefaultCookieStore(cookieStore)
.build();
try {
HttpUriRequest login = RequestBuilder.post()
.setUri(new URI("https://myapp.mydomain.com/login"))
.addParameter("username_field", myUserName)
.addParameter("password_field", myPassword)
.build();
CloseableHttpResponse httpResponse = httpclient.execute(login);
try {
HttpEntity entity = httpResponse.getEntity();
log.info("Response status from my app: " + httpResponse.getStatusLine());
EntityUtils.consume(entity);
if (cookieStore.getCookies().isEmpty())
log.info("No session cookies found for my app!");
for (Cookie aCookie : cookieStore.getCookies()) {
log.info("Setting the following session cookie on response: " + aCookie.toString());
javax.servlet.http.Cookie sessionKeyCookie =
new javax.servlet.http.Cookie(aCookie.getName(), aCookie.getValue());
// little math here -- this isn't super precise due to int casting, but this code is
// really in here for completeness as the expiration date may not be set on the
// session cookie (null)
if (aCookie.getExpiryDate() != null) {
long maxAge = (aCookie.getExpiryDate().getTime() - System.currentTimeMillis()) / 1000;
sessionKeyCookie.setMaxAge((int)maxAge);
}
sessionKeyCookie.setPath(aCookie.getPath());
sessionKeyCookie.setComment(aCookie.getComment());
sessionKeyCookie.setDomain(aCookie.getDomain());
sessionKeyCookie.setSecure(aCookie.isSecure());
sessionKeyCookie.setVersion(aCookie.getVersion());
>               // place the session cookie into the header response using 'addProperty'
response.addProperty(sessionKeyCookie);
}
} finally {
httpResponse.close();
}
} finally {
httpclient.close();
}
}
else {
log.info("My app is already authenticated");
}
}


And that's it!  If you're looking to put SSO in your portal applications, I hope this blog helped you along so you can concentrate on further refining the login process for all those other conditions.  Good luck.

References

Here's a blog link in which I got the cookie expiry date to max age conversion: http://sangupta.com/tech/convert-between-java-servlet-and-apache.html
 

Managing JIRA at Scale White Paper

TAGS: Portal, Software Solutions

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Subscribe to Our Newsletter

Recent Blog Posts