Custom JAAS LoginModule

Fortunately, LoginModule uses a standard JAAS API and as such is well documented in many books and on the Internet. Here, we will write the simplest LoginModule that solves our problem of validating the principals over a legacy external SSO system using the HTTP protocol. As a didactical support, we will also write in the log when the Security Services container will call our method so that we can figure out when and how many times they are called.

Keep in mind that LoginModule is a stateful Bean; it must retain configuration data when it is initialized, and from the login callback state to the commit state (or abort or whatever) it must keep the state to answer in a correct and expected way.

Let's start with the definition; the instance fields will be declared as and when we need them. The code for our custom LoginModule is as follows:

public class PacktLoginModuleImpl implements LoginModule {
  private final static Logger LOGGER = Logger.
    getLogger(PacktLoginModuleImpl.class.getSimpleName());

    private Subject subject;
    private CallbackHandler callbackHandler;
   private String url;

    public void
    initialize( Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options )
{
        LOGGER.info("PacktLoginModuleImpl.initialize");

        this.subject = subject;
        this.callbackHandler = callbackHandler;
        this.url = options.get("url").toString();
    }

Here we simply print the log information saying that a new instance of LoginModule has been created, and save the information needed for the authentication process, explained as follows:

  • subject: This is the current subject that we will enrich with our principals in case of a successful login
  • callbackhandler: This is our official supplier of credentials to authenticate the user (in this example, we will use the canonical couple—the username and password combination)
  • url: This is our very own configuration, the legacy SSO system URL

The login() method

After we have correctly initialized the object, we can expect the container to call the login() method to give us a chance of authentication. Use the following code:

public boolean login() throws LoginException {
  LOGGER.fine("PacktLoginModuleImpl.login");

Like we said, we decided to log all our method calls in this LoginModule, but the first real thing that we need to do is to define which Callback methods will give us the correct information to accept or decline user identity, as shown in the following code snippet:

    Callback[] callbacks = new Callback[]{
            new NameCallback("username: "),
            new PasswordCallback("password: ", false)
    };

    try {
        callbackHandler.handle(callbacks);
    } catch (Exception e) {
        LOGGER.throwing("PacktLoginModuleImpl", "login", e);
        throw new LoginException(e.getMessage());
    }

Here we define two Callback methods, one that will ask the system for a username and another for the password, and then we use the callbackHandler interface that we saved during the initialization state to populate them, so that we can then extract what we need, as shown in the following code:

    PasswordCallback passwordCallback = (PasswordCallback) callbacks[1];
    char[] passwordChars = passwordCallback.getPassword();
    passwordCallback.clearPassword();
    final String password = new String(passwordChars);

Here, we clear the PasswordCallback internal state after we get the password because it is always a best practice to not store sensitive information in the heap (our variables are all young generation and will be garbaged after the method call).

Having retrieved the username and password, we can now call the legacy system as shown in the following code:

if (userName != null && password != null && userName.length() > 0 && password.length() > 0) 
{
        checkUsernameAndPassword(userName, password);
    } else {
        throw new LoginException("username and/or password cannot be null");
    }

We launch LoginException if our necessary credentials are null or if the SSO system cannot validate our user. If everything is right we can declare a successful login and save our principals for later use, as shown in the following code snippet:

    loginSucceeded = true;

    principalsForSubject.add(new WLSUserImpl(userName));
    principalsForSubject.add(new WLSGroupImpl("Packt"));

    return loginSucceeded;
}

Here principalsForSubject and loginSucceeded are two fields declared in the following manner:

private boolean loginSucceeded;
private List<Principal> principalsForSubject = new ArrayList<Principal>();

Lifecycle methods – commit(), abort(), and logout()

Now, we need to implement the remaining commit, abort, and logout methods, taking care of the behavior expected by the client of the LoginModule interface (you can read more about this at http://docs.oracle.com/javase/6/docs/technotes/guides/security/jaas/JAASLMDevGuide.html#commit).

The expected external behavior for the commit method is as follows:

login()/commit()

Success

Failure

Success

return true

throw exception

Failure

return false

return false

We saved the result of the login() method in loginSucceeded. We must always return false in case of a failed login and return true otherwise (this simple method can't fail in the case of a successful login, so we remove throw LoginException from the method signature), as shown in the following code snippet:

@Override
public boolean commit() {
    LOGGER.info("PacktLoginModuleImpl.commit");
    if (loginSucceeded) {
        subject.getPrincipals().addAll(principalsForSubject);
        principalsInSubject = true;
        return true;
    } else {
        return false;
    }
}

We need to save the fact that the commit method was successful in the principalsInSubject local field so that we can react appropriately in case of of abort().

For the abort() method we must distinguish when it's called after a successful login, as shown in the following table:

commit()/abort()

Success

Failure

Success

return true

throw exception

Failure

return true

throw exception

Or after an unsuccessful login, as follows:

commit()/abort()

Success

Failure

Success

return false

return false

Failure

return false

return false

The following code shows a simple implementation considering that abort() itself can't fail:

@Override
public boolean abort() {
    LOGGER.info("PacktLoginModuleImpl.abort");
    if( !loginSucceeded ) {
      return false;
   }
   if (principalsInSubject) {
        subject.getPrincipals().
        removeAll(principalsForSubject);
        principalsInSubject = false;
    }
    return true;
}

Finally, we need to implement logout() as shown in the following code snippet:

@Override
public boolean logout() throws LoginException {
  LOGGER.info("PacktLoginModuleImpl.logout");
  if( principalsInSubject ) {
    if( !subject.isReadOnly() ) {
      subject.getPrincipals().removeAll(principalsForSubject);
    } else {
      for(Principal principal: principalsForSubject ) {
        if( principal instanceof Destroyable ) {
           try {
              ((Destroyable)principal).destroy();
            } catch (DestroyFailedException e) {
              LOGGER. throwing("PacktLoginModuleImpl", "logout", e);
               throw new LoginException("cannot destroy principal " + principal.getName());
            }
         } else {
         	throw new LoginException("cannot destroy principal "+principal.getName());
         }
      }
    }
  }
  return true;
}

As we can see in the previous code, the implementation must remove or destroy the principals inside the associated Subject objects that were created by this LoginModule, while taking care not to touch any principals added by other modules, and eventually signaling its failure of clearing them with a LoginException exception.

We can do a clean-install again now and then our Authentication Provider is ready for use.

A simple SSO JSP

Before we can run our Provider, we need to emulate the legacy SSO system. We do this now with a simple JSP, as shown in the following code snippet:

<%@ page contentType="text/plain;charset=UTF-8" language="java" %><%
    if( "testuser".equals(request.getParameter("username")) &&
   "testpassword".equals(request.getParameter("password"))) {
        response.setStatus(200);
    } else {
        response.setStatus(401);
    }
%>

For this example, we can deploy it inside the webapps/ROOT folder of any Tomcat server running on port 8080. In case you use another port (or to add a context-path), the authentication.services.url property must be updated.

Running the provider

We can now run the provider calling the servlet that we wrote in the chapter3-web folder by using http://localhost:7001/chapter3-web/myprotectedresource.

We will be asked for the username and password, and after entering testuser and testpassword respectively we will receive an error telling us that the current user does not have enough privileges to execute the EJB method. The following error message will be displayed:

[EJB:010160]Security violation: User testuser has insufficient permission to access EJB type=<ejb>, application=chapter3-ear, module=/chapter3-web, ejb=NoInterfaceBeanInWarModule, method=echo, methodInterface=Local, signature={java.lang.String}.

This is not an error but a desired effect; it is just that our testuser is not entitled to call the EJB method. In case you want to see an execution working well you can simply comment out the execution of the EJB in the servlet, as shown in the following code:

//resp.getWriter().println("echo:"+service.echo("echo"));

Alternatively, modify the EJB so that this user is powerful enough to execute it. It is a good exercise to map a new role to the group that is associated with this user.

If now we look at the WebLogic's log, we can see that our lifecycle methods have been correctly called in the order we expected:

INFO: PacktLoginModuleImpl.initialize
INFO: PacktLoginModuleImpl.login
INFO: PacktLoginModuleImpl.commit
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.220.22