Sunday, 27 May, 2012

jquery - the answer you expect

jQuery is the most intuitive library - I haven't yet investigated the other options, it always does what I think it should.

Though I occasionally need to look-up details... so for my own reference, here are a couple of functions that are in all my layout/headers. Focus first visible, enabled, non-submit input, catch enter key submits and clear visible form values:



$(document).ready(function() {

// prevent enter from submitting the form without validation
$('.input').keypress(function(e) {
if (e.which == 13) {
jQuery(this).blur();
jQuery('input[id$=submit]').focus().click();
}
});

// focus first form input
$(":input:not(:submit):visible:enabled:first").focus();

});


function clearForm() {
$(":input:visible:not(:submit):not(:button)").val('');
$("select option[value='']").attr('selected', 'selected');
}

Sunday, 20 May, 2012

OAM 11g custom authentication plugin

The oracle documentation for custom authentication plugins could use a little more detail - it's easy to get the deployment wrong. Some notes:

Reuse java classname everywhere; the filename of the jar, to the xml filename in the root directory of the jar:

[PluginName].jar / [PluginName].xml / <Plugin name="[PluginName]" type="Authentication"> / com.companyname.[PluginName].java / etc.

Your project structure should look like this.


(If you upload and get "class not found", double check!)

The docs tell us we need four jars from the OAM app-inf folder (felix, felix-service, extensibility_lifecycle, oam-plugin). Actually, even if you compile the sample code from the docs site, you also need utilities.jar and identitystore.jar.

[DOMAIN]/servers/[SERVERNAME]/tmp/_WL_user/oam_server/[RANDOM]/APP-INF/lib

No special build required, "export jar" in eclipse should be OK.

Navigate to the OAM console for authentication plugins and select "Import". The steps to activation are :

  1. Upload (validates jar/xml contents)
  2. Distribute (sends info to OAM servers - they don't need to be running)
  3. Activate

If "Distribute" doesn't appear to do anything, possibly it's the interface not refreshing - wait for it, login again, or restart the AdminServer and see if it's updated.

Download Example CustomAuthPlugIn - contains full eclipse java project and correct metadata (link becomes inactive if not d/l'd in past month, post a comment if you can't get the file).

Sunday, 1 April, 2012

JSF validator - multiple fields / message redirect

Every so often I am pleasantly surprised when code/frameworks work as I wanted them to, without looking it up first.

Starting with the idea of validating multiple fields (password & confirm), BalusC has it covered in a well-aged article.

Taking it one step further; I wanted the validator to put the error on another field based on two selections.

In this case, the user needed to enter their email if either one of two fields was set to a certain value, furthermore at least one of two needed to have a value.

JSF validation is somewhat black & white for fields that are "required" - if a field isn't required, and no value entered, validators don't activate.

Given that limitation, I pinned the validator of a field was *was required*, username. The following example is a simplified version of implementation.

Username validation based on the "binding" from three extra fields: two roles fields and email; at least one role must be selected and if the role is admin, email must be supplied.

So here's the JSF snippet, note the "attribute", "binding" and "immediate":

<h:outputLabel for="username" value="#{text['user.username']}" />
    <h:inputText value="#{userForm.user.username}" id="username"
      styleClass="text" required="true">
      <f:validateLength minimum="8" maximum="16" />
      <f:validator
       validatorId="net.sbeynon.example.webapp.util.UserLoginValidator" />
      <f:validator
       validatorId="net.sbeynon.example.webapp.util.RoleSelectValidator" />
      <f:attribute name="adminRole" value="#{adminRole.value}" />
      <f:attribute name="appRole" value="#{appRole.value}" />
      <f:attribute name="email" value="#{email.value}" />
    </h:inputText>
    <h:message for="username" styleClass="errorMessage" />

    <h:outputLabel for="appRole" value="#{text['user.role']}" />
    <h:selectOneMenu id="appRole" value="#{userForm.appRole}"
      styleClass="select" immediate="true" binding="#{appRole}">
      <f:selectItems value="#{userForm.applicationRoles}" />
    </h:selectOneMenu>
    <h:message for="appRole" styleClass="errorMessage" />

    <h:outputLabel for="adminRole" value="#{text['admin.role']}" />
    <h:selectOneMenu id="adminRole" value="#{userForm.adminRole}"
      styleClass="select" immediate="true" binding="#{adminRole}">
      <f:selectItems value="#{userForm.adminRoles}" />
    </h:selectOneMenu>
    <h:message for="adminRole" styleClass="errorMessage" />


Above, each of the values we need is binding/immediate, such that the value can be passed as an attribute to the username RoleSelectValidator.

This validation involves a trick; throwing an empty validator exception, but adding a faces message to the correct field manually.

@FacesValidator("net.sbeynon.example.webapp.util.RoleSelectValidator")
public class RoleSelectValidator implements Validator {
  protected static final Logger log = LoggerFactory.getLogger(RoleSelectValidator.class);

  @Override
  public void validate(FacesContext context, UIComponent c, Object val) throws ValidatorException {
    // grab the f:attribute values
    String adminRole = (String) c.getAttributes().get("adminRole");
    String appRole = (String) c.getAttributes().get("appRole");
    String email = (String) c.getAttributes().get("email");

    log.info("Validating role/s : admin:" + adminRole + " & app:" + appRole);
    // Check if they both are filled in.
    if ((adminRole == null || adminRole.isEmpty()) && (appRole == null || appRole.isEmpty())) {
      // add the message to the correct field
      FacesContext.getCurrentInstance().addMessage("userForm:appRole",
          new FacesMessage("Please select at least one role."));

      // stop action continuing with an empty validator exception
      throw new ValidatorException(new FacesMessage(""));
    }

    log.debug("check email " + email + " for specific roles: " + appRole);
    if (!StringUtils.isEmpty(adminRole)) {

      if (StringUtils.isEmpty(email)) {
        FacesMessage msg = new FacesMessage();
        msg.setSeverity(FacesMessage.SEVERITY_ERROR);
        msg.setDetail("Please provide an email address (only optional for non-admin user roles.)");
        FacesContext.getCurrentInstance().addMessage("userForm:email", msg);

        // stop action continuing with an empty validator exception
        throw new ValidatorException(new FacesMessage(""));
      }
    }
  }
}

Monday, 26 March, 2012

Making OHS talk to tomcat with Proxy Balancer


My scenario is webserver OHS (Oracle HTTP build on top of Apache2.2) in the DMZ, SSL-only environment, reverse proxying to a backend tomcat (fire-walled, non-SSL).

Until getting to production, standard apache ReverseProxy and Proxy balancer directives were doing the trick. (solaris-sparc-64bit has no JK binary, and seems it's a massive headache to build from the google results).

Then in prod, suddenly tomcat URLs were converted to "localhost" every time a response.sendRedirect 302 was initiated from the application (standard get/post browsing was fine).

So the solution was fooling about with tomcat server.conf

This is the front end OHS conf:
ProxyRequests off
ProxyPreserveHost On

<proxy balancer://tomcat>
BalancerMember http://10.1.10.11:8080 timeout=600 ## add more members here.
ProxySet stickysession=node1
</Proxy>

ProxyPass        /webapp balancer://tomcat/webapp 
ProxyPassReverse /webapp balancer://tomcat/webapp 

And here is the "proxy-enabled" tomcat server conf:
TOMCAT_HOME/conf/server.xhtml

Adding the scheme, proxyName and proxyPort finally did the trick.

<connector port="8080" protocol="HTTP/1.1"
               scheme="https" proxyName="www.secure.frontend.ca" proxyPort="443" 
               connectionTimeout="20000"
               redirectPort="8443" />

Sunday, 15 January, 2012

Fun with OAM 11g SDK

OAM 11g was a step backward from 10g, but slowly - as of .5 - it's becoming usable again. Bringing back custom authentication plugins and schemes was vital, and then there's the topic of this post, the indispensable; AccessServerSDK.

I am still not pleased - Certain things are still missing, like the "originally requested URL" - making it impossible to do something like, allow a user to self-register and return to their original destination. I read that Oracle are working on the request, but who knows how long it will take with all the legitimate bugs out there.

OFM OAM SDK 11g


The 11g SDK is a simple Java API. Download and extract "ofm_oam_sdk_generic_11.1.1.5.0_disk1_1of1.zip" - no installer necessary. Sort of.

To get started, create a 10g gate in the OAM console, and copy the generated ObAccessClient.xml to your "AccessServerSDK" home folder within the subfolders: oblix\lib. For example, on my workstation, I created "C:\Oracle\asdk\oblix\lib".

From that point, you can copy the AccessServerSDK directory ("C:\Oracle\asdk") to setup multiple clients on developer or test machines, no worries, as long as the path is the same for everyone.

Add your oam-sdk jar to the project classpath - that's it. See Oracle API for a basic example or keep reading.

The fun part is using the SDK to authenticate a user for a valid SSO session they can take with them. I will get to that in a second, let's show the basic concept first.

Basic OAM SDK client class


Let's start with logging in with a basic OAM SDK client. To run this test, update: configLocation, login, password, resource for your environment.

You can use any http resource already protected in OAM, or create a new one with any FORM type Authentication Scheme.

The resource format is : "//HostnameFromHostIdentifier:80/ResourcePath"

In my environment, I need the code to work on Windows or Unix, so there are two configLocation values - use is determined by the java System Property for OS.

import java.util.Hashtable;

import oracle.security.am.asdk.AccessClient;
import oracle.security.am.asdk.AccessException;
import oracle.security.am.asdk.AuthenticationScheme;
import oracle.security.am.asdk.ResourceRequest;
import oracle.security.am.asdk.UserSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * To generate the Access SDK log, you must provide a logging configuration file
 * when you start the application. e.g.
* java -Djava.util.logging.config.file=JRE_DIRECTORY/lib/logging.properties
 */
public class Simple10gClient {
  protected static final Logger log = LoggerFactory.getLogger(Simple10gClient.class);

  // assume all resources are http GETs
  public static final String res_type = "http";
  public static final String ms_method = "GET";

  // Using standard location for SDK; will choose based on OS.
  public static String configLocation = "/Oracle/Middleware/asdk";
  public static String winConfigLocation = "C:/Oracle/asdk";
  private static AccessClient ac;

  private Simple10gClient() {}

  // simple test 
  public static void main(String argv[]) {
    String anonResource = "//AccessServerSDK_HostID:443/ResourceProtectedWithNoPasswordScheme";
    String login = "testuser1";
    String password = null;

    try {
      String sId = Simple10gClient.authenticate(anonResource, login, password);

      Simple10gClient.isLoggedIn(sId);

      Simple10gClient.shutdown();
      
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  @SuppressWarnings("rawtypes")
  private static void setupAccessClient() {
    try {
      String os = System.getProperty("os.name").toLowerCase();
      if (os.indexOf("win") > -1) {
        configLocation = winConfigLocation;
      }
      // "C:/Oracle/asdk/oblix/lib/ObAccessClient.xml" must exist/be valid:
      log.info("getAccessClient() config:" + configLocation );
      ac = AccessClient.createDefaultInstance(configLocation , AccessClient.CompatibilityMode.OAM_10G);
      log.info("Nap:" + AccessClient.getNAPVersion() + " SDK:" + AccessClient.getSDKVersion());
      Hashtable diagnostic = ac.getServerDiagnosticInfo();
      for (Object key : diagnostic.keySet()) {
        log.info("Server Diagnostic " + key + "==" + diagnostic.get(key));
      }
      diagnostic = ac.getClientDiagnosticInfo();
      for (Object key : diagnostic.keySet()) {
        log.info("Client Diagnostic " + key + "==" + diagnostic.get(key));
      }

    } catch (AccessException ae) {
      if (ae.getMessage().indexOf("OAMAGENT-02055") > -1) {
        log.error("AccessServerSDK - is this agent webgate configured? " + ae);
      } else {
        log.error("Access Exception: " + ae.getMessage(), ae);
      }
    }
  }

  public static String authenticate(String resource, String login, String pw) {
    log.debug("authenticate() user=" + login + " res:" + res_type + " " + resource);
    try {
      if (ac == null || !ac.isInitialized()) {
        setupAccessClient();
      }
      ResourceRequest rrq = new ResourceRequest(res_type, resource, ms_method);
      if (rrq.isProtected()) {
        log.info("Resource is protected: " + resource);
        AuthenticationScheme authnScheme = new AuthenticationScheme(rrq);
        if (authnScheme.isForm()) {
          log.info("Form Authentication Scheme: " + authnScheme.getName());
          Hashtable creds = new Hashtable();
          // to know credential key names, either you write the AuthN scheme yourself
          // or go with the Oracle example.
          creds.put("userid", login);
          if (pw != null && pw.length() > 0)
            creds.put("password", pw);
          UserSession session = new UserSession(rrq, creds);
          if (session.getStatus() == UserSession.LOGGEDIN) {
            if (session.isAuthorized(rrq)) {
              log.info("User is logged in and authorized, level " + session.getLevel());
            } else {
              log.info("User is logged in but NOT authorized");
            }
            log.info("User sessionToken:" + session.getSessionToken());
            return session.getSessionToken();
          } else {
            log.warn("User is NOT logged in");
            return null;
          }
        } else {
          log.warn("non-Form Authentication Scheme.");
        }
      } else {
        log.warn("Resource is NOT protected.");
      }
    } catch (AccessException ae) {
      log.error("authenticate() Access Exception: " + ae.getMessage());
    }
    return null;
  }

  public static void logout(String sessionId) {
    try {
      if (ac == null || !ac.isInitialized()) {
        setupAccessClient();
      }
      oracle.security.am.asdk.UserSession session = new UserSession(ac, sessionId);
      log.info("is logout required? " + session);
      if (session.getStatus() == 1)
        session.logoff();
    } catch (Exception ae) {
      log.error("logout() Access Exception: " + ae.getMessage(), ae);
    }
  }

  /**
   * @param sessionId
   */
  public static boolean isLoggedIn(String sessionId) {
    // 0 for AWAITINGLOGIN
    // 1 for LOGGEDIN
    // 2 for LOGGEDOUT
    // 3 for LOGINFAILED
    // 4 for EXPIRED
    try {
      if (ac == null || !ac.isInitialized()) {
        setupAccessClient();
      }
      oracle.security.am.asdk.UserSession session = new UserSession(ac, sessionId);
      log.info("isLoggedIn  startTime:" + session.getStartTime() + " status:" + session.getStatus());
      java.util.Date since = new java.util.Date(session.getStartTime());
      log.debug("since? " + since);
      return session.getStatus() == 1;
    } catch (Exception ae) {
      log.error("isLoggedIn() Access Exception: " + ae.getMessage(), ae);
    }
    return false;
  }

  public static void shutdown() {
    try {
      if (ac != null)
        ac.shutdown();
    } catch (Exception e) {
      log.warn("AccessServerSDK shutdown error " + e);
    }
  }
}

Right click and Run in eclipse, and you should see something like:

- authenticate() user=testuser1 resource:http //AccessServerSDK_HostID:443/ResourceProtectedWithNoPasswordScheme
- getAccessClient() config:C:/Oracle/asdk
- Nap:3 SDK:OAMAccessSDK_Ver_11.1.1.5
- Server Diagnostic oamserverhostname5575=={}
- Client Diagnostic oamserverhostname5575=={host=oamserverhostname, port=5575, priority=1, createtime=1326647958849}
- Resource is protected.
- Form Authentication Scheme.
- User is logged in and authorized for the request at level 3
- User sessionToken:LEHZ+zmSIZiamCTmHNQh9S+4JH0R8AvJ0eVwLPkUwmZArWb9qOg
KHYtEKrNTYHmEjt/UcqfFQ3Bq+mWud7APd0lqZlf31IigqrUAKJn7wRbRHanIpIQ1ygj64s
FjCoP6CwSTvSGeLY6gv49okJCM/2QivVqWb2Ce1Xwru89Vs/ZaE7YVXfjS8DnWwPBqYR8kQ
ONN348NI2cRMSMbryhG4neWah7rSFtJDH/gwAU55xwpE4SCcfTsXor8Qm7Ag==

Using the OAM SDK to authenticate a web client


My next step is wildly insecure, but a good shortcut for this example. Using the session ID generated with the SDK login, construct a valid SSO cookie and send the user on their merry authenticated way.

First, in the OAM console: Create a resource for your AccessServerSDK host, path "ResourceProtectedWithNoPasswordScheme" and add a "No Password Authentication Scheme" to it. I created my own AuthN scheme, setting the level to 3 - because I needed to match the authentication level of the forwarding online resource.

Create a LogMeIn servlet class taking a request parameter for login ID, and log the user in with a request parameter (see what I mean about being wildly insecure!):

String login = request.getParameter("login");
  String anonResource = "//AccessServerSDK_HostID:443/ResourceProtectedWithNoPasswordScheme";

      try {
        String sessId = Simple10gClient.authenticate(anonResource, login, null);
        log.info("Anon authenticate sessId:" + sessId);

        // using the Session ID, create a ObSSOCookie and give it to the user
        if (sessId != null) {
          Cookie obCookie = new Cookie("ObSSOCookie", sessId);
          obCookie.setDomain(".domain.com");
          // obCookie.setHttpOnly(true); -- not sure why Weblogic 10.3.5 wasn't able to find this method
          obCookie.setPath("/");
          response.addCookie(obCookie);

          // now send a response.redirect to a level 3 protected resource, user is logged in

        }
      } catch (Exception e) {
        response.getOutputStream().print("Exception " + e);
        log.error("logmein failed", e);
      }


Deloying on the OIM/OAM WLS Domain


There was a bit of a gotcha using this sdk jar on the same domain as OIM/OAM - class/jar conflicts. Easily solved by adding a weblogic.xml to the war WEB-INF, requesting that weblogic look at the web libraries only.

  <wls:container-descriptor>
    <wls:prefer-application-packages>
 <!-- add package names from the Oracle Access Server SDK -->
 <wls:package-name>com.oblix.*</wls:package-name>
 <wls:package-name>oracle.security.am.*</wls:package-name>
    </wls:prefer-application-packages>
  </wls:container-descriptor>

The end result of this is that you now can authenticate with standard username / password, check if the value of an ObSSOCookie is a valid session token, and generate for the end-user a valid SSO token to continue their with session on other OAM protected resources.



Friday, 30 December, 2011

Spring Security with SSO Headers - integrating with OAM WebGate

How to integrate java web applications with SSO? Usually a simple J2EE filter looking for the correct header name can suffice, however, if you want to take advantage of Spring Security you need to set it up with a "PreAuthentication" filter. This states that spring is not performing authentication, it's already done.

This solution may be overkill, but it provides more control/log info, and the ability to test outside of the SSO environment without implementing alternate authentication providers (although it would also be possible to have a form/database authentication fallback as well).

My environment: J2ee (any app server) fronted by a Oracle Access WebGate protected web server.

Spring Security 2.5+ (tested with 3.0.5)

What I need: Convert Request Headers OAM_REMOTE_USER and ROLES to map to an authenticated principal.

Making it usable: Allow a fallback security configuration for local testing.

Step 1. PreAuthentication Config


There are two custom beans in this file "HeaderAuthenticationFilter" and "HeaderAuthenticationDetails" and three configuration properties:

security.principal.header.name=OAM_REMOTE_USER
security.roles.header.name=ROLES
security.test.principal=no_header_in_test_mode

applicationContext-security.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
  xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"
  default-lazy-init="true">

  <bean
    class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />

  <!-- this bean name must match the filter name defined in security.xml below -->
  <bean id="ssoHeaderFilter"
    class="com.sharnibey.sample.security.HeaderAuthenticationFilter">
    <property name="principalRequestHeader" value="${security.principal.header.name}" />
    
    <!-- fall back to other authentication providers is OAM SSO is not there -->
    <property name="exceptionIfHeaderMissing" value="false" />
    <!-- hard code a testUserId for local tests -->
    <property name="testUserId" value="${security.test.principal}" />

    <property name="authenticationManager" ref="authenticationManager" />

    <property name="authenticationDetailsSource">
      <bean class="com.sharnibey.sample.security.HeaderAuthenticationDetails">
        <!-- look for the request header set by the webgate and map to local 
          roles -->
        <property name="roleHeaderName" value="${security.roles.header.name}" />
        <property name="userRoles2GrantedAuthoritiesMapper">
          <bean
            class="org.springframework.security.core.authority.mapping.SimpleAttributes2GrantedAuthoritiesMapper">
            <property name="convertAttributeToUpperCase" value="true" />
          </bean>
        </property>
        <!-- setup a testing role if not deployed with a webgate - this only 
          applies if ENV_NAME != uat/prod -->
        <property name="testingRoles">
          <set>
            <value>USER</value>
          </set>
        </property>        
        <!-- all available roles for this application -->
        <property name="allRoles">
          <set>
            <value>USER</value>
            <value>ADMIN</value>
          </set>
        </property>
      </bean>
    </property>
  </bean>
  <bean id="preauthAuthProvider"
    class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
    <property name="preAuthenticatedUserDetailsService" ref="preAuthenticatedUserDetailsService" />
  </bean>
  <!-- magically map the user header to a valid user object -->
  <bean id="preAuthenticatedUserDetailsService"
    class="org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesUserDetailsService" />

  <bean id="securityContextHolderAwareRequestFilter"
    class="org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter" />
</beans>



PreAuthentication Code



public class HeaderAuthenticationFilter 
    extends AbstractPreAuthenticatedProcessingFilter {
  protected final Logger log = LoggerFactory.getLogger(HeaderAuthenticationFilter.class);
  private String principalRequestHeader = "OAM_REMOTE_USER";
  /**
   * Configure a value in the applicationContext-security for local tests.
   */
  private String testUserId = null;
  /**
   * Configure whether a missing SSO header is an exception.
   */
  private boolean exceptionIfHeaderMissing = false;

  /**
   * Read and return header named by principalRequestHeader from Request
   * 
   * @throws PreAuthenticatedCredentialsNotFoundException
   *             if the header is missing and
   *             exceptionIfHeaderMissing is set to true.
   */
  protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
    String principal = request.getHeader(principalRequestHeader);

    if (principal == null) {
      if (exceptionIfHeaderMissing) {
        throw new PreAuthenticatedCredentialsNotFoundException(principalRequestHeader
            + " header not found in request.");
      } if (StringUtils.isNotBlank(testUserId)) {
          log.warn("spring configuration has a test user id " + testUserId);
          principal = testUserId;
      } else if (request.getSession().getAttribute("session_user") != null) {
// A bit of a hack for testers - allow the principal to be 
// obtained by session. Must be set by a page with no security filters enabled.
// should remove for production.
        principal = (String) request.getSession().getAttribute("session_user");
      }
    }
    // also set it into the session, sometimes that's easier for jsp/faces
    // to get at..
    request.getSession().setAttribute("session_user", principal);
    return principal;
  }

  /**
   * Credentials aren't applicable here for OAM WebGate SSO.
   */
  protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
    return "password_not_applicable";
  }

  public void setPrincipalRequestHeader(String principalRequestHeader) {
    Assert.hasText(principalRequestHeader, "principalRequestHeader must not be empty or null");
    this.principalRequestHeader = principalRequestHeader;
  }

  public void setTestUserId(String testId) {
    if (StringUtils.isNotBlank(testId)) {
      this.testUserId = testId;
    }
  }

  /**
   * Exception if the principal header is missing. Default false
   * @param exceptionIfHeaderMissing
   */
  public void setExceptionIfHeaderMissing(boolean exceptionIfHeaderMissing) {
    this.exceptionIfHeaderMissing = exceptionIfHeaderMissing;
  }

  public void setAuthenticationDetailsSource(AuthenticationDetailsSource source) {
    log.info("testing authenticationDetailsSource set " + source);
    super.setAuthenticationDetailsSource(source);
  }
}

public class HeaderAuthenticationDetails extends AuthenticationDetailsSourceImpl {
  protected final Logger log = LoggerFactory.getLogger(HeaderAuthenticationDetails.class);

  /**
   * Can be setup in applicationContext-security if the ROLES header value is
   * not found.
   */
  private Set testingRoles = new HashSet();

  /**
   * Security principal will only contain roles from "allRoles" - letting us
   * cut down the irrelevant values setup by the webgate SSO header.
   */
  protected Set allRoles = new HashSet();

  /**
   * setup in applicationContext-security
   */
  private String roleHeaderName = "ROLES";

  protected Attributes2GrantedAuthoritiesMapper grantedAuthoritiesMapper 
    = new SimpleAttributes2GrantedAuthoritiesMapper();

  public HeaderAuthenticationDetails() {
    super.setClazz(PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails.class);
  }

  /**
   * Build the authentication details object. If the specified authentication
   * details class implements {@link MutableGrantedAuthoritiesContainer}, a
   * list of pre-authenticated Granted Authorities will be set based on the
   * roles for the current user.
   */
  public Object buildDetails(Object context) {
    Object result = super.buildDetails(context);
    List userGas = new ArrayList();
    if (result instanceof MutableGrantedAuthoritiesContainer) {
      Collection userRoles = getUserRoles(context, allRoles);
      userGas = grantedAuthoritiesMapper.getGrantedAuthorities(userRoles);
      ((MutableGrantedAuthoritiesContainer) result).setGrantedAuthorities(userGas);
    }
    return result;
  }

  /**
   * Allows the roles of the current user to be determined from the context
   * object
   * 
   * @param context
   *            the context object (HttpRequest, PortletRequest etc)
   * @param mappableRoles
   *            the possible roles determined by the
   *            MappableAttributesRetriever
   * @return Collection subset of mappable roles current user has.
   */
  protected Collection getUserRoles(Object context, Set mappableRoles) {
    ArrayList requestRoles = new ArrayList();
    if (((HttpServletRequest) context).getHeader(roleHeaderName) != null) {
      String[] roles = ((HttpServletRequest) context).getHeader(roleHeaderName).split(",");
      for (int i = 0; i < roles.length; i++) {
        if (mappableRoles.contains(roles[i])) {
          requestRoles.add(roles[i]);
        }
      }
    } else if ( testingRoles != null) {
      log.warn("Failed to retrieve Roles from Header, for debug purposes set to testingRole");
      requestRoles.addAll(testingRoles);
    } else {
      log.warn("Failed to retrieve Roles from Header, setup as 'user' role.");
      requestRoles.add("USER");
    }
    // add them to the session for convenience
    ((HttpServletRequest) context).getSession().setAttribute("ROLES", requestRoles);
    return requestRoles;
  }

  /**
   * @param mapper
   *            The Attributes2GrantedAuthoritiesMapper to use
   */
  public void setUserRoles2GrantedAuthoritiesMapper(Attributes2GrantedAuthoritiesMapper mapper) {
    grantedAuthoritiesMapper = mapper;
  }

  /**
   * All available roles for this application
   * 
   * @param allRoles
   */
  public void setAllRoles(Set allRoles) {
    this.allRoles = allRoles;
  }
  /**
   * @param roleHeaderName
   */
  public void setRoleHeaderName(String roleHeaderName) {
    this.roleHeaderName = roleHeaderName;
  }
  /**
   * @param testingRole
   */
  public void setTestingRoles(Set testingRole) {
    this.testingRoles = testingRole;
  }
}

web.xml updates


These snippets will look familiar if you've ever used spring security; define the filter, map to all resources. Spring contextConfiguration locations should have your resource properties loaded first, then security config, and everything else.

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value> 
 classpath:/ctx/applicationContext-resources.xml 
 classpath:/ctx/applicationContext-security.xml 
 /WEB-INF/security.xml
 /WEB-INF/applicationContext*.xml
    </param-value>
  </context-param>

  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

Final Step - Setup Spring Security


security.xml is application specific, but my sample application is based on appfuse - JSF version - so it should cover most example uses.

The intercept-url element defines the roles or authentication states that are required to access a URL path. Roles should be comma-delimited.

To restrict pages by user type instead of user role the following values can be used:
IS_AUTHENTICATED_ANONYMOUSLY - Allow access to any user.
IS_AUTHENTICATED_REMEMBERED - Allow access to logged-in users or users with a "remember me" cookie.
IS_AUTHENTICATED_FULLY - Allow access to logged-in users.

To remove all Spring Security processing from a page use the filters="none" attribute.

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
              http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">

 <http auto-config="true" lowercase-comparisons="false">
  <intercept-url pattern="/images/**" filters="none" />
  <intercept-url pattern="/styles/**" filters="none" />
  <intercept-url pattern="/scripts/**" filters="none" />
  <intercept-url pattern="/javax.faces.resource/**"
   filters="none" />

  <!-- direct xhtml access disallowed -->
  <intercept-url pattern="/**/*.xhtml" access="ROLE_NOBODY" />

  <!-- local authentication is unused, but this is how it's configured -->
  <intercept-url pattern="/j_security*" access="IS_AUTHENTICATED_ANONYMOUSLY" />
  <intercept-url pattern="/login*" access="IS_AUTHENTICATED_ANONYMOUSLY" />

  <intercept-url pattern="/a4j.res/**"
   access="ROLE_ANONYMOUS,ROLE_ADMIN,ROLE_USER" />
  <intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
  <intercept-url pattern="/user/**" access="ROLE_ADMIN,ROLE_USER" />

  <!-- show request headers and session variables for any user -->
  <intercept-url pattern="/env.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />

  <intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />

  <!-- matches the bean name for HeaderAuthenticationFilter class above -->
  <custom-filter position="PRE_AUTH_FILTER" ref="ssoHeaderFilter" />

  <form-login login-page="/login" authentication-failure-url="/login?error=true"
   login-processing-url="/j_security_check" always-use-default-target="true"
   default-target-url="/" />
 </http>

 <authentication-manager alias="authenticationManager">
  <authentication-provider ref="preauthAuthProvider" />

  <!-- this is an example of alternate user authentication providers, although 
   we only have the PRE_AUTH_FILTER defined above, so it isn't used. -->
  <authentication-provider>
   <user-service>
    <user authorities="ROLE_USER" name="guest" password="guest" />
   </user-service>
  </authentication-provider>
 </authentication-manager>
</beans:beans>


If you read this far, and you want a Ready-To-Go example of how all this fits together, leave a comment and I will upload a full-source war to a temporary share site. Please don't put your email in the comments.

OIM Recon LDAP - adding fields


First, grab your metadata from MDS:

$MW_HOME/Oracle_IDM[1,2]/server/bin/

Setup weblogic.properties


wls_servername=oim_server1
application_name=oim
metadata_from_loca=/data/temp
metadata_to_loca=/data/temp

metadata_files=/db/LDAPUser,/db/RA_LDAPUSER.xml,/metadata/iam-features-ldap-sync/LDAPUser.xml


weblogicExportMetadata.sh/bat (connect as weblogic to the AdminServer e.g. t3://localhost:7001)

OIM Default/Existing Field Example


TO show you the fields that are required for update, here's a generic example to describe the LDAP/recon process for the OIM Role (user type) field to the LDAP attribute employeetype:

metadata/db/LDAPUser: Attribute appears twice, under "reconFields" and reconToOIMMappings:


reconFields

<reconAttr>
<oimFormDeescriptiveName>Role</oimFormDescriptiveName> (Attribute Name from OIM user attribute config)
<reconFieldName xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">employeetype</reconFieldName>
<reconColName>RECON_USR_EMP_TYPE</reconColName>
<emDataType>string</emDataType>
<formFieldType/>
<targetattr keyfield="false" encrypted="false" required="false" type="String" name="usr_emp_type"/> (name of field in USR table)
</reconAttr>

reconToOIMMappings

<reconAttr>
<oimFormDescriptiveName>Role</oimFormDescriptiveName> (Attribute Name from OIM user attribute config)
<reconFieldName xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">employeetype</reconFieldName> (LDAPUser.xml fieldname)
<reconColName>RECON_USR_EMP_TYPE</reconColName>
<emDataType>string</emDataType>
<formFieldType/>
<targetattr keyfield="false" encrypted="false" required="false" type="String" name="usr_emp_type"> (name of field in USR table)
<Transformation name="OneToOne">
<Parameter name="employeetype" fieldname="employeetype"/> (name of field in OVD)
</Transformation>
</targetattr>
</reconAttr>

/metadata/dbRA_LDAPUSer.xml: Attribute and recon field defined, under "entity-attributes" and "target-fields":


entity-attributes

<attribute name="Role"> (Attribute Name from OIM user attribute config)
<type>string</type>
<required>false</required>
<searchable>true</searchable>
<MLS>false</MLS>
<attribute-group>Basic</attribute-group>
<metadata-attachment/>
</attribute>
<attribute-map>
<entity-attribute>Role</entity-attribute>
<target-field>RECON_USR_EMP_TYPE</target-field>
</attribute-map>

target-fields

<field name="RECON_USR_EMP_TYPE">
<type>string</type>
<required>false</required>
</field>

/metadata/iam-features-ldap-sync/LDAPUser.xml: Mapping is defined, under "entity-attributes", "target-fields" and "attribute-maps"


<attribute name="Role">
<type>string</type>
<required>false</required>
<attribute-group>Basic</attribute-group>
<searchable>true</searchable>
</attribute>
<field name="employeeType">
<type>string</type>
<required>false</required>
</field>
<attribute-map>
<entity-attribute>Role</entity-attribute>
<target-field>employeeType</target-field>
</attribute-map>

Now a real example - Adding Lock Fields


The problem with a fully integrated OIM/OAM/Ldap Sync/OVD 11g is that it's broken.

Users lock themselves through OAM webgate login, correctly setting the oblockouttime - but OIM knows nothing about it. I have no idea how to get a unix-style epoch time (oblockouttime) through the reconcile process to OIM - if anyone knows how to convert to a date type, that would be awesome?

Oracle SR response not forthcoming (after several weeks), we looked for alternatives. Found an OVD field which was set during lockout: pwdaccountlockedtime.

First OVD must know about the target LDAP attribute. OAM schema (ob* attributes) should have been loaded to OVD, this is the relevant objectclass containing the OAM lockout info:

objectclasses: ( 1.3.6.1.4.1.3831.0.1.21 NAME 'oblixPersonPwdPolicy' DESC 'Oracle Access Manager defined objectclass' SUP top 
AUXILIARY MAY ( obpasswordcreationdate $ obpasswordhistory $ obpasswordchangeflag $ obpasswordexpmail $ oblockouttime $ oblogintrycount $ obfirstlogin $ obresponsetries $ oblastloginattemptdate $ oblastresponseattemptdate $ obresponsetimeout $ oblastsuccessfullogin $ oblastfailedlogin $ etc. etc. ) )

Next, to have two-way updates (LDAP back to OIM) you need a RECON field, I added these two to the RA_LDAPUSER table in the OIM database: lock date and number of failed login attempts.

describe RA_LDAPUSER 

 ... rest of the RA_LDAPUSER table above ...
RECON_ORCLUSERLOCKEDON DATE 
RECON_LOGINATTEMPTS    NUMBER(19) 

LDAPUser


<reconAttr>
<oimFormDescriptiveName>Locked On</oimFormDescriptiveName> (This field must match the OIM Attribute Name - see Configuration, User Attributes to confirm)
<reconFieldName xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">pwdaccountlockedtime</reconFieldName>
<reconColName>RECON_ORCLUSERLOCKEDON</reconColName>
<emDataType>date</emDataType>
<formFieldType/>
<targetattr keyfield="false" encrypted="false" required="false" type="Date" name="pwdaccountlockedtime"/> (This field must match your OVD or LDAP)
</reconAttr>
<reconAttr>
<oimFormDescriptiveName>usr_login_attempts_ctr</oimFormDescriptiveName> (This field must match the OIM Attribute Name - see Configuration, User Attributes to confirm)
<reconFieldName xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">oblogintrycount</reconFieldName>
<reconColName>RECON_LOGINATTEMPTS</reconColName>
<emDataType>number</emDataType>
<formFieldType/>
<targetattr keyfield="false" encrypted="false" required="false" type="String" name="oblogintrycount"/> (This field must match your OVD or LDAP)
</reconAttr>


<reconAttr>
<oimFormDescriptiveName>Locked On</oimFormDescriptiveName> (This field must match above - the OIM Attribute Name)
<reconFieldName xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">pwdaccountlockedtime</reconFieldName>
<reconColName>RECON_ORCLUSERLOCKEDON</reconColName>
<emDataType>date</emDataType>
<formFieldType/>
<targetattr keyfield="false" encrypted="false" required="false" type="Date" name="usr_locked_on"> (This field must match the USR table column name)
<Transformation name="OneToOne">
<Parameter name="pwdaccountlockedtime" fieldname="pwdaccountlockedtime"/>
</Transformation> (This field must match your OVD or LDAP)
</targetattr>
</reconAttr>
<reconAttr>
<oimFormDescriptiveName>usr_login_attempts_ctr</oimFormDescriptiveName>
<reconFieldName xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">oblogintrycount</reconFieldName>
<reconColName>RECON_LOGINATTEMPTS</reconColName>
<emDataType>number</emDataType>
<formFieldType/>
<targetattr keyfield="false" encrypted="false" required="false" type="String" name="usr_login_attempts_ctr">
<Transformation name="OneToOne">
<Parameter name="oblogintrycount" fieldname="oblogintrycount"/>
</Transformation>
</targetattr>
</reconAttr>

RA_LDAPUSER.xml


<attribute-map>
<entity-attribute>Locked On</entity-attribute>
<target-field>RECON_ORCLUSERLOCKEDON</target-field>
</attribute-map>
<attribute-map>
<entity-attribute>usr_login_attempts_ctr</entity-attribute>
<target-field>RECON_LOGINATTEMPTS</target-field>
</attribute-map>

<field name="RECON_ORCLUSERLOCKEDON">
<type>date</type>
<required>false</required>
</field>
<field name="RECON_LOGINATTEMPTS">
<type>number</type>
<required>false</required>
</field>

LDAPUser.xml


<attribute name="Locked On">
<type>date</type>
<required>false</required>
<attribute-group>Basic</attribute-group>
<searchable>true</searchable>
</attribute>
<attribute name="usr_login_attempts_ctr">
<type>number</type>
<required>false</required>
<attribute-group>Basic</attribute-group>
<searchable>true</searchable>
</attribute>

<field name="pwdaccountlockedtime">
<type>date</type>
<required>false</required>
</field>
<field name="oblogintrycount">
<type>number</type>
<required>false</required>
</field>

<attribute-map>
<entity-attribute>Locked On</entity-attribute>
<target-field>pwdaccountlockedtime</target-field>
</attribute-map>
<attribute-map>
<entity-attribute>usr_login_attempts_ctr</entity-attribute>
<target-field>oblogintrycount</target-field>
</attribute-map>

Re-import LDAP/Recon config


Back to the OIM_HOME server bin directory, run weblogicImportMetadata.sh/bat

Doesn't appear to need a restart, but I usually run "PurgeCache.sh All" at the same time.

Test reconciliation


Run the scheduled job; LDAP Full User Group reconcile - look to error details, fix typos and run again.

Thursday, 22 December, 2011

An open house at site3.ca

So here's this naff video i made talking to the folks at Site 3 coLaboratory - check it out: http://site3.ca/news/how-does-it-work/

Wednesday, 7 December, 2011

OIM 11g really delete deleted

I run through a *lot* of accounts testing in OIM/OAM 11.1.1.5. Things happen, accounts get borked.

Easiest way to keep your UAT system from getting out of hand is to delete sometimes, but the Reuse ID functionality in OIM is extremely dangerous (i.e. it doesn't work).

So I suggest using this SQL to clean your deleted test accounts (this is not something I would do in production!)

delete from oud where oiu_key in (select oiu_key from oiu where usr_key in (select USR_KEY from USR where usr_status='Deleted'));
delete from osi where req_key in (select req_key from req where orc_key in (select orc_key from orc where orc.usr_key in (select USR_KEY from USR where usr_status='Deleted')));
delete from osi where osi_assigned_to_usr_key  in (select USR_KEY from USR where usr_status='Deleted');
delete from osh where osh_assigned_to_usr_key  in (select USR_KEY from USR where usr_status='Deleted');
delete from rcd where rce_key in (select rce_key from rce,orc where rce.orc_key = orc.orc_key and orc.usr_key  in (select USR_KEY from USR where usr_status='Deleted'));
delete from rch where rce_key in (select rce_key from rce,orc where rce.orc_key = orc.orc_key and orc.usr_key in (select USR_KEY from USR where usr_status='Deleted'));
delete from rcu where rce_key in (select rce_key from rce,orc where rce.orc_key = orc.orc_key and orc.usr_key  in (select USR_KEY from USR where usr_status='Deleted'));
delete from rcb where rce_key in (select rce_key from rce,orc where rce.orc_key = orc.orc_key and orc.usr_key  in (select USR_KEY from USR where usr_status='Deleted'));
delete from rce where orc_key in (select orc_key from orc where orc.usr_key in (select USR_KEY from USR where usr_status='Deleted'));
delete from oio where orc_key in (select orc_key from orc where orc.usr_key  in (select USR_KEY from USR where usr_status='Deleted'));
delete from oiu where usr_key  in (select USR_KEY from USR where usr_status='Deleted');
delete from oti where orc_key in (select orc_key from orc where orc.usr_key in (select USR_KEY from USR where usr_status='Deleted'));
delete from osi where orc_key in (select orc_key from orc where orc.usr_key in (select USR_KEY from USR where usr_status='Deleted'));
delete from orc where usr_key  in (select USR_KEY from USR where usr_status='Deleted');
delete from upd where upp_key in (select upp_key from upp where upp.usr_key in (select USR_KEY from USR where usr_status='Deleted'));
delete from upp where usr_key  in (select USR_KEY from USR where usr_status='Deleted');
delete from usg where usr_key  in (select USR_KEY from USR where usr_status='Deleted');
delete from uhd where uph_key in (select uph_key from uph where uph.usr_key in (select USR_KEY from USR where usr_status='Deleted'));
delete from uph where usr_key  in (select USR_KEY from USR where usr_status='Deleted');
delete from pcq where usr_key  in (select USR_KEY from USR where usr_status='Deleted');
delete from rcu where usr_key  in (select USR_KEY from USR where usr_status='Deleted');
delete from RECON_USER_MATCH where USR_KEY in (select USR_KEY from USR where usr_status='Deleted');
delete from RECON_ROLE_MATCH where RE_KEY in (select re_key from RECON_EVENTS where usr_key in (select USR_KEY from USR where usr_status='Deleted'));
delete from RECON_ROLE_MEMBER_MATCH where RE_KEY in (select re_key from RECON_EVENTS where usr_key in (select USR_KEY from USR where usr_status='Deleted'));
delete from RA_LDAPROLEMEMBERSHIP where RE_KEY in (select re_key from RECON_EVENTS where usr_key in (select USR_KEY from USR where usr_status='Deleted'));
delete from RECON_HISTORY where RE_KEY in (select re_key from RECON_EVENTS where usr_key in (select USR_KEY from USR where usr_status='Deleted'));
delete from RA_LDAPUSER where RE_KEY in (select re_key from RECON_EVENTS where usr_key in (select USR_KEY from USR where usr_status='Deleted'));
delete from RECON_EVENTS where usr_key in (select USR_KEY from USR where usr_status='Deleted');
delete from usr where usr_key in (select USR_KEY from USR where usr_status='Deleted');

OIM 11g SPML Modify Request

This post is a follow-on from my post about getting the SPML service to work security-wise.

Now we're using CXF to send a mod request - including User Defined Fields (UDF). My UDF is TermsAgreement, if you're not sure what to use, I'm not surprised.

In API code, if it's Thor API you need to use USR_UDF_ATTRIBNAME.. if it's Oracle API you need to use exactly as appears on User Configuration -> User Attributes tab; in the "Attribute Name" column.

Also verify with what's in your OIM metadata/iam-features-requestactions/model-data/ModifyUserDataset.xml - the value of what you set for AttributeReference name="YourAttributeName"

Also, it doesn't seem to love spaces.

The full source project: http://wikisend.com/download/608008/spmlclient-11.1.1.5.cxf.jar

private static final QName SERVICE_NAME = new QName("http://xmlns.oracle.com/idm/identity/webservice/SPMLService",
   "SPMLService");

 public static void main(String args[]) throws Exception {
 
  URL wsdlURL = new URL("http://localhost:14000/spml-xsd/SPMLService?wsdl");
  // URL wsdlURL = SPMLService.WSDL_LOCATION;

  SPMLService ss = new SPMLService(wsdlURL, SERVICE_NAME);
  SPMLRequestPortType port = ss.getSPMLServiceProviderSoap();

  Map ctx = ((BindingProvider) port).getRequestContext();
  ctx.put("ws-security.username", "xelsysadm");
  ctx.put("ws-security.password", "the password");

  // adding logging
  Client client = ClientProxy.getClient(port);
  client.getInInterceptors().add(new LoggingInInterceptor());
  client.getOutInterceptors().add(new LoggingOutInterceptor());

  ServiceHeaderType serviceHeader = new ServiceHeaderType();

  System.out.println("Invoking spmlModifyRequest...");
  oracle.iam.wsschema.model.spmlv2.core.ModifyRequestType modifyBody = new oracle.iam.wsschema.model.spmlv2.core.ModifyRequestType();
 
  java.util.List modCapData = new java.util.ArrayList();

  oracle.iam.wsschema.model.spmlv2.core.CapabilityDataType modCap = new oracle.iam.wsschema.model.spmlv2.core.CapabilityDataType();
  java.util.List modCapAny = new java.util.ArrayList();
  modCap.getAny().addAll(modCapAny);
  modCap.setMustUnderstand(Boolean.TRUE);
  modCap.setCapabilityURI("urn:oasis:names:tc:SPML:2:0:reference");
  modCapData.add(modCap);
  modifyBody.getCapabilityData().addAll(modCapData);
  modifyBody.setRequestID("RequestID-763892610");

  oracle.iam.wsschema.model.spmlv2.core.ExecutionModeType async = oracle.iam.wsschema.model.spmlv2.core.ExecutionModeType.ASYNCHRONOUS;
  modifyBody.setExecutionMode(async);
  modifyBody.setLocale("en");

  oracle.iam.wsschema.model.spmlv2.core.PSOIdentifierType modifyBodyPsoID = new oracle.iam.wsschema.model.spmlv2.core.PSOIdentifierType();

  java.util.List modifyBodyPsoIDAny = new java.util.ArrayList();
  java.lang.Object modifyBodyPsoIDAnyVal1 = null;
  modifyBodyPsoIDAny.add(modifyBodyPsoIDAnyVal1);
  modifyBodyPsoID.getAny().addAll(modifyBodyPsoIDAny);
  modifyBodyPsoID.setID("identity:6C9B96E99FC8DC32E040E50A3D5252F5");

  java.util.List mods = new java.util.ArrayList();
  oracle.iam.wsschema.model.spmlv2.core.ModificationType modType = new oracle.iam.wsschema.model.spmlv2.core.ModificationType();

  oracle.iam.wsschema.model.spmlv2.core.SelectionType component = new oracle.iam.wsschema.model.spmlv2.core.SelectionType();
  java.util.List componentAny = new java.util.ArrayList();
  component.getAny().addAll(componentAny);
  java.util.List componentNamespacePrefixMap = new java.util.ArrayList();
  component.getNamespacePrefixMap().addAll(componentNamespacePrefixMap);
  component.setPath("/identity");
  component.setNamespaceURI("http://www.w3.org/TR/xpath20");
  modType.setComponent(component);

  oracle.iam.wsschema.model.common.pso.ProvisioningObjectType modsData = new oracle.iam.wsschema.model.common.pso.ProvisioningObjectType();

  oracle.iam.wsschema.model.common.pso.Identity idAttribs = new oracle.iam.wsschema.model.common.pso.Identity();
  oracle.iam.wsschema.model.common.pso.UnboundedAttributes unboundedIdAttribs = new oracle.iam.wsschema.model.common.pso.UnboundedAttributes();

                // Must use these unbounded attributes for UDFs.
  java.util.List modsDataIdentityAttributesAttr = new java.util.ArrayList();
  DsmlAttr udfAttr = new DsmlAttr();
  udfAttr.setName("TermsAgreement");
  udfAttr.getValue().add("1.0");
  modsDataIdentityAttributesAttr.add(udfAttr);
  unboundedIdAttribs.getAttr().addAll(modsDataIdentityAttributesAttr);
  idAttribs.setAttributes(unboundedIdAttribs);


                // Standard attributes for SPML modifications follows.
  idAttribs.setActiveEndDate(javax.xml.datatype.DatatypeFactory.newInstance().newXMLGregorianCalendar(
    "2011-06-14T16:07:20.498-04:00"));
  idAttribs.setActiveStartDate(javax.xml.datatype.DatatypeFactory.newInstance().newXMLGregorianCalendar(
    "2011-06-14T16:07:20.514-04:00"));

  oracle.iam.wsschema.model.common.pso.MultiValuedString deptNum = new oracle.iam.wsschema.model.common.pso.MultiValuedString();
  java.util.List deptNumValue = new java.util.ArrayList();
  deptNumValue.add("12345");
  deptNum.getValue().addAll(deptNumValue);
  idAttribs.setDepartmentNumber(deptNum);

  oracle.iam.wsschema.model.common.pso.MultiValuedString firstName = new oracle.iam.wsschema.model.common.pso.MultiValuedString();
  java.util.List firstNameValue = new java.util.ArrayList();
  firstNameValue.add("Hello");
  firstName.getValue().addAll(firstNameValue);
  idAttribs.setGivenName(firstName);

  idAttribs.setHireDate(javax.xml.datatype.DatatypeFactory.newInstance().newXMLGregorianCalendar(
    "2011-06-14T16:07:20.514-04:00"));

  oracle.iam.wsschema.model.common.pso.MultiValuedString mail = new oracle.iam.wsschema.model.common.pso.MultiValuedString();
  java.util.List mailValue = new java.util.ArrayList();
  mail.getValue().addAll(mailValue);
  idAttribs.setMail(mail);

  idAttribs.setMiddleName("MiddleName");

  oracle.iam.wsschema.model.common.pso.MultiValuedBinary passwd = new oracle.iam.wsschema.model.common.pso.MultiValuedBinary();
  java.util.List passwdValue = new java.util.ArrayList();
  passwdValue.add("Testing123!".getBytes());
  passwd.getValue().addAll(passwdValue);
  idAttribs.setPassword(passwd);

  idAttribs.setPreferredLanguage("fr");

  idAttribs.setUserType("NONW");

  modsData.setIdentity(idAttribs);

  oracle.iam.wsschema.model.common.pso.Member modsDataMember = new oracle.iam.wsschema.model.common.pso.Member();
  // Entity ID / USR_KEY from OIM
  modsDataMember.setIdentityPSOID("3");

  modsData.setMember(modsDataMember);
  modType.setData(modsData);

  oracle.iam.wsschema.model.spmlv2.core.ModificationModeType modTypeModificationMode = oracle.iam.wsschema.model.spmlv2.core.ModificationModeType.REPLACE;
  modType.setModificationMode(modTypeModificationMode);
  mods.add(modType);
  modifyBody.getModification().addAll(mods);

  oracle.iam.wsschema.model.spmlv2.core.ReturnDataType modifyBodyReturnData = oracle.iam.wsschema.model.spmlv2.core.ReturnDataType.DATA;
  modifyBody.setReturnData(modifyBodyReturnData);

  oracle.iam.wsschema.model.spmlv2.core.ModifyResponseType modReturn = port.spmlModifyRequest(modifyBody,
    serviceHeader);
  System.out.println("spmlModifyRequest.result=" + modReturn);

       }

And since some people are assembling the requests in other clients, here's an example of the request by the above code:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <wsse:Security soap:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
      <wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="UsernameToken-1">
        <wsse:Username>xelsysadm</wsse:Username>
        <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">the password</wsse:Password>
      </wsse:UsernameToken>
    </wsse:Security>
    <ServiceHeader xmlns:ns9="http://xmlns.oracle.com/idm/identity/OperationData" xmlns:ns8="urn:oasis:names:tc:SPML:2:0:reference" xmlns:ns7="urn:oasis:names:tc:SPML:2:0:suspend" xmlns:ns6="http://xmlns.oracle.com/idm/identity/PSO" xmlns:ns5="http://xmlns.oracle.com/idm/identity/spmlv2custom/Username" xmlns:ns4="urn:oasis:names:tc:SPML:2:0:password" xmlns:ns3="urn:oasis:names:tc:SPML:2:0:async" xmlns:ns2="urn:oasis:names:tc:SPML:2:0" xmlns="urn:names:spml:ws:header"/>
  </soap:Header>
  <soap:Body>
    <ns2:modifyRequest xmlns="urn:names:spml:ws:header" xmlns:ns2="urn:oasis:names:tc:SPML:2:0" xmlns:ns3="urn:oasis:names:tc:SPML:2:0:async" xmlns:ns4="urn:oasis:names:tc:SPML:2:0:password" xmlns:ns5="http://xmlns.oracle.com/idm/identity/spmlv2custom/Username" xmlns:ns6="http://xmlns.oracle.com/idm/identity/PSO" xmlns:ns7="urn:oasis:names:tc:SPML:2:0:suspend" xmlns:ns8="urn:oasis:names:tc:SPML:2:0:reference" xmlns:ns9="http://xmlns.oracle.com/idm/identity/OperationData" returnData="data" requestID="RequestID-763892610" executionMode="asynchronous" locale="en">
      <ns2:capabilityData mustUnderstand="true" capabilityURI="urn:oasis:names:tc:SPML:2:0:reference"/>
      <ns2:modification modificationMode="replace">
        <ns2:component path="/identity" namespaceURI="http://www.w3.org/TR/xpath20"/>
        <ns2:data>
          <ns6:identity>
            <ns6:attributes>
              <ns6:attr name="TermsAgreement">
                <ns6:value>1.0</ns6:value>
              </ns6:attr>
            </ns6:attributes>
            <ns6:activeEndDate>2011-06-14T16:07:20.498-04:00</ns6:activeEndDate>
            <ns6:activeStartDate>2011-06-14T16:07:20.514-04:00</ns6:activeStartDate>
            <ns6:departmentNumber>
              <ns6:value>12345</ns6:value>
            </ns6:departmentNumber>
            <ns6:givenName>
              <ns6:value>Hello</ns6:value>
            </ns6:givenName>
            <ns6:hireDate>2011-06-14T16:07:20.514-04:00</ns6:hireDate>
            <ns6:mail/>
            <ns6:middleName>MiddleName</ns6:middleName>
            <ns6:password>
              <ns6:value>VGVzdGluZzEyMyE=</ns6:value>
            </ns6:password>
            <ns6:preferredLanguage>fr</ns6:preferredLanguage>
            <ns6:userType>NONW</ns6:userType>
          </ns6:identity>
          <ns6:member>
            <ns6:identityPSOID>3</ns6:identityPSOID>
          </ns6:member>
        </ns2:data>
      </ns2:modification>
    </ns2:modifyRequest>
  </soap:Body>
</soap:Envelope>