Thursday, September 8, 2011

Using the OIF Business Process Plug-in

There are a few extension points in OIF that allow you to easily extend or tweak the product's behavior. The one you're most likely to use is the Business Process plug-in.

I recently completed a PoC where OIF was the Identity/OpenID Provider and the customer wanted to send a bunch of attributes along to the Service Provider/Relying Party. All that is out of the box behavior. What's not OOTB is that they wanted to prompt the user to fill in any values that weren't in the LDAP directory before the user was sent back to the SP/RP.

The Business Processing plug-in gives you the opportunity to do that.

First up is the plug-in code itself:

package com.oracleateam.feddemo.bpplugin;

// yes, yes, unnecessary. But it makes me feel better.
import com.oracleateam.feddemo.bpplugin.Configuration;

import java.net.URLEncoder;

import java.util.Iterator;
import java.util.List;

import javax.naming.NamingException;

import oracle.security.fed.plugins.bizops.BusinessProcessingConstants;
import oracle.security.fed.plugins.bizops.BusinessProcessingException;
import oracle.security.fed.plugins.bizops.ListenerResult;
import oracle.security.fed.plugins.bizops.OperationData;
import oracle.security.fed.plugins.bizops.OperationListener;
import oracle.security.fed.plugins.bizops.OperationTypes;

public class UserAttributeChecker implements OperationListener {
  Configuration conf        = null;
  LDAPConnection ldconn     = null;

  public UserAttributeChecker() {
    conf = new Configuration();

    try {
      ldconn = new LDAPConnection( conf.getLdapURL(), conf.getLdapDN(), conf.getLdapPW() );
    } catch (NamingException e) {
      System.err.println( "Failed to initialize LDAP connection." );
      System.out.println( "BP Plug-in " + this.getClass().getName() + " will not operate." );
    }
  }


  public ListenerResult process(int operationType,
                                OperationData params) throws BusinessProcessingException {

    ListenerResult result =
      new ListenerResult(BusinessProcessingConstants.STATUS_OK);

    String uid = params.getStringProperty(BusinessProcessingConstants.DATA_STRING_USERID);

    if ( operationType == OperationTypes.BUSINESS_IDP_SSO ) {
      // on an SSO we need to check to see if the user has the required attrs

      try {
        List missingAttrs;

        missingAttrs = ldconn.getMissingAttributes( uid, conf.getRequiredAttributes() );

        if ( missingAttrs.size() > 0 ) {
          System.out.println( "At least one attribute is missing." );

          // Which attrs are we missing again?
          String missingAttrsParam = null;
          Iterator it = missingAttrs.iterator();
          while ( it.hasNext() ) {
            String s = (String) it.next();
            if ( null == missingAttrsParam )
              missingAttrsParam = s;
            else
              missingAttrsParam += "," + s;
          }

          // Build up the URL to redirect the user
          String url = conf.getUiURL() +
                       "?uid=" + uid +
                       "&missing=" + missingAttrsParam;

          result.setStatus( BusinessProcessingConstants.STATUS_REDIRECT );
          result.setRedirectURL(url);
        }

      } catch (NamingException e) {
        System.out.println( "Naming exception caught checking for missing attributes" );
        e.printStackTrace();
      } catch (Exception e) {
        System.out.println( "Exception caught checking for missing attributes" );
        e.printStackTrace();
      }
    }

    return result;
  }
}

What this code does is pretty simple - OIF invokes it on an SSO event, the code looks through the LDAP record for the user and checks for missing attributes. If it finds any it redirects the user to some URL tacking on ?uid= plus the username and &missing= and a list of the missing attributes.

OIF takes that URL and adds on one extra parameter - "refid". We'll need that value later to give control back to OIF so we need to hang on to it when we get it.

Once it's built to install it just follow the instructions in the OIF manual where it talks about the plug-in. Note that I encountered an issue in my environment (NoClassDefFound looking for something from the Apache Commons Codec stuff); if you hit it here's how to fix it.

In the real world the plug-in would probably redirect the user to OIM or some other "real" UI to manage the attributes, and you wouldn't just pass everything along in clear text. But since this is a PoC quick and dirty is the way to go - so I didn't bother with all of that and I just whipped up a JSP.

And here it is:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<%@ page contentType="text/html;charset=ISO-8859-1"%>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
    <title>index</title>
  </head>
  <body>
  <div align="center">
<%
  // coming in we have a couple of parameters
  // "uid"      = username of end user
  // "missing"  = list of attributes that are missing
  // "refid"    = refid needed to pass control back to OIF
  
  // Grab 'em and save 'em
  String uid = request.getParameter("uid");
  String refid = request.getParameter("refid" );  

  String missingStr = request.getParameter("missing");
  String[] missingFields = null;

  if ( null == missingStr ) {
    missingFields = new String[0];
  }
  if ( missingStr.contains(",") )
    missingFields = missingStr.split(",");
  else {
    missingFields = new String[1];
    missingFields[0] = missingStr;
  }
%>
<B>Welcome <%=uid%></B>
<P/>

Before you continue we need a little more information from you.
<P/>
<form method="POST" action="update.jsp">
<input type="hidden" name="uid" value="<%=uid%>"/>
<input type="hidden" name="refid" value="<%=refid%>"/>
<input type="hidden" name="missing" value="<%=missingStr%>"/>
<table border=0>
<%
  for (String field : missingFields)
  {
    out.print( "<tr><td>" );
    out.print( field );
    out.print( "</td><td>" );
    out.print( "<input type=\"text\" name=\"" + field + "\">" );
    out.print( "</td></tr>" );
  }
%>
<tr><td colspan="2"><input type="submit" value=" Submit "/></td>
</table>
</form>
  </div>
  </body>
</html>

And when you hit Submit your browser POSTS to update.jsp:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<%@ page contentType="text/html;charset=ISO-8859-1"%>
<%@ page import="java.net.URLEncoder" %>
<%@ page import="com.oracleateam.feddemo.bpplugin.*" %>
<%@ page import="com.oracleateam.feddemo.bpplugin.LDAPUpdate" %>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
    <title>update</title>
  </head>
  <body>
<div align="center">
<%
  // coming in we should have the same params as before:
  // this really should come from an include
  
  // coming in we have a couple of parameters
  // "uid"      = username of end user
  // "missing"  = list of attributes that are missing
  // "refid"    = refid needed to pass control back to OIF
  
  // Grab 'em and save 'em
  String uid = request.getParameter("uid");
  String refid = request.getParameter("refid" );  

  String missingStr = request.getParameter("missing");
  String[] missingFields = null;

  if ( null == missingStr ) {
    missingFields = new String[0];
  }
  if ( missingStr.contains(",") )
    missingFields = missingStr.split(",");
  else {
    missingFields = new String[1];
    missingFields[0] = missingStr;
  }

  // end of argument parsing
  
  // now update the user record as needed
  Configuration conf = new Configuration();
  LDAPConnection conn = new LDAPConnection( conf.getLdapURL(), conf.getLdapDN(), conf.getLdapPW() );

  // OK, now we need to build the update to LDAP
  LDAPUpdate update = new LDAPUpdate();
  for (String field : missingFields)
  {
    update.addAttribute( field, request.getParameter(field) );
  }
  
  conn.update(uid, update);

  // if we get here we should redirect the user back from whence they came
  String returnURL = conf.getOifURL() + "/user?refid=" + URLEncoder.encode( refid );


%>

Thank you.
<P/>
<a href="<%=returnURL%>">Continue</a>
</div>
  </body>
</html>

update.jsp writes the data back to the record - notice that it doesn't do any sanity checking? That's bad and you'd need to do better! Once it's written the data back it gives the user a link to continue. When we run this the returnURL is going to be "/fed/user?refid=" plus the refid that came in when we first got called.

OIF picks up from there, calls the plug-in again to give it an opportunity to make sure everything is now OK and this time the plug-in returns STATUS_OK so OIF goes ahead and generates the assertion and sends the user along to the SP/RP.

If you ever need this code let me know - I have the whole thing in a JDeveloper project.

2 comments:

  1. hi chris,

    Can you please share the whole code ? Appreciate your help.

    Thanks,
    Sashi

    ReplyDelete
  2. Sashi: if you want the JDev project please send me an email. My email is Christopher.Johnson at Oracle

    ReplyDelete

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