Developing your own security providers is a relatively specialized task it is necessary only if WebLogic's default providers are insufficient. The majority of custom providers tend to change the default authentication or identity assertion mechanisms. The following sections provide an example of each of these. We recommend that you read WebLogic's well-documented security provider API to understand the life cycle of each provider if you intend to create your own. This information is supplied on the official web site, http://edocs.bea.com/wls/docs81/dvspisec/index.html. BEA's dev2dev web site (http://www.dev2dev.bea.com) also contains a number of example providers.
17.7.1 MBeans
WebLogic's provider architecture is MBean-based (see Chapter 20) if you are going to write a new provider, it has to have a corresponding MBean implementation. WebLogic provides tools for creating the necessary MBean deployment files and implementations. At runtime, the MBean representing your provider will be used to create an instance of your provider implementation the MBean is, in a sense, a factory for the provider implementation that you will have to supply. This, in turn, will use the MBean to read its configuration information. Any provider MBean must extend the appropriate base MBean type, supplied with WebLogic. To facilitate in the creation of these peripheral classes, WebLogic provides a few utilities. They are based around an MBean Definition File (MDF), an XML file that describes your MBean implementation. Example 17-5 lists such an XML file.
Example 17-5. A simple MDF (MyAuthentication.xml) for an authentication provider
Package = "com.oreilly.wlguide.security.iap" Extends = "weblogic.management.security.authentication.Authenticator" PersistPolicy = "OnUpdate"> Default = ""O'Reilly Authentication Provider"" />
If you are to implement your own authentication provider, copy Example 17-5 verbatim, changing only the lines that have been highlighted (unless you want to add additional attributes). The ProviderClassName MBean attribute is the most important. This indicates the class name that represents the custom provider that we will have to implement, in this case, com.oreilly.wlguide.security.iap.MyAuthenticationProviderImpl.
Once you have this file, you can use the MbeanMaker utility to take the MDF file and generate the MBean and stubs:
java -DcreateStubs="true" weblogic.management.commo.WebLogicMBeanMaker -MDF ..MyAuthentication.xml -files outdirectory
This generates a number of files, all of which can be ignored unless you implement custom operations and attributes. These are all placed in the outdirectory. All you need to do now is supply the actual implementation of the provider, recompile and repackage the whole lot, and deploy it.
Note that to use these utilities, you not only need to execute the setEnv script to prepare your environment, but you also have to add an extra JAR file to your classpath. This JAR file is located in WL_HOME/server/lib/mbeantypes/wlManagement.jar.
17.7.2 Authentication Provider
Now we turn to the authentication provider implementation itself. Recall that in the MDF, we specified the class name of a class that is to supply the authentication provider (see Example 17-6). This class must implement the weblogic.security.spi.AuthenticationProvider interface.
Example 17-6. An authentication provider implementation
package com.oreilly.wlguide.security.iap; import java.util.HashMap; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag; import weblogic.management.security.ProviderMBean; import weblogic.security.provider.PrincipalValidatorImpl; import weblogic.security.spi.*; public final class MyAuthenticationProviderImpl implements AuthenticationProvider { private String description; private LoginModuleControlFlag controlFlag; // Our mapping of users to passwords/groups, instead of being in LDAP or in a // database, is represented by a HashMap of MyUserDetails objects.. public class MyUserDetails { String pw; String group; // We use this to represent the user's groups and passwords public MyUserDetails(String pw, String group) { this.pw=pw; this.group = group; } public String getPassword( ) {return pw;} public String getGroup( ) {return group;} } // This is our database private HashMap userGroupMapping = null; public void initialize(ProviderMBean mbean, SecurityServices services) { MyAuthenticatorMBean myMBean = (MyAuthenticatorMBean)mbean; description = myMBean.getDescription( ) + " " + myMBean.getVersion( ); System.err.println("#In realm:" + myMBean.getRealm( ).wls_getDisplayName( )); // We would typically use the realm name to find the database // we want to use for authentication. Here, we just create one. userGroupMapping = new HashMap( ); userGroupMapping.put("a", new MyUserDetails("passworda", "g1")); userGroupMapping.put("b", new MyUserDetails("passwordb", "g2")); userGroupMapping.put("system", new MyUserDetails("12341234", "Administrators")); String flag = myMBean.getControlFlag( ); if (flag.equalsIgnoreCase("REQUIRED")) { controlFlag = LoginModuleControlFlag.REQUIRED; } else if (flag.equalsIgnoreCase("OPTIONAL")) { controlFlag = LoginModuleControlFlag.OPTIONAL; } else if (flag.equalsIgnoreCase("REQUISITE")) { controlFlag = LoginModuleControlFlag.REQUISITE; } else if (flag.equalsIgnoreCase("SUFFICIENT")) { controlFlag = LoginModuleControlFlag.SUFFICIENT; } else { throw new IllegalArgumentException("Invalid control flag " + flag); } } public AppConfigurationEntry getLoginModuleConfiguration( ) { HashMap options = new HashMap( ); options.put("usermap", userGroupMapping); return new AppConfigurationEntry( "com.oreilly.wlguide.security.provider.MyLoginModuleImpl", controlFlag, options ); } public String getDescription( ) { return description; } public PrincipalValidator getPrincipalValidator( ) { return new PrincipalValidatorImpl( ); } public AppConfigurationEntry getAssertionModuleConfiguration( ) { return null; } public IdentityAsserter getIdentityAsserter( ) { return null; } public void shutdown( ) {} }
The class simply provides a way of getting to the various modules, most of which are optional. For example, we do not provide an identity asserter, so we just return null. Likewise, we simply return WebLogic's default principal validator implementation as the principal validator. All of the action happens in the initialize( ) and getLoginModuleConfiguration( ) methods.
Typically, the job of the initialize( ) method is to set up the resources necessary to perform any authentication. As you can see from the implementation, it has direct access to the MBean so, if you added extra attributes or operations to the MBean, they can be used here. In our case, we simply set up a local database in the form of a hashmap. It then stores the control flag that is used in the module configuration. This eventually will get passed to the login module.
The getLoginModuleConfiguration( ) method returns an object that encapsulates the login module the final piece of functionality that we have to implement. You can think of this method as playing the same role as the JAAS configuration file. It is instructing the provider as to which login module to use, on the server side this time. Note that the login module is wrapped in an object (AppConfigurationEntry) that also captures the control flag and options.
17.7.3 Login Module
We now need to supply a login module, as shown in Example 17-7. This follows the standard JAAS API.
Example 17-7. A login module
package com.oreilly.wlguide.security.provider; import java.io.IOException; import java.util.*; import javax.security.auth.Subject; import javax.security.auth.callback.*; import javax.security.auth.login.*; import javax.security.auth.spi.LoginModule; import weblogic.security.principal.WLSGroupImpl; import weblogic.security.principal.WLSUserImpl; /** * This login module will be called by our Authentication Provider. * It assumes that the option, usermap, will be passed which contains * the map of users to passwords and groups. */ public class MyLoginModuleImpl implements LoginModule { private Subject subject; private CallbackHandler callbackHandler; private HashMap userMap; // Authentication status private boolean loginSucceeded; private boolean principalsInSubject; private Vector principalsBeforeCommit = new Vector( ); public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { this.subject = subject; this.callbackHandler = callbackHandler; // Fetch user/password map that should be set by the authenticator userMap = (HashMap) options.get("usermap"); } /* Called once after initialize to try and log the person in */ public boolean login( ) throws LoginException { // First thing we do is create an array of callbacks so that // we can get the data from the user Callback[] callbacks; callbacks = new Callback[2]; callbacks[0] = new NameCallback("username: "); callbacks[1] = new PasswordCallback("password: ", false); try { callbackHandler.handle(callbacks); } catch (IOException eio) { throw new LoginException(eio.toString( )); } catch (UnsupportedCallbackException eu) { throw new LoginException(eu.toString( )); } String username = ((NameCallback) callbacks[0]).getName( ); char [] pw = ((PasswordCallback) callbacks[1]).getPassword( ); String password = new String(pw); if (username.length( ) > 0) { if (!userMap.containsKey(username)) throw new FailedLoginException("Authentication Failed: Could not find user: " + username); String realPassword = ((MyAuthenticationProviderImpl.MyUserDetails) userMap. get(username)).getPassword( ); if (realPassword == null || !realPassword.equals(password)) throw new FailedLoginException("Authentication Failed: Password incorrect for user" + username); } else { // No Username, so anonymous access is being attempted } loginSucceeded = true; // We collect some principals that we would like to add to the user // once this is committed. // First, we add his username itself principalsBeforeCommit.add(new WLSUserImpl(username)); // Now we add his group principalsBeforeCommit.add(new WLSGroupImpl(((MyAuthenticationProviderImpl. MyUserDetails)userMap.get(username)).getGroup( ))); return loginSucceeded; } public boolean commit( ) throws LoginException { if (loginSucceeded) { subject.getPrincipals( ).removeAll(principalsBeforeCommit); principalsInSubject = true; return true; } else { return false; } } public boolean abort( ) throws LoginException { if (principalsInSubject) { subject.getPrincipals( ).removeAll(principalsBeforeCommit); principalsInSubject = false; } return true; } public boolean logout( ) throws LoginException { return true; } }
Although long, this is pretty straightforward. Let's go through it. The initialize( ) method has direct access to the options that were configured in the provider. As we put our database in the options, this method provides the ideal place to extract the database. The rest of the action occurs in a combination of the login( ), commit( ), and abort( ) methods. If a login succeeds, and the control flags of the login modules are such that the entire login is to commit, the commit( ) method will be called otherwise, the abort( ) method will be called. These methods simply ensure that the principals that should be associated with the subject are placed into the subject. The login( ) method does all of the work. First, it sets up a number of callbacks we need the username and password. Note that the actual callback implementation is going to be handled by WebLogic. For example, WebLogic prompts you for the system user credentials when you try and boot a WebLogic server or access the Administration Console. It is the data from these callbacks that eventually will be supplied to the login method. After handling the callbacks, we extract the username and password of the user and locate it in our database. If successful, we make a list of principals that we want to associate with the user, storing these in the variable principalsBeforeCommit. The principals are added to the subject only if WebLogic calls the commit( ) method.
17.7.4 Deploying the Provider
Once you have created the authentication provider and login module, you can package these together with the generated stub and MBI files. To do this, execute the following command:
java weblogic.management.commo.WebLogicMBeanMaker -MJF myAuth.jar -files .
You are now ready to deploy your new provider. Copy myAuth.jar to the WL_HOME/server/lib/mbeantypes directory, and then reboot the server. Note that all custom providers have to be located in this directory. Start up the Administration Console and navigate to the Security/myrealm/Providers/Authentication node. In the list of available authenticators and identity asserters, you should find an option for "Configure a new My Authenticator." Selecting this option and clicking Create will configure the authenticator. On the following tab, you will notice that you can change the control flag. If you change this to something such as requisite, make sure that your database has a user in the Administrators group. If not, you won't even be able to boot the server! Use the OPTIONAL flag during development to avoid these problems.
Introduction
Web Applications
Managing the Web Server
Using JNDI and RMI
JDBC
Transactions
J2EE Connectors
JMS
JavaMail
Using EJBs
Using CMP and EJB QL
Packaging and Deployment
Managing Domains
Clustering
Performance, Monitoring, and Tuning
SSL
Security
XML
Web Services
JMX
Logging and Internationalization
SNMP