Tuesday, March 24, 2015

Open AM Session Events Listener

I found an interesting feature in Open AM. The context was a possible need to extend Open AM's session with an external session store to prevent housing too much information in Open AM's JVM memory. If we built such a service the question was, is it possible to be alerted by Open AM when sessions are created and later purged. A definite possibility was a post authentication processor. But one drawback there is that no notification occurs for sessions terminated due to max session time or max inactivity time being surpassed. But digging through the code exposed a feature that appears to originally be related to policy agents. That feature is still there and it appears that it could be used for such a purpose if done with appropriate precautions.

What you get


The feature is found in the SessionService and allows an account to be granted a specific attribute that, if had, allows such a user to register a URL to be called for all session events. For each such event occurring in that server the server passes to that URL a chunk of XML that looks like the following. I've highlighted some interesting pieces. 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <NotificationSet vers="1.0" svcid="session" notid="41">
  <Notification>
   <![CDATA[<SessionNotification vers="1.0" notid="103">
<Session sid="AQIC5wM2LY4SfcxDdzAB-95DMKd_5f5OV5funAEvj1614qc.*AAJTSQACMDEAAlNLABMxNTEzMzA1MzM0MTAyMTEwNjI2*" stype="user" cid="id=amadmin,ou=user,dc=openam,dc=forgerock,dc=org" cdomain="dc=openam,dc=forgerock,dc=org" maxtime="20" maxidle="10" maxcaching="3" timeidle="129" timeleft="1071" state="destroyed">
 <Property name="CharSet" value="UTF-8"></Property>
 <Property name="UserId" value="amadmin"></Property>
 <Property name="FullLoginURL" value="/sso/UI/Login?service=adminconsoleservice&amp;goto=http%3A%2F%2Fident-local.lds.org%3A8080%2Fsso%2Fbase%2FAMAdminFrame"></Property>
 <Property name="successURL" value="/sso/console"></Property>
 <Property name="cookieSupport" value="true"></Property>
 <Property name="AuthLevel" value="0"></Property>
 <Property name="SessionHandle" value="shandle:AQIC5wM2LY4Sfcw7N1YkJclElxXeuBaVC1_qSIXVCbvTIzA.*AAJTSQACMDEAAlNLABMxNTEzMzA1MzM0MTAyMTEwNjI2*"></Property>
 <Property name="UserToken" value="amadmin"></Property>
 <Property name="loginURL" value="/sso/UI/Login"></Property>
 <Property name="IndexType" value="service"></Property>
 <Property name="Principals" value="amadmin"></Property>
 <Property name="Service" value="ldapService"></Property>
 <Property name="sun.am.UniversalIdentifier" value="id=amadmin,ou=user,dc=openam,dc=forgerock,dc=org"></Property>
 <Property name="amlbcookie" value="01"></Property>
 <Property name="Organization" value="dc=openam,dc=forgerock,dc=org"></Property>
 <Property name="Locale" value="en_US"></Property>
 <Property name="HostName" value="127.0.0.1"></Property>
 <Property name="com-iplanet-am-console-location-dn" value="/"></Property>
 <Property name="AuthType" value="DataStore"></Property>
 <Property name="Host" value="127.0.0.1"></Property>
 <Property name="UserProfile" value="Required"></Property>
 <Property name="AMCtxId" value="d478f1f30cdec1ad01"></Property>
 <Property name="clientType" value="genericHTML"></Property>
 <Property name="authInstant" value="2015-03-20T23:36:05Z"></Property>
 <Property name="Principal" value="id=amadmin,ou=user,dc=openam,dc=forgerock,dc=org"></Property>
</Session>
<Type>3</Type>
<Time>1426894694667</Time>
</SessionNotification>]]>
</Notification>
</NotificationSet>

That last highlighted piece, type, appears to match the static values found in the com.iplanet.dpro.session.SessionEvent class. The value in this chunk says that this session was logged out:

/** Session creation event */
public static final int SESSION_CREATION = 0;

/** Session idle time out event */
public static final int IDLE_TIMEOUT = 1;

/** Session maximum time out event */
public static final int MAX_TIMEOUT = 2;

/** Session logout event */
public static final int LOGOUT = 3;

/** Session reactivation event */
public static final int REACTIVATION = 4;

/** Session destroy event */
public static final int DESTROY = 5;

/** Session Property changed */
public static final int PROPERTY_CHANGED = 6;

/** Session quota exhausted */
public static final int QUOTA_EXHAUSTED = 7;

I've only been able to generate the creation, logout, and destroy events even though I did test a max session and max inactivity expiry. They resulted only in the destroy event.

From the code it appears that the feature uses a thread pool with a wait queue so under load conditions it is conceivable that some events could be dropped.

A Running Environment


To test this out I used my markboydcode/openam-v12-10 docker image. You can too with the following command or use a locally installed Open AM instance. Start a container of that image with the following. Keep in mind that this is a single line in case it gets wrapped in your browser:

docker run -it -p 8081:8081 -p 8082:8082 -p 50389:50389 markboydcode/openam-v12-10

Once started I then ran the ./start.sh script and not long after could hit Open AM at the following URL:

http://localhost.lds.org:8082/openam

(See the /readme.txt file in the root of the container for details on why I must use that hostname.)

A URL to Call


Now to try this feature I need a URL that it can call. I created the following JSP named sessionListener.jsp. There is a carriage return and line feed following the terminating "OK" characters. The feature looks for that returned payload parsing it with a BufferedReader. 

<%@ page import="java.io.ByteArrayInputStream" %>
<%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %><%    response.addHeader("X-Frame-Options", "DENY");    response.addHeader("Cache-Control", "no-cache");    response.addHeader("Cache-Control", "no-store");    response.addHeader("Cache-Control", "must-revalidate");    response.addHeader("Pragma", "no-cache");    java.io.InputStream is = request.getInputStream();    java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();    int bytesRead = -1;    byte[] bytes = new byte[4096];
    while (is != null && (bytesRead = is.read(bytes)) != -1) {        baos.write(bytes, 0, bytesRead);    }    java.lang.System.out.println("----- listener called ----- " + new String(baos.toByteArray()));%>OK


I then placed this in Open AM's web application root so that it was available at:

http://localhost.lds.org:8082/openam/sessionListener.jsp

A Priviledged User and Registered URL


Next I need a user that can register a URL to be called. I used Apache Directory Studio to connect to the Open DJ port. Once in there I selected a user, 'demo' in this case, and added the following attribute and value:

iplanet-am-session-add-session-listener-on-all-sessions : true

Next I had to had to execute the following code within Open AM to register the URL. I used a context listener but this could be done with a post authentication processor or some other extension point. The key is that you need to run this code once Open AM has started up and run it within the Open AM process. In this code I'm using the AuthContext class to authenticate the user against the '/' domain and default ldapService authentication chain. Then I acquire a Session object for that user in order to call the appropriate method on the SessionService class:

try {
    AuthContext ac = new AuthContext("/");
    ac.login(AuthContext.IndexType.SERVICE, "ldapService");
    while (ac.hasMoreRequirements()) {
        Callback[] cbs = ac.getRequirements(true);

        for (int i=0; i<cbs.length; i++) {
            if (cbs[i] instanceof NameCallback) {
                NameCallback nm = (NameCallback) cbs[i];
                nm.setName("demo");
            }
            else if (cbs[i] instanceof PasswordCallback) {
                PasswordCallback pc = (PasswordCallback) cbs[i];
                pc.setPassword("password".toCharArray());
            }
        }
        ac.submitRequirements(cbs);
    }
    if (ac.getStatus() == AuthContext.Status.SUCCESS) {
        System.out.println("--------->>> AUTH'D");
        SSOToken admTk = ac.getSSOToken();
        SessionID sessionId = new SessionID(admTk.getTokenID().toString());
        Session admSession = Session.getSession(sessionId, false);
        SessionService ss = SessionService.getSessionService();
        ss.addSessionListenerOnAllSessions(admSession, "http://localhost.lds.org:8082/sso/sessionListener.jsp");
        System.out.println("---------->>> REG'D");
    }
    else {
        System.out.println("---------->>> NOT AUTH'D");
    }
}
catch(Throwable t) {
    System.out.println("---------->>> ");
    t.printStackTrace();
}

Trying it out


Once this is all done the JSP starts getting called for session events. I tailed catalina.out to see it dumping the payload of such calls into the log. Try logging in. Try logging out. Let your inactivity time out. Let your session max time expire. The JSP was called for all such events.

Caveats


Now to be fair, there is a line in the code warning that session hijacking would be used by this mechanism. With the session token included in the payload, of course it could. So if used you should use https to protect the traffic. And as noted, under load it is conceivable that some events could be lost. If others more familiar with the code know of other limitations or concerns I'd love to hear about them.

Enjoy.
 
 
 


1 comment:

  1. Looks like you went through a lot of trouble to implement something similar to Session Timeout Handlers. The only downside is that those aren't invoked for logout events, but that you could easily overcome by implementing a post authenticaiton processing plugin as well. Then you just need to have both of these extensions to call the same underlying handler, and you saved yourself from a whole lot of trouble really.
    You could also have registered session/ssotokenlisteners to each session individually from a PAP for example to prevent the need yet again from performing HTTP calls against the same server and essentially occupying a request processing thread from the web container.

    ReplyDelete