Monday, March 23, 2009

GZip Compress Static Files

My PeopleSoft web server contains several JavaScript and CSS files that I embed in PeopleSoft pages using the various techniques described in this blog. Unlike files served by the PeopleSoft application, the web server does not GZip compress these static text files. Until recently, I used the GZip ServletFilter described by Jayson Falkner in his post Two Servlet Filters Every Web Application Should Have. But GZipping every request for the same static file seemed like an unnecessary waste of CPU cycles. I got to thinking...

Could I eliminate the CPU cycles expended by GZipping those static files on every request? Is there a way store static GZipped content and serve the GZipped version to browsers that accept GZip encoding while still making the plain text version available to other browsers?

Here is the solution I cooked up: using the following rules with the tuckey.org UrlRewriteFilter ServletFilter, I can serve a static GZip file to browsers that accept GZip compression while still serving the plain text version to browsers that don't.

The URL Rewrite rules:

<!-- Browsers that support GZip -->
<rule>
<condition type="header" name="Accept-Encoding">.*gzip.*</condition>
<from>^/scripts/([^&lt;&gt;:"/\|?*]+\.js)$</from>
<to type="forward">/compressed/bin/scripts/$1.gz</to>
<set type="response-header" name="Content-Encoding">gzip</set>
</rule>

<rule>
<condition type="header" name="Accept-Encoding">.*gzip.*</condition>
<from>^/css/([^&lt;&gt;:"/\|?*]+\.css)$</from>
<to type="forward">/compressed/bin/css/$1.gz</to>
<set type="response-header" name="Content-Encoding">gzip</set>
</rule>

<!-- Browsers that do NOT support GZip -->
<rule>
<condition type="header" name="Accept-Encoding" operator="notequal">.*gzip.*</condition>
<from>^/scripts/([^&lt;&gt;:"/\|?*]+\.js)$</from>
<to type="forward">/compressed/minified/scripts/$1</to>
</rule>

<rule>
<condition type="header" name="Accept-Encoding" operator="notequal">.*gzip.*</condition>
<from>^/css/([^&lt;&gt;:"/\|?*]+\.css)$</from>
<to type="forward">/compressed/minified/css/$1</to>
</rule>

From this rules file, you can see that I store GZip compressed versions of my JavaScript files in /compressed/bin/scripts. If a requesting browser has the value gzip in the Accept-Encoding header and the request is for a file in the /scripts/ directory, then the first rule tells the UrlRewriteFilter to add the response header Content-Encoding: gzip and serve the version located at /compressed/bin/scripts/. Rule #2 is similar, but for CSS files. Rules 3 and 4 are the inverse of rules 1 and 2, telling the UrlRewriteFilter to serve files from /compressed/minified/scripts/ and /compressed/minified/css/. For example, if an IE 7 browser requests /scripts/jquery-1.3.2.min.js, then the UrlRewriteFilter will add the Content-Encoding: gzip response header and serve /compressed/bin/scripts/jquery-1.3.2.min.js.gz. If an LWP Perl browser posted the same request, then the UrlRewriteFilter would serve the file located at /compressed/minified/scripts/jquery-1.3.2.min.js (assuming the LWP Accept-Encoding header is not set).

Using URL Rewriting in this manner, I don't serve files from my /scripts/ and /css/ directories, and, therefore, don't need these directories on my web server. To avoid confusion caused by developers searching for these files on my web server, I put readme.txt files in each of these directories to explain that URL's pointing at these directories are rewritten to the /compressed directory.

If you use the UrlRewriteFilter, then be sure to use filter-mapping patterns that won't rewrite PeopleSoft URL's. Here are my mappings for the /css/ and /scripts/ URL's

<filter-mapping>
<filter-name>GzipFilter</filter-name>
<url-pattern>/scripts/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>GzipFilter</filter-name>
<url-pattern>/css/*</url-pattern>
</filter-mapping>

You can create GZip versions of your files using the following command:

cat $file_location/scripts/jquery.js | gzip --stdout -9 > $file_location/bin/scripts/jquery.js.gz

And on Windows:

cat %file_location%\scripts\jquery.js | gzip --stdout -9 > %file_location%\bin\scripts\jquery.js.gz

Unfortunately, Windows doesn't have a cat or gzip command. To utilize these commands on Windows, I recommend installing UnxUtils.

Caveat: adding ServletFilters as described in this post may violate your PeopleSoft limited use web server license. Consult your license agreement to ensure compliance. If you use WebLogic, you can leverage the full power of your WebLogic instance by purchasing a WebLogic license from your Oracle rep. For a low cost alternative, you can reverse proxy your PeopleSoft web server with Apache's httpd server and use Apache's URL Rewrite engine (mod_rewrite) instead of the ServletFilter mentioned in this post.

Sunday, March 22, 2009

PeopleSoft as a Password Authentication "Ticket" Server

My post Generating an AuthToken for SwitchUser demonstrates how to acquire and expire PeopleSoft authentication tokens. Using this approach, you could hook any custom application into the PeopleSoft security model, allowing PeopleSoft to manage security for many of your custom enterprise applications. Continuous token (ticket) validation could be implemented through a very simple web service that calls SwitchUser and returns the result. If SwitchUser returns true, then the token is valid.

Really, if you are interested in a centeralized, integrated security solution, then you should speak with your Oracle rep about Oracle's Identity Management Suite.

Generating an AuthToken for SwitchUser

As the name implies, the PeopleCode SwitchUser function allows developers to switch the logged in Operator ID from one user to another. For security reasons, you can only switch identities if you either have another user's operator ID and password or another user's valid authentication token (AuthToken). While I'm sure there are numerous uses for this function, here are my top two:

  • Presenting a sign on Pagelet to a GUEST user
  • Switching the runtime context of Integration Broker PeopleCode

In both of these scenarios, PeopleCode is already running as one user, but you need to switch the runtime context to a different user. For example, if we build a web service that exposes Approval Workflow Engine (AWE) approvals, we must authenticate the caller to ensure the caller has access to a specific approval.

The PeopleCode SwitchUser function takes four parameters. If you use the first two parameters, UserID and Password, then you don't use the third. Likewise, if you use the third parameter, AuthToken, then you don't use the first and second. They are mutually exclusive. The fourth parameter is irrelevant for this discussion. Since the first two parameters are self explanatory, the remainder of this discussion will focus on the third parameter, the AuthToken.

Let's further consider the AWE example. Here is the scenario:

Managers want to approve AWE transactions from their mobile devices (BlackBerry, iPhone, etc). PeopleSoft, however, does not support mobile browsers. One method to enable mobile access is to create a stand alone web application that communicates with PeopleSoft using web services. The initial page for this web application will prompt the user for a PeopleSoft user name and password. The second page of this web application will display information about an approval and provide the manager with action buttons to approve, deny, or push back the transaction. When the manager selects one of the action buttons, the web application will use web services to update the PeopleSoft AWE transaction.

The process flow in this scenario requires the web application to call at least two PeopleSoft web services. Since web services are stateless, we need to authenticate the user on each call. Because these web service calls span multiple request/response cycles, the web application will need to store that authentication information between mobile client requests, making session variables the logical place to store this information. At this point, the question we need to ask is, "What authentication information do we want to store in a session variable?" User name and password? As an alternative, we could store an AuthToken in a session variable and use that as a parameter to SwitchUser. Unlike user names and passwords, authentication tokens can be invalidated and are subject to configurable expiration rules.

How do you generate an AuthToken? Here is the method I use: I create an HTTP connection to my PeopleSoft server's sign on URL and then parse the returned cookies. Here is an example that uses Java and Jakarta Commons HttpClient:

HttpClient client = new HttpClient();

// Posting to the PS login URL
PostMethod post = new PostMethod("http://my.ps-server.com/psp/hrms/?cmd=login");
post.addParameter("userid", username);
post.addParameter("pwd", password);

// expect redirect response code
if(client.executeMethod(post) != 302) {
throw new Exception("Expected a redirect response code.");
}

Cookie[] cookies = client.getState().getCookies();
Cookie pstokenCookie = null;
String pstoken = null;

for (int cookieIdx = 0; cookieIdx < cookies.length; cookieIdx++) {
Cookie cookie = cookies[cookieIdx];
if (cookie.getName().equals("PS_TOKEN") &&
cookie.getDomain().equals(".ps-server.com")) {
pstoken = cookie.getValue();
pstokenCookie = cookie;
}
}

if (pstoken == null) {
throw new Exception("Ack! Didn't find PS_TOKEN cookie");
}

With my AuthToken (PS_TOKEN) identified, I can store it in a session variable, pass it along to Integration Broker, and then invalidate it when the mobile user logs out. Here is some sample code that demonstrates how to invalidate an AuthToken:

HttpClient client = new HttpClient();

client.getState().addCookie(pstokenCookie);

GetMethod get = new GetMethod("http://my.ps-server.com/psp/hrms/?cmd=logout");

int httpResponseCode = client.executeMethod(get);
// TODO: validate response code