Tuesday, April 13, 2010

ADF Security: Playing with Weblogic APIs on Authentication Providers

As promised in my last post, in this post I will share with you how to play with the Weblogic API through JMX to handle tasks such as creating users, creating roles, assigning users to roles, and letting users change their own password. This post will clear out the gray areas in implementing user subscription or registration forms and the generation of salted-hashed passwords that can be recognized by weblogic. This post assumes that you have already properly set-up your SQLAuthenticator provider in weblogic.

Though this post is specific to SQLAuthenticator, the concept demonstrated herein is applicable to the Weblogic API as a whole. Just changing the MBEAN_INTERFACE constant to "weblogic.security.providers.authentication.DefaultAuthenticatorMBean", this post already applies to the DefaultAuthenticator.

Prerequisites

  • Setup the SQLAuthenticator required schema
  • Create a Datasource in weblogic console that points to the schema in step 1.
  • Setup SQLAuthenticator provider in weblogic console.

Implementation

The key to playing with Weblogic APIs is through JMX (Java Management Extensions). I cant tell through experience that understanding JMX is not for the faint-hearted, but if you got it, you will realize how simple it is. Below is a sample adapter class that I device so that I will deal with JMX only once, and the rest of my application that needs to access the weblogic API will just point to my adapter class.
package soadev.adapters;

import java.io.Serializable;
import java.io.IOException;
import java.util.Hashtable;
import javax.management.Descriptor;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.modelmbean.ModelMBeanInfo;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.naming.Context;

public class SQLAuthenticatorAdapter implements Serializable {
    private static final String MBEAN_INTERFACE = "weblogic.security.providers.authentication.SQLAuthenticatorMBean";
    private MBeanServerConnection connection;
    private JMXConnector connector;
    private ObjectName providerON;
    
    
    public void createUser(String username, String password,
                           String description) throws Exception {
        connection.invoke(providerON, "createUser",
                          new Object[] { username, password, description },
                          new String[] { "java.lang.String",
                                         "java.lang.String",
                                         "java.lang.String" });
    }
    
    public void createGroup(String groupName, String description) throws Exception {
        connection.invoke(providerON, "createGroup",
                          new Object[] { groupName, description },
                          new String[] { "java.lang.String",
                                         "java.lang.String" });
    }
    
    public void addMemberToGroup(String groupName, String username)throws Exception{
        connection.invoke(providerON, "addMemberToGroup",
                          new Object[] { groupName, username },
                          new String[] { "java.lang.String", "java.lang.String" });
    }

    public void changeUserPassword(String username, String oldPassword,
                                   String newPassword) throws Exception {
        connection.invoke(providerON, "changeUserPassword",
                          new Object[] { username, oldPassword, newPassword },
                          new String[] { "java.lang.String",
                                         "java.lang.String",
                                         "java.lang.String" });
    }
    
    public boolean isMember(String parentGroupName, String memberUserOrGroupName, boolean recursive)throws Exception{
        return (Boolean) connection.invoke(providerON, "isMember",
                          new Object[] { parentGroupName, memberUserOrGroupName, recursive },
                          new String []{"java.lang.String", "java.lang.String", "java.lang.Boolean"});
    }
    
    private ObjectName getAuthenticationProviderObjectName(String type)throws Exception{
       
            ObjectName defaultRealm = getDefaultRealm();
            ObjectName[] atnProviders =
                (ObjectName[])connection.getAttribute(defaultRealm,
                                                      "AuthenticationProviders");
            ObjectName MBTservice =
                new ObjectName("com.bea:Name=MBeanTypeService,Type=weblogic.management.mbeanservers.MBeanTypeService");
            for (int p = 0; atnProviders != null && p < atnProviders.length;
                 p++) {
                ObjectName provider = atnProviders[p];
                ModelMBeanInfo info =
                    (ModelMBeanInfo)connection.getMBeanInfo(provider);
                Descriptor desc = info.getMBeanDescriptor();
                String className =
                    (String)desc.getFieldValue("interfaceClassName");
                String[] mba =
                    (String[])connection.invoke(MBTservice, "getSubtypes",
                                                new Object[] { type },
                                                new String[] { "java.lang.String" });
                for (int i = 0; i < mba.length; i++) {
                    if (mba[i].equals(className)) {
                        return provider;
                    }
                }
            }
            return null;
    }
    private ObjectName getDefaultRealm() throws Exception {
        ObjectName service =
            new ObjectName("com.bea:Name=DomainRuntimeService,Type=weblogic.management.mbeanservers.domainruntime.DomainRuntimeServiceMBean");
        ObjectName domainMBean =
            (ObjectName)connection.getAttribute(service, "DomainConfiguration");
        ObjectName securityConfiguration =
            (ObjectName)connection.getAttribute(domainMBean,
                                                "SecurityConfiguration");
        ObjectName defaultRealm =
            (ObjectName)connection.getAttribute(securityConfiguration,
                                                "DefaultRealm");
        return defaultRealm;
    }
    public void connect(){
        String hostname = "localhost";
        String username = "weblogic";
        String password = "weblogic1";
        int port = 7101;
        connect(hostname, username, password, port);
    }
    
    public void connect(String hostname, String username, String password, int port){
        try {       
            String protocol = "t3";
            String jndi =
                "/jndi/weblogic.management.mbeanservers.domainruntime";
            JMXServiceURL serviceURL =
                new JMXServiceURL(protocol, hostname, port, jndi);
            Hashtable env = new Hashtable();
            env.put(Context.SECURITY_PRINCIPAL, username);
            env.put(Context.SECURITY_CREDENTIALS, password);
            env.put(JMXConnectorFactory.PROTOCOL_PROVIDER_PACKAGES,
                    "weblogic.management.remote");
            env.put("jmx.remote.x.request.waiting.timeout", new Long(10000));
            connector = JMXConnectorFactory.connect(serviceURL, env);
            connection = connector.getMBeanServerConnection();
            providerON = getAuthenticationProviderObjectName(MBEAN_INTERFACE);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
    public void close(){
        try {
            connector.close();
        } catch (IOException ioe) {
            throw new RuntimeException(ioe);
        }
    }
}

Please update the connect() method with your own credentials and port information if necessary.

Below is a sample java client that utilize this adapter:
package soadev.client;

import soadev.adapters.SQLAuthenticatorAdapter;

public class SQLAuthenticatorAdapterClient {
    private SQLAuthenticatorAdapter adapter = new SQLAuthenticatorAdapter();
    public static void main(String[] args) {
    SQLAuthenticatorAdapterClient client = new SQLAuthenticatorAdapterClient();
       try {
           client.connect();
            client.testCreateUser();
            client.testCreateGroup();
            client.testAddMemberTopGroup();
            client.close();
        } catch (Exception e) {
            // TODO: Add catch code
            client.close();
            e.printStackTrace();
        }
    }
    
    public void testCreateUser()throws Exception{
        String username = "pino";
        String password = "password1";
        String displayName = "Pino SOADEV";
        adapter.createUser(username, password, displayName);
    }
    
    public void testCreateGroup()throws Exception{
        String groupName = "SOADevGroup";
        String description = "This is a especial group created through SQLAuthenticatorAdapter";
        adapter.createGroup(groupName, description);
    }
    
    public void testAddMemberTopGroup()throws Exception{
        String username = "pino";
        String groupName = "SOADevGroup";
        adapter.addMemberToGroup(groupName, username);
    }
    
    public void close(){
        adapter.close();
    }
    public void connect(){
        adapter.connect();
    }
}

Be sure that "Weblogic 10.3 Thin-Client" is included in the libraries and classpath of your client project.

After running the client above, you will see that user "pino", group "SOADevGroup, and the assignment of user "pino" to group "SOADevGroup" were persisted respectively in the USERS, GROUPS, and GROUPMEMBERS tables.

Prototype

//TODO

Conclusion

This post demonstrates how easy it is to play with Weblogic API so that you have something as reference to jump-start your own implementation. More interfaces can be implemented by referring to the resources below. Be wary the examples above do not exemplify proper exception and concurrency handling.

Cheers!

Pino

Resources

Friday, April 9, 2010

ADF Security: SQLAuthenticator is Simply the Best!

When choosing authentication repositories, something that is more familiar can be the best choice. And what is that "something familiar" for us developers?- Of course, the relational database (unless, if you know LDAPs more :D). Relational databases (RDMS) has encryption capability while authentication providers supports cryptographic hashing, so what else could you ask?
Plus the recommendation below of an an expert, who is previously from Bea Weblogic Portal Team:
When it comes to Authentication repositories, my experience tells me that you are safest performance-wise with a database backed authentication store. While customers have certainly been successful with other types of authentication repositories, if you want to minimize risk, the database approach trumps all others.
-- Peter Laird, Architect for Tendril Networks
In our case, relational database is simply the choice. We need not only know - what roles our users have, but also what data they can access based on the organizations that were assigned to them (plus a lot more...).

In this post I will share the knowledge that I have acquired related to the best database-based authentication provider, the SQLAuthenticator.


Some tips on SQLAuthenticator to avoid being miserable :D :
  1. Stick as much as possible to the default schema. With the default schema, you need not worry tweaking the SQL select and insert statements defined on the SQLAuthenticator provider details. For your convenience, below is the script:
    CREATE TABLE USERS (
        U_NAME VARCHAR(200) NOT NULL,
        U_PASSWORD VARCHAR(50) NOT NULL,
        U_DESCRIPTION VARCHAR(1000))
    ;
    ALTER TABLE USERS
       ADD CONSTRAINT PK_USERS
       PRIMARY KEY (U_NAME)
    ;
    CREATE TABLE GROUPS (
        G_NAME VARCHAR(200) NOT NULL,
        G_DESCRIPTION VARCHAR(1000) NULL)
    ;
    ALTER TABLE GROUPS
       ADD CONSTRAINT PK_GROUPS
       PRIMARY KEY (G_NAME)
    ;
    CREATE TABLE GROUPMEMBERS (
        G_NAME VARCHAR(200) NOT NULL,
        G_MEMBER VARCHAR(200) NOT NULL)
    ;
    ALTER TABLE GROUPMEMBERS
       ADD CONSTRAINT PK_GROUPMEMS
       PRIMARY KEY (
          G_NAME, 
          G_MEMBER
       )
    ;
    ALTER TABLE GROUPMEMBERS
       ADD CONSTRAINT FK1_GROUPMEMBERS
       FOREIGN KEY ( G_NAME )
       REFERENCES GROUPS (G_NAME)
       ON DELETE CASCADE
    ;
    
  2. If tip# 1 is not possible, be wary that aside from Users, Groups can also be a member of a given Group. GROUPMEMBERS table is not simply a join table between Users and Groups but also can be a recursion between groups. I believe this is the reason why the default schema did not use surrogate keys (meaningless Ids). Imagine what will happen if you have a User with Long id 1 and Group with the same id 1, then what would the following record from the GROUPMEMBERS table mean?
    GROUP_ID          MEMBER_ID 
       2                 1
    
    Does above mean Group 1 is a member of Group 2?
    Or does it mean USer 1 is a member of Group2?

    Another thing to note - a Group Membership or Grants table implemented like the following script will NOT support membership of groups into other groups which defeats some aspects of weblogic authorization:

    CREATE TABLE JHS_USER_ROLE_GRANTS  
    (  
    ID NUMBER(*, 0) NOT NULL,  
    USR_ID NUMBER(*, 0) NOT NULL,  
    RLE_ID NUMBER(*, 0) NOT NULL  
    );
    

  3. Do not enable Plaintext Passwords. This is to ensure that users are created in the right process. You would not like to see your secret password. Do you?
  4. Be sure to set the "Group Membership Searching" to "limited" and set the "Max Group Membership Search Level" to a value like "5". This is to avoid infinite loop when the in situations where for example Group A is a member of Group B, while Group B is also a member of Group A.
  5. You do not need to change the identity store in Jdeveloper. jazn.xml is perfectly fine.
  6. Andrejus was right -you need not modify the role mapping in weblogic.xml. The default like below is perfectly fine:
    <?xml version = '1.0' encoding = 'windows-1252'?>
    <weblogic-web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                      xsi:schemaLocation="http://www.bea.com/ns/weblogic/weblogic-web-app http://www.bea.com/ns/weblogic/weblogic-web-app/1.0/weblogic-web-app.xsd"
                      xmlns="http://www.bea.com/ns/weblogic/weblogic-web-app">
      <security-role-assignment>
        <role-name>valid-users</role-name>
        <principal-name>users</principal-name>
      </security-role-assignment>
    </weblogic-web-app>
    
  7. For each application role you defined in jazn.xml, create an equivalent group in your SQLAuthenticator provider in weblogic console, and recreate those roles in the Enterprise Roles in jazn.xml. In jazn, make the corresponding enterprise role as member of the appropriate application role.

With these tips, I believe that you could already setup SQLAuthenticator easily. Given enough time, I am planning to consolidate the steps in other blogs to give a one-stop shop in configuring SQLAuthenticator.

In the next post, I will share how to play with weblogic APIs to access our security realm and to do tasks such adding user, letting user change password, listing users and roles, and more using Java (not the WLST):D

Kudus to Edwin Biemond for introducing to us the SQLAuthenticator!
Cheers!

Useful Resources