3 Replies Latest reply on Sep 26, 2006 8:12 AM by daviddpark

    How to marshall principal/credentials from client login to s

    daviddpark

      I am attempting to implement java security in an application that is architected as follows:

      We have a web tier running in a jboss instance. This web tier has no datasources available to it. The only way it has access to data is through a business tier, a set of applications running in another jboss instance. For simplicity, we will refer to the two instances as web-1 and service-1.

      To complicate things further in our system, we require three bits of information to uniquely identify a user. The loginid, the password and the domain. In our system, we enforce unique loginids in each domain, but they all live relatively harmoniously in the same database tables. The loginid in and of itself will not necessarily be unique, but coupled with the domain, we could have a primary key, although the real primary key of the user table is a userid column, which is what we REALLY want as our principal once we get into the business tier. (We allow our customers to choose their own loginid, but the userid is system generated)

      Ok, so having read Chapter 8 of the admin guide 23 times, I understand that there will be two parts of the JAAS login process.

      I first attempted to use the ClientLoginModule that JBoss provides to marshall the login credentials from the web layer to the service layer. We are instantiating the LoginContext with "other", which is mapped to ClientLoginModule in login-config.xml. (The service layer ejbs are all part of the same security-domain, which we'll refer to as "jwdomain")

      Here is my login-config.xml snippet:


       <application-policy name="jwdomain">
       <authentication>
       <login-module code = "my.package.MyLoginModule" flag = "required">
       <module-option name="unauthenticatedIdentity">guest</module-option>
       <module-option name="password-stacking">useFirstPass</module-option>
       <module-option name="multi-threaded">true</module-option>
       </login-module>
       <login-module code="org.jboss.security.ClientLoginModule" flag="required">
       <module-option name="password-stacking">useFirstPass</module-option>
       <module-option name="multi-threaded">true</module-option>
       </login-module>
       </authentication>
       </application-policy>
       <application-policy name = "other">
       <authentication>
       <login-module code="org.jboss.security.ClientLoginModule" flag="required">
       <module-option name="unauthenticatedIdentity">guest</module-option>
       <module-option name="multi-threaded">true</module-option>
       </login-module>
       </authentication>
       </application-policy>
      


      And a snippet from my login action in the web layer:
      (Since unauthenticated I can't yet look up the userid, I get creative and construct a unique "username" from loginid, a string of text that we hope nobody ever uses as a loginid or domain, and the actual domain strings.)

      ProcessLoginAction.java snippet:

      CallbackHandler cbHandler = new UsernamePasswordHandler
       (new StringBuffer(loginid).append("UNIQUETOKEN").append(domain).toString(), password.toCharArray());
      LoginContext loginContext;
      try {
       loginContext = new LoginContext("other",cbHandler);
       loginContext.login();
      } catch (LoginException e) {
       throw new ApplicationException(e);
      }
      


      Stepping through the executing code, things seem to progress nicely. My ProcessLoginAction class instantiates the "other" LoginContext, and by the time the commit() method is called, we can see that the Subject is populated with Principal and Credentials, as expected from what is passed in via the CallbackHandler.

      After the LoginContext.login() succeeds, we invoke the create() methot on an EJB that has a) been set to unchecked method-permission and b) security-domain of "jwdomain" (See jboss.xml and ejb-jar.xml snippets below) and the server side of the JAAS login proceeds as planned, as we step through MyLoginModule. By the time we've gone through the login() and commit() method, we've done actual authentication on the credentials marshalled to the service login invocation via the client side login. So far, so good.

      MyLoginModule.java:

      public class MyLoginModule extends AbstractServerLoginModule {
       private Principal csIdentity;
       public boolean login() throws LoginException {
       String userid = null;
       Callback[] callbacks = new Callback[2];
       callbacks[0] = new NameCallback("loginid+domain should have been provided by web layer");
       callbacks[1] = new PasswordCallback("Password should have been provided by web layer",false);
       String ltd = null;
       String password = null;
       try {
       callbackHandler.handle(callbacks);
       ltd = ((NameCallback)callbacks[0]).getName();
       if (((PasswordCallback)callbacks[1]).getPassword() != null) {
       password = new String(((PasswordCallback)callbacks[1]).getPassword());
       }
       } catch (Exception e) {
       throw new LoginException(e.getMessage());
       }
       try {
       if (ltd != null && password != null) {
       String loginid = null;
       String domain = null;
       String token = "UNIQUETOKEN";
       String[] loginiddomain = ltd.split(token);
       if ( loginiddomain.length == 2 ) {
       loginid = loginiddomain[0];
       domain = loginiddomain[1];
       }
       userid = getUserid(loginid,domain);
       if(userid == null ) {
       throw new LoginException("Unable to determine unique identifier for "+loginid+"/"+assoc+".");
       }
       String expectedPassword = getPassword(userid);
       if(password != null && !password.equals(expectedPassword)) {
       throw new LoginException("Passwords do not match.");
       }
       csIdentity = createIdentity(userid);
       loginOk = true;
       }
       } catch (Exception e) {
       throw new LoginException(e.getMessage());
       }
       return super.login();
       }
       protected String getUserid(String loginid, String association) {
       //implementation details not needed
       }
       protected String getPassword(String userid) {
       //implementation details not needed
       }
       public Principal getIdentity() {
       return csIdentity;
       }
       protected Group[] getRoleSets() throws LoginException {
       Group roles = new SimpleGroup("Roles");
       //actual implementation to follow after successful test
       Principal role = new SimplePrincipal("foorole");
       roles.addMember(csIdentity);
       roles.addMember(role);
       Group cp = new SimpleGroup("CallerPrincipal");
       cp.addMember(csIdentity);
       return new Group[]{roles,cp};
       }
      }
      


      The server side login() succeeds, stepping through the above code shows success, but the subsequent EJB create() fails with:

      Insufficient method permissions, principal=myLoginidUNIQUETOKENmyDomain, ejbName=AccountManager, method=create, interface=HOME, requiredRoles=[], principalRoles=null
      


      Now this failure happened while the ProcessLoginAction attempted to lookup and call a method on an EJB in the service layer, secured by the "jwdomain" as follows:

      <jboss>
       <security-domain>java:/jaas/jwdomain</security-domain>
       <enterprise-beans>
       <session>
       <ejb-name>AccountManager</ejb-name>
       <jndi-name>AccountManagerManager</jndi-name>
       <local-jndi-name>AccountManagerManagerLocal</local-jndi-name>
       </session>
      ...
      </jboss>
      

      But ALL of my session bean methods are flagged as unchecked! (for now)
      <ejb-jar >
      
       <description>AccountManager to retrieve info from service tier</description>
       <enterprise-beans>
       <session >
       <ejb-name>AccountManager</ejb-name>
       ...
       <session-type>Stateless</session-type>
       <transaction-type>Bean</transaction-type>
       <security-identity>
       <use-caller-identity />
       </security-identity>
       </session>
      ...
       <method-permission >
       <unchecked/>
       <method >
       <ejb-name>AccountManager</ejb-name>
       <method-intf>Remote</method-intf>
       <method-name>create</method-name>
       <method-params>
       </method-params>
       </method>
       </method-permission>
      ...
      </ejb-jar >
      


      So, I thought that maybe there's something awry between requiredRoles=[] and principalRoles=null.... that maybe AT LEAST ONE role is expected. So I try to force this to happen... I try it two ways:

      The first way, I changed my login-config.xml as follows:

       <application-policy name = "other">
       <authentication>
       <login-module code="org.jboss.security.auth.spi.RunAsLoginModule" flag="required">
       <module-option name="roleName">foo-admin</module-option>
       </login-module>
       <login-module code="org.jboss.security.ClientLoginModule" flag="required">
       <module-option name="unauthenticatedIdentity">guest</module-option>
       <module-option name="multi-threaded">true</module-option>
       </login-module>
       </authentication>
       </application-policy>
      


      That didn't help me at all. So, I changed ClientLoginModule to a custom client side login module that extends UsernamePasswordLoginModule so that I could specify a dummy role via the getRoleSets() method, overriding the validatePasswords() method as well so that it would always return true (after all, it's a client side login module that doesn't _really_ provide any meaningful authentication... the sole purpose of this login module is to marshall the Subject Principal and credentials to the service layer where the real business is transacted. So, I implement a getRoleSets() method as follows:

      public class MyClientLoginModule extends UsernamePasswordLoginModule {
       String password = "";
       public String getUsersPassword() { return password; }
       protected boolean validatePassword(String inputPassword, String expectedPassword) { return true; }
       protected Group[] getRoleSets() throws LoginException {
       Principal identity = super.getIdentity();
       Group roles = new SimpleGroup("Roles");
       Principal role = new SimplePrincipal("foorole");
       roles.addMember(identity);
       roles.addMember(role);
       return new Group[]{roles};
       }
      }
      


      Straight up, right? When I execute this code, it all _looks_ good, but still no dice, same error. Maybe I'm missing something in the way Subject<->Principal<->Principals<->Groups<->Roles all _really_ work together. It seems that if I can just get a bogus role associated with my client side Subject then maybe the EJB create() would work?

      Then again, maybe I'm just barking up the wrong tree, because what I really want for my principal is not that bastardized concatenization of loginid, unique token and domain, but the actual system primary key user id, that which the server side MyLoginModule identity identifies once a LoginModule is executing in a place where the customer provided loginid/domain/password can be properly authenticated and turned into a proper Principal (rather CallerPrincipal, what I am ultimately after)

      If anything glaringly obvious jumps off the page, please enlgihten me, as I simply cannot see the forest through the trees.

      Thanks in advance,
      -david


        • 1. Re: How to marshall principal/credentials from client login
          markash

          Good Day,

          Why don't you do something similar to how Kerberos represents the username with domain information? For example,

          user@domain

          Your callbackhandler can take care of putting the two together and your login module on the server can seperate the two parts.

          Regards,
          Mark P Ashworth

          • 2. Re: How to marshall principal/credentials from client login
            daviddpark

            I originally did not use "@" as my UNIQUETOKEN because logins can be email addresses, but there's surely nothing wrong with splitting on lastIndexOf("@") rather than String.split()...

            Although a good idea, this doesn't help me with my particular problem. It matters not how the two strings are concatenated if they are not marshalled through to the server side JAAS login module.

            • 3. Re: How to marshall principal/credentials from client login
              daviddpark

              I found a solution to my problem. I was using xdoclet/ejbdoclet to generate my session interfaces as well as the ejb-jar.xml files for my beans. It turns out that when I annotated the create() methods, I didn't allow for method-permission declarations on the remote HOME method.

              The proper annotation would look something like:

               /**
               * @ejb.create-method
               * @ejb.permission unchecked="true" method-intf="Home,LocalHome"
               */
               public void ejbCreate() {
               }
              


              And the resulting ejb-jar.xml would have something like:

               <method-permission >
               <description><![CDATA[description not supported yet by ejbdoclet]]></description>
               <unchecked/>
               <method >
               <description><![CDATA[]]></description>
               <ejb-name>MyBeanName</ejb-name>
               <method-intf>LocalHome</method-intf>
               <method-name>create</method-name>
               <method-params>
               </method-params>
               </method>
               </method-permission>
               <method-permission >
               <description><![CDATA[description not supported yet by ejbdoclet]]></description>
               <unchecked/>
               <method >
               <description><![CDATA[]]></description>
               <ejb-name>MyBeanName</ejb-name>
               <method-intf>Home</method-intf>
               <method-name>create</method-name>
               <method-params>
               </method-params>
               </method>
               </method-permission>