I had an opportunity to work on OpenSSO with Federation (Account Linking) Saml2 use cases (Registration, Already Registration, Un-Registration) using JDBC datastore.    If you would like to read about saml2 and the Federation, please read chapter 5 start at page 26 on this saml tech overview document on oasis.http://www.oasis-open.org/committees/download.php/27819/sstc-saml-tech-overview-2.0-cd-02.pdf

We had configured and developed the Service Provider (SP) using this OpenSSO Express Build 8 version and the client is using IBM Tivoli product for their Identify Provider (IDP).

I have found two major bugs on this OpenSSO Express Build 8 product, which it did not work when we persist the nameId or delete the nameId correctly  (NameId or Alias is the unique identify of the account between the IDP and SP).  I have bold the font to where the change is in the code and including the work around solutions.

First Problem and Work around Solution: When we do the Registration use cases, we need to be able to save the nameId associate to this userId in the database table.  But OpenSSO is inconsistent, it works and then it is not working .   Let take a look of this method updateUser() in the com.sun.identity.idm.plugins.database.JdbcSimpleUserDao.java , where it need to save the nameId the database.

 /**
     *
     * @param userID is user id
     * @param attrMap is a Map that contains attribute/column names as keys
     *        and values are Sets of values
     */
    public void updateUser(String userID, String userIDAttributeName,
            Map > attrMap) {       
                            
        if(debug.messageEnabled()) {
            debug.message("JdbcSimpleUserDao.updateUser: called with params"
                    + " id=" + userID
                    + " userIDAttributeName=" + userIDAttributeName
                    + " attrMap =\n" + attrMap);
        }       
        if (userID == null || userID.trim().length() == 0
                || userIDAttributeName == null
                || userIDAttributeName.trim().length() == 0
                || attrMap == null || attrMap.isEmpty() ) {
            if(debug.messageEnabled()) {
                debug.message("JdbcSimpleUserDao.updateUser: one or more of"
                    + " parameters is null or empty, so not executing update"
                    + " command. userID=" + userID + " userIDAttributeName="
                    + userIDAttributeName + " attrMap=" + attrMap);
            }
            return;
        }
        userID = userID.trim();
        userIDAttributeName = userIDAttributeName.trim();
       
        /**
         *  RFE: make sure update does not mess up referential integrity...
        //dont want to change primary key id uid so lets just get rid of
        if (attrMap.containsKey(primaryKetAttrname)){
            attrMap.remove("uid");
        }
        **/
       
        //FIX: need to consider multi-valued attributes later
        //FIX: Need to make sure all required(non null) attrs/columns has values
        //matches the column names in DB and in prepared statement
        Map positionMap = new HashMap();
        String updateUserStmt = "UPDATE" + SPACE + userTableName + SPACE
                                + "SET" + SPACE;
         //build update statement from attrMap
        Set attrKeySet = attrMap.keySet();
        Iterator attrs = attrKeySet.iterator();             
        //query will look like
        //   UPDATE userTable SET givenname = ?, sn = ? WHERE uid = ?"
        for(int position=1; attrs.hasNext(); position++) {
            String attr = attrs.next();
            positionMap.put(position, attr);
            updateUserStmt = updateUserStmt + SPACE + attr;  
            updateUserStmt = updateUserStmt + SPACE + "= ?";
            if(attrs.hasNext()) {
                updateUserStmt = updateUserStmt + COMMA;
            }
        }
        updateUserStmt= updateUserStmt + SPACE + "WHERE" + SPACE
                + userIDAttributeName + SPACE + "= ?";    
        if(debug.messageEnabled()) {
            debug.message("JdbcSimpleUserDao.create: SQL update statement = "
                    + updateUserStmt);
        }
       
        Connection con = null;
        PreparedStatement stmt = null;
        try {
            con = getConnection();
            stmt = con.prepareStatement(updateUserStmt);
            //FIX: later deal better with various types and multi-valued attrs
            for (int i=1; i<=positionMap.size(); i++){               
                String keyAtPosition = positionMap.get(i);
                Set valSet = attrMap.get(keyAtPosition);
                if (valSet != null && !valSet.isEmpty()) {
                  Iterator it = valSet.iterator();
                  String value = null;//null may be a valid value if not required column
                  if(it.hasNext()) {
                    value = it.next();                               
                  }
                  //what if value == null, should I use setNull() ???
                  stmt.setString(i, value);
                }
            }
            int uidIndexPosition = positionMap.size() + 1;
            stmt.setString(uidIndexPosition, userID); //add uid for where clause        
            stmt.executeUpdate();
        } catch (Exception ex1) {
            if(debug.messageEnabled()) {
                debug.message("JdbcSimpleUserDao.updateUser:" + ex1);
            }
            throw new RuntimeException(ex1);
        } finally {
            closeStatement(stmt);
            closeConnection(con);
        }
    }

 

As you can see they are passing the HashMap of keys and values and compose the sql statement to make the database call.  Since this is the HasMap, it will have one to many sets.  In this OpenSSO version, they have 2 sets.  One had the value and the other one is empty.  When the first set had the value, everything is great, your nameId is persisted. When the second set had the value and this first one is empty, nothing is persisted.  (That is why it is working and suddenly it is not). This could be the bugs somewhere on the HashMap before passing to this DAO class.  But I did made the work around to fix the code to check and get the NON empty set and persist it to the database.  Here is my fix code version of this updateUser() method:

  public void updateUser(String userID, String userIDAttributeName, Map> attrMap)
  {
    if (debug.messageEnabled()) {
      debug.message("JdbcSimpleUserDao.updateUser: called with params id=" + userID + " userIDAttributeName=" + userIDAttributeName + " attrMap =\n" + attrMap);
    }

    if ((userID == null) || (userID.trim().length() == 0) || (userIDAttributeName == null) || (userIDAttributeName.trim().length() == 0) || (attrMap == null) || (attrMap.isEmpty()))
    {
      if (debug.messageEnabled()) {
        debug.message("JdbcSimpleUserDao.updateUser: one or more of parameters is null or empty, so not executing update command. userID=" + userID + " userIDAttributeName=" + userIDAttributeName + " attrMap=" + attrMap);
      }

      return;
    }
    userID = userID.trim();
    userIDAttributeName = userIDAttributeName.trim();

    Map positionMap = new HashMap();
    String updateUserStmt = "UPDATE " + userTableName + " " + "SET" + " ";

    Set attrKeySet = attrMap.keySet();
    Iterator attrs = attrKeySet.iterator();

    for (int position = 1; attrs.hasNext(); position++) {
      String attr = (String)attrs.next();
      System.out.println("updateUser() attr: " + attr);
      System.out.println("updateUser() position: " + position);
      positionMap.put(Integer.valueOf(position), attr);
      updateUserStmt = updateUserStmt + " " + attr;
      updateUserStmt = updateUserStmt + " " + "= ?";
      if (attrs.hasNext()) {
        updateUserStmt = updateUserStmt + ",";
      }
    }
    updateUserStmt = updateUserStmt + " " + "WHERE" + " " + userIDAttributeName + " " + "= ?";

    if (debug.messageEnabled()) {
      debug.message("JdbcSimpleUserDao.create: SQL update statement = " + updateUserStmt);
    }

    Connection con = null;
    PreparedStatement stmt = null;
    try {
      con = getConnection();
      stmt = con.prepareStatement(updateUserStmt);

      for (int i = 1; i <= positionMap.size(); i++) {
        String keyAtPosition = (String)positionMap.get(Integer.valueOf(i));
        System.out.println("updateUser() keyAtPosition: " + keyAtPosition);
        Set valSet = (Set)attrMap.get(keyAtPosition);
        System.out.println("updateUser() valSet: " + valSet.size());

        String value = "";
        if ((valSet != null) && (!valSet.isEmpty()))
        {
          Iterator iterator = valSet.iterator();
          String setValue = null;
          int j = -1;
          while (iterator.hasNext()) {
            setValue = (String)iterator.next();
            j += 1;
            if ((setValue != null) && (!setValue.equals(""))) {
              value = setValue;
              System.out.println("get the value at possition: " + j + " and value is" + value);

              break;
            }
            System.out.println("value null or empty at possition: " + j);
          }

        }

        System.out.println("my code value: " + value);
        stmt.setString(i, value);
      }

      int uidIndexPosition = positionMap.size() + 1;
      stmt.setString(uidIndexPosition, userID);
      stmt.executeUpdate();
    } catch (Exception ex1) {
      if (debug.messageEnabled()) {
        debug.message("JdbcSimpleUserDao.updateUser:" + ex1);
      }
      throw new RuntimeException(ex1);
    } finally {
      closeStatement(stmt);
      closeConnection(con);
    }
  }

 

2.  Second Problem and Work around Solution:  When we do the Termination (Un-Registraton) usecase, the nameId is not able to be removed from the database table.  Again, this is inconsistent.  If it is not working, you can restart the application and it 'may' works again.  The real problem is something to do with Cache on this openSSO version.  At the time of termination call, it need to look up the nameId and send to DAO module to remove it.  In some cases,  it was losing this nameId in the Cache and fails.  Instead of we need to dig in to how the Cache in OpenSSO works, I know that I still have this UserId, from this userId then I can get nameId from database and remove it in the database.  I had worked with two different database tables for two different SP service provider applications, so I externalize the external config file outside.  Here is the update to com.sun.identity.saml2.common.AccountUtils java class:


   
    static DataSource datasource; //for JNDI style connections
    static String nameIdSQL;
    static String datasourceName; //for JNDI style connections
    static Properties properties;
    static String nameIdInfoColumn = "sun_fm_saml2_nameid_info";
    static String removeNameIdSQL;

  
    static {
        try {
            metaManager= new SAML2MetaManager();
        } catch (SAML2MetaException se) {
            SAML2Utils.debug.error("Unable to obtain Meta Manager.", se);
        }

// To Initialize the JDBC datasource using external config file
        System.out.println("Starting to initial the JDBC datasource.....");
        //set the datasource class field
        FileInputStream inputStream = null;      

        try {
              String configPath = System.getProperty("configPath");
              System.out.println("configPath: " + configPath);
              properties = new Properties();
             
              String jdbcFile = configPath+"/jdbc"+".properties";
              System.out.println("jdbcFile: " + jdbcFile);
                 inputStream = new FileInputStream(jdbcFile);
                 properties.load(inputStream);
              datasourceName = "java:comp/env/" + properties.getProperty("datasourceName");
              System.out.println("datasourceName: " + datasourceName);
              nameIdSQL = properties.getProperty("nameIdSQL");
              System.out.println("nameIdSQL: " + nameIdSQL);
              nameIdInfoColumn = properties.getProperty("nameIdInfoColumn");
              removeNameIdSQL = properties.getProperty("removeNameIdSQL");
            Context ctx = new InitialContext();
            //java:comp/env requires a resource-ref in web.xml
            datasource = (DataSource) ctx.lookup(datasourceName);  
                    
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
              try {
                            inputStream.close();
                        } catch (IOException e){
                            e.printStackTrace();
                        }
        }

    }

   
     private static Connection getConnection() throws SQLException {
        Connection conn = null;
      
        conn = datasource.getConnection();
     
        return conn;
    }

   

   
    private static void closeConnection(Connection dbConnection) {
        try {
            if (dbConnection != null && !dbConnection.isClosed()) {
                dbConnection.close();
            }
        } catch (SQLException e) {
           e.printStackTrace();
        }
    }
   

    private static String getUserName (String userId){
           
          StringTokenizer st = new StringTokenizer(userId, ",");
          String idInfo = st.nextToken();
          System.out.println("id username: " + idInfo);
          String userName = idInfo.substring(3,idInfo.length());
          System.out.println("userName: " + userName);
          return userName;
         
         
    }

  
    private static String getNameIdbyUserId(String userName){
         
            Connection con = null;
        PreparedStatement stmt = null;
        ResultSet result = null;
      
        System.out.println("nameIdSQL: " + nameIdSQL);
          String nameIdInfo = null;
        try {
       
             con = getConnection();
           stmt = con.prepareStatement(nameIdSQL); 
           stmt.setString(1, userName.toLowerCase());  
           result = stmt.executeQuery();
         
           while( result.next() ) {
                   nameIdInfo = result.getString(nameIdInfoColumn);
           } 
              
                                   
        } catch (Exception ex1) {
           ex1.printStackTrace();
        } finally {
            closeStatement(stmt);
            closeConnection(con);
        }
       
        System.out.println("nameIdInfo from database: " + nameIdInfo);
       
        return nameIdInfo;
    }

 
 
   private static void removeNameIdbyUserId(String userName){
         
            Connection con = null;
        PreparedStatement stmt = null;
      
      
        System.out.println("removeNameIdSQL: " + removeNameIdSQL);
        try {
       
             con = getConnection();
           stmt = con.prepareStatement(removeNameIdSQL); 
           stmt.setString(1, userName.toLowerCase());  
           int total = stmt.executeUpdate();
         
           System.out.println("update status: " + total);
              
                                   
        } catch (Exception ex1) {
           ex1.printStackTrace();
        } finally {
            closeStatement(stmt);
            closeConnection(con);
        }
       
      
    }

   

     //should I catch all Exceptions instead of just SQL ????? I think so
    private static void closeStatement(PreparedStatement stmt) {
        try {
            if (stmt != null) {
                stmt.close();
            }
        } catch (SQLException se) {
           se.printStackTrace();
        }
    }


    /**
     * Returns the account federation information of a user for the given
     * identity provider and a service provider.
     * @param userID user id for which account federation needs to be returned.
     * @param hostEntityID EntityID of the hosted entity.
     * @param remoteEntityID EntityID of the remote entity.
     * @return the account federation info object.
     *         null if the account federation does not exist.
     * @exception SAML2Exception if account federation retrieval is failed.
     */
    public static NameIDInfo getAccountFederation(
           String userID,
           String hostEntityID,
           String remoteEntityID) throws SAML2Exception {

        SAML2Utils.debug.message("AccountUtils.getAccountFederation:");

        if(userID == null) {
           throw new SAML2Exception(SAML2Utils.bundle.getString(
                 "nullUserID"));
        }
                System.out.println("userID: " + userID);
                 String userName = getUserName(userID);
                 System.out.println("userName: " + userName);
        if(hostEntityID == null) {
           throw new SAML2Exception(SAML2Utils.bundle.getString(
                 "nullHostEntityID"));
        }
                System.out.println("hostEntityID: " + hostEntityID);
        if(remoteEntityID == null) {
           throw new SAML2Exception(SAML2Utils.bundle.getString(
                 "nullRemoteEntityID"));
        }
                System.out.println("remoteEntityID: " + hostEntityID);
        try {
            Set set = SAML2Utils.getDataStoreProvider().getAttribute(
                  userID, getNameIDInfoAttribute());
                        String info = null;
            if(set == null || set.isEmpty()) {
              
               //return null;
               // we do have the user_id here, we should able to do the select to get the nameId
               System.out.println("Go to my code fix for termination");
               info = getNameIdbyUserId(userName);

               if (info == null){
                           if(SAML2Utils.debug.messageEnabled()) {
                      SAML2Utils.debug.message("AccountUtils.getAccount" +
                      "Federation : user does not have any account federations.");
                       }
                       return null;
                  }
              
            } else {
         
                String filter = hostEntityID + DELIM + remoteEntityID + DELIM;
                if(SAML2Utils.debug.messageEnabled()) {
                   SAML2Utils.debug.message("AccountUtils.getAccountFederation: "+
                   " filter = " + filter + " userID = " + userID);
                }
              
   
                for(Iterator iter = set.iterator(); iter.hasNext();) {
                    String value = (String)iter.next();
                    if(value.startsWith(filter)) {
                       info = value;
                       break;
                    }
                }
     
                if(info == null) {
                   if(SAML2Utils.debug.messageEnabled()) {
                      SAML2Utils.debug.message("AccountUtils.getAccount" +
                      "Federation : user does not have account federation " +
                      " corresponding to =" + filter);
                   }
                   return null;
                }
                 
              }

            return NameIDInfo.parse(info);

        } catch (DataStoreProviderException dse) {

           SAML2Utils.debug.error("AccountUtils.readAccountFederation" +
           "Info: DataStoreProviderException", dse);
           throw new SAML2Exception(dse.getMessage());
        }
       
    }

  
    /**
     * Removes the account federation of a user.
     * @param info NameIDInfo object.
     * @param userID user identifie for which the account federation needs to
     *               be removed.
     * @return true if the account federation is removed successfully.
     * @exception SAML2Exception if any failure.
     */
    public static boolean removeAccountFederation(
        NameIDInfo info, String userID) throws SAML2Exception {

         SAML2Utils.debug.message("AccountUtils.removeAccountFederation:");
         if(info == null) {
            throw new SAML2Exception(SAML2Utils.bundle.getString(
                  "nullNameIDInfo"));
         }

         if(userID == null) {
            throw new SAML2Exception(SAML2Utils.bundle.getString(
                  "nullUserID"));
         }

         try {
             Set existingFed =  SAML2Utils.getDataStoreProvider().
                   getAttribute(userID, getNameIDInfoAttribute());
             Set existingInfoKey = SAML2Utils.getDataStoreProvider().
                   getAttribute(userID, getNameIDInfoKeyAttribute());

             if(existingFed == null || existingFed.isEmpty()) {
                if(SAML2Utils.debug.messageEnabled()) {
                   SAML2Utils.debug.message("AccountUtils.removeAccount" +
                   "Federation: The cache is screwing up again");
                }
                // The cache is screw up again, do it manually
                System.out.println("Manually remove the database");
                String userName = getUserName(userID);
                System.out.println("userName: " + userName);
                removeNameIdbyUserId(userName);

               
                  return true;
             } else {

 
                 String infoValue = info.toValueString();
                 String infoKeyValue = info.getNameIDInfoKey().toValueString();
   
                 if(SAML2Utils.debug.messageEnabled()) {
                    SAML2Utils.debug.message("AccountUtils.removeAccount" +
                    "Federation: info to be removed:"+ infoValue + "user="+
                     userID + "infoKeyValue = " + infoKeyValue);
                 }
                
                 if(existingFed.contains(infoValue)) {
   
                    existingFed.remove(infoValue);
                    if(existingInfoKey != null &&
                           existingInfoKey.contains(infoKeyValue)) {
                       existingInfoKey.remove(infoKeyValue);
                    }
   
                    Map map = new HashMap();
                    map.put(getNameIDInfoAttribute(), existingFed);
                    map.put(getNameIDInfoKeyAttribute(), existingInfoKey);
                    SAML2Utils.getDataStoreProvider().setAttributes(userID, map);
                    return true;
                 }
   
                 if(SAML2Utils.debug.messageEnabled()) {
                    SAML2Utils.debug.message("AccountUtils.removeAccount" +
                    "Federation: account federation info not found.");
                 }
                 return false;
               }

         } catch (DataStoreProviderException dse) {
             SAML2Utils.debug.error("SAML2Utils.removeAccountFederation: " +
             "DataStoreProviderException", dse);
             throw new SAML2Exception(dse.getMessage());
         }
    }

Enclose three update files: 2 java files and one external jdbc properties file.  Please remove .txt extention at the end of the file name.

1.  JdbcSimpleUserDao.java.txt

2.  AccountUtils.java.txt

3   jdbc.properties.txt