Friday, April 22, 2011

Authenticated Sessions and WebLogic (including clusters)

When you write a J2EE app or use any of the technologies that are built on top of J2EE some aspects of what happens underneath you are one step removed from magic. That's great when you're in the development process, but when you get closer to production you may need pull back the curtain a bit so you can plan properly.

Let's say you have a very simple Servlet that does two things: tells you who you are and counts the number of times you've loaded the servlet. Something like this:

package project1;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class TestServlet extends HttpServlet {

  public void doGet(HttpServletRequest request,
                    HttpServletResponse response) throws ServletException,
                                                         IOException {
    PrintWriter out = response.getWriter();

    out.write("Username: " + request.getRemoteUser() + "\n");

    HttpSession session = request.getSession(true);
    Integer iCount = 0;
    if (!session.isNew()) {
      iCount = (Integer)session.getAttribute("count");
    }
    iCount++;
    session.setAttribute("count", iCount);

    out.write("Count: " + iCount);
  }

}

And let's say that you protect the app with Basic authentication, like so:

<?xml version = '1.0' encoding = 'ISO-8859-1'?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5" xmlns="http://java.sun.com/xml/ns/javaee">
  <servlet>
    <servlet-name>TestServlet</servlet-name>
    <servlet-class>project1.TestServlet</servlet-class>
    <load-on-startup>0</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>TestServlet</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
  <security-constraint>
    <web-resource-collection>
      <web-resource-name>all</web-resource-name>
      <url-pattern>/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
      <role-name>allusers</role-name>
      <role-name>allusers</role-name>
    </auth-constraint>
  </security-constraint>
  <login-config>
    <auth-method>BASIC</auth-method>
    <realm-name>Session Test</realm-name>
  </login-config>
  <security-role>
    <role-name>allusers</role-name>
  </security-role>
</web-app>

When you hit the app via HTTP the server will see that you haven't authenticated and will respond with a 401 and your browser will pop up a Basic auth box.

The next time you make an HTTP request and this time include your credentials the server will:

  1. validate the creds
  2. see that you don't have a session
  3. create a session for the user
  4. squirrel the session away in memory on that server (i.e. in that JVM)
  5. issue a cookie named JSESSIONID
  6. and finally it will it execute the servlet.

Hit reload a few times and your browser sends the JSESSIONID cookie and the counter goes up each time the page loads.

By editing the WebLogic deployment descriptor (weblogic.xml) you can tweak a bunch of those behaviors - you can rename the cookie or even tell the server not issue you a cookie at all. If you use JDeveloper you can use a simple GUI to do that for you:

When you deploy this application to one server everything just works - you know, like magic. But when it's really important that this application be available 24x7x365 you're going to want to have it running on more than one server. In fact you'll probably want to have a couple of servers here and another couple somewhere else in the world in case something goes wrong in the first data center. And you still want the counter in my little test app to increment properly, right?
Well for that you need to deploy a cluster.

There's a whole lot of info out there about WebLogic Server clustering, so I'm not going to go into the details of how the AdminServer, managed servers and clusters work. This is a blog about security stuff in the Fusion stack so you didn't come here to read about managed servers and clusters anyway.

The only reason I'm writing about all of this is because there are some aspects of this that affect security in a way you might not have thought of until now.

Like what?

Did you know that once you have a session established you don't have to present the credentials again? If you present the JSESSIONID cookie WebLogic will go look in the session, figure out that you've already authenticated and it will go ahead and service the request in the context of that user.

In fact that's how HTML Forms-based authentication works - you present your username and password via HTTP POST (to j_security_check) and the WLS Server establishes the session and stores the subject and principals in the session.

It's also how SAML authentication works. If you present a SAML assertion (via IdP-Initiated POST for example) the assertion is validated by WLS and a session gets established; subsequent requests include the session information and the SAML assertion isn't needed. All of the built in Authenticators and Identity Asserters as well as any custom one you might write works exactly the the same way.

In all of these cases once a session is established you just submit the request with the session info (in the JSESSIONID cookie by default) and you can go ahead and invoke any URL in the app as that user.

For the purposes of this demo I setup one AdminServer and a couple of Managed Servers (I called them server1 and server2). I then created a single cluster (called "Cluster-0" because that's the default) composed of those two servers. The result looks like this in the config.xml:

  <server>
    <name>server1</name>
    <listen-port>7031</listen-port>
    <cluster>Cluster-0</cluster>
    <listen-address></listen-address>
    <jta-migratable-target>
      <user-preferred-server>server1</user-preferred-server>
      <cluster>Cluster-0</cluster>
    </jta-migratable-target>
    <server-diagnostic-config>
      <name>server1</name>
      <diagnostic-context-enabled>true</diagnostic-context-enabled>
    </server-diagnostic-config>
  </server>
  <server>
    <name>server2</name>
    <listen-port>7032</listen-port>
    <cluster>Cluster-0</cluster>
    <listen-address></listen-address>
    <jta-migratable-target>
      <user-preferred-server>server2</user-preferred-server>
      <cluster>Cluster-0</cluster>
    </jta-migratable-target>
    <server-diagnostic-config>
      <name>server2</name>
      <diagnostic-context-enabled>true</diagnostic-context-enabled>
    </server-diagnostic-config>
  </server>
  <cluster>
    <name>Cluster-0</name>
    <multicast-address>239.192.0.0</multicast-address>
    <multicast-port>7001</multicast-port>
    <cluster-messaging-mode>unicast</cluster-messaging-mode>
  </cluster>

In the screen shot above you may have noticed that the WebLogic Deployment Descriptor has a setting called Store Type. The default is MEMORY and I changed it in my test to REPLICATE_IF_CLUSTERED. Here's the complete list of possibilities:

If you want to test this yourself here's a little perl script I wrote to prove that out:

#!/usr/bin/perl

$URL1 = "http://10.99.2.125:7031/SessionTest/";
$URL2 = "http://10.99.2.125:7032/SessionTest/";
#$URL2 = $URL1;

$USERNAME = "testuser1";
$PASSWORD = "ABcd1234";

$DELAY = 5;

use LWP;
require HTTP::Request;
require HTTP::Response;

use HTTP::Cookies;
$cookie_jar = HTTP::Cookies->new;

# first request - forcibly include the credentials
$request1 = HTTP::Request->new(GET => $URL1 );
$request1->authorization_basic($USERNAME, $PASSWORD);

print "========================================================\n";
print "|                       REQUEST #1                     |\n";
print "========================================================\n";

print "HTTP Request\n------------\n";
print $request1->as_string;
print "\n";


$ua = LWP::UserAgent->new;
$response1 = $ua->request($request1);

print "HTTP Response\n-------------\n";
print $response1->as_string;

print "========================================================\n";
#exit;

print "Sleeping $DELAY seconds.\n";
sleep $DELAY;
print "\g";

# extract any set-cookie headers and put them in the cookie jar
# note: I do it this way rather than finding the JSESSIONID specifically in case the
# cookie has been renamed.
$cookie_jar->extract_cookies($response1);


#$cookie_jar->scan( dumpCookies );
#print "Cookies: \n";
#print $cookie_jar->as_string();
#
#print "----";

$request2 = HTTP::Request->new(GET => $URL2 );
#$request2->authorization_basic($USERNAME, $PASSWORD);
$cookie_jar->add_cookie_header($request2);

print "HTTP Request\n------------\n";
print $request2->as_string;
print "\n";


$response2 = $ua->request($request2);

print "HTTP Response\n-------------\n";
print $response2->as_string;

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.