2.2. Security service

Security service makes a simple, unified way for the authentication and the storing/propagation of user sessions through all the eXo components and J2EE containers. JAAS is supposed to be the primary login mechanism but the Security Service framework should not prevent other (custom or standard) mechanisms from being used. You can learn more about JAAS in this tutorial.

Framework

The central point of this framework is the ConversationState object which stores all information about the state of the current user (very similar to the Session concept). The same ConversationState also stores acquired attributes of an Identity which is a set of principals to identify a user.

The ConversationState has definite lifetime. This object should be created when the user's identity becomes known by eXo (login procedure) and destroyed when the user leaves an eXo based application (logout procedure).

ConversationState and ConversationRegistry

The ConversationState can be stored:

  • In a static local thread variable; Or,

  • As a key-value pair in the ConversationRegistry component.

Either, or both methods can be used to set/retrieve the state at runtime. The most important thing is that they should be complementary, for example, make sure that the conversation state is set before you try to use it.

  • Local Thread Variable: Storing the ConversationState in a static local thread variable makes it possible to represent it as a context (current user's state).

    ConversationState.setCurrent(conversationState);
    
    ....
    ConversationState.getCurrent();
  • Key-Value way

    If you store the ConversationState inside the ConversationRegistry component as a set of key-value pairs, the session key is an arbitrary String (such as username, ticket id, httpSessionId).

    conversationRegistry.register("key", conversationState); 
    
    ...
    conversationRegistry.getState("key");
  • ConversationRegistry

    The ConversationRegistry is a mandatory component deployed into eXo Container as follows:

    
    <component>
        <type>org.exoplatform.services.security.ConversationRegistry</type>
    </component>

Authenticator

An Authenticator is responsible for Identity creation. It consists of two methods:

  • validateUser() accepts an array of credentials and returns userId (which can be something different from the username).

  • createIdentity() accepts userId and returns a newly created Identity object.

public interface Authenticator

{
   /**
    * Authenticate user and return userId which can be different to username.
    *
    * @param credentials - list of users credentials (such as name/password, X509
    * certificate etc)
    * @return userId the user's identifier.
    * @throws LoginException in case the authentication fails
    * @throws Exception if any exception occurs
    */
   String validateUser(Credential[] credentials) throws LoginException, Exception;
   /**
    * @param userId the user's identifier
    * @return returns the Identity representing the user
    * @throws Exception if any exception occurs
    */
   Identity createIdentity(String userId) throws Exception;
   /**
    * Gives the last exception that occurs while calling {@link #validateUser(Credential[])}. This
    * allows applications outside JAAS like UI to be able to know which exception occurs
    * while calling {@link #validateUser(Credential[])}.
    * @return the original Exception that occurs while calling {@link #validateUser(Credential[])}
    * for the very last time if an exception occurred, <code>null</code> otherwise.
    */
   Exception getLastExceptionOnValidateUser();
}

Depending on the application developer (and deployer), whether to use the Authenticator component(s) and how many implementations of this component should be deployed in eXo container. The developer is free to create an Identity object using a different way, but the Authenticator component is the highly recommended way from architectural considerations.

The typical functionality of the validateUser(Credential\[] credentials) method is comparison of incoming credentials (such as username/password, digest) with those credentials that are stored in an implementation specific database. Then, validateUser(Credential\[] credentials) returns userId or throws a LoginException in case of wrong credentials.

The default Authenticator implementation is org.exoplatform.services.organization.auth.OrganizationAuthenticatorImpl which compares incoming username/password credentials with the ones stored in OrganizationService. See the configuration example below:


<component>
  <key>org.exoplatform.services.security.Authenticator</key> 
  <type>org.exoplatform.services.organization.auth.OrganizationAuthenticatorImpl</type>
</component>

Usage

JAAS login module

The eXo Core framework described is not coupled with any authentication mechanism, but the most logical and implemented by default one is the JAAS login module. The typical sequence looks as follows (see org.exoplatform.services.security.jaas.DefaultLoginModule):

  • LoginModule.login() creates a list of credentials using standard JAAS Callbacks features, obtains an Authenticator instance, and creates an Identity object by calling the Authenticator.authenticate(..) method.

Authenticator authenticator = (Authenticator) container()

          .getComponentInstanceOfType(Authenticator.class); 
// RolesExtractor can be null     
RolesExtractor rolesExtractor = (RolesExtractor) container().
getComponentInstanceOfType(RolesExtractor.class);
Credential[] credentials = new Credential[] {new UsernameCredential(username), new PasswordCredential(password) };
String userId = authenticator.validateUser(credentials);
identity = authenticator.createIdentity(userId);
  • LoginModule.commit() obtains the IdentityRegistry object, and registers the identity using userId as a key.

When initializing the login module, you can set the singleLogin optional parameter. With this option, you can disallow the same Identity to log in at the same time.

By default, singleLogin is disabled, so the same identity can be registered more than once. This parameter passed in this form can be singleLogin=yes or singleLogin=true.

IdentityRegistry identityRegistry = (IdentityRegistry) getContainer().getComponentInstanceOfType(IdentityRegistry.class);

      
if (singleLogin && identityRegistry.getIdentity(identity.getUserId()) != null) 
  throw new LoginException("User " + identity.getUserId() + " already logined.");
identity.setSubject(subject);
identityRegistry.register(identity);

In case of using several LoginModules, JAAS allows placing the login() and commit() methods in different REQUIRED modules.

After that, the web application must use the SetCurrentIdentityFilter filter which obtains the ConversationRegistry object and tries to get the ConversationState by sessionId (HttpSession). If there is no ConversationState, SetCurrentIdentityFilter will create a new one, register it and set it as the current one using ConversationState.setCurrent(state).

  • LoginModule.logout() can be called by JAASConversationStateListener which extends ConversationStateListener.

This listener must be configured in web.xml. The sessionDestroyed(HttpSessionEvent) method is called by ServletContainer. This method removes ConversationState from ConversationRegistry ConversationRegistry.unregister(sesionId) and calls LoginModule.logout().

ConversationRegistry conversationRegistry = (ConversationRegistry) getContainer().getComponentInstanceOfType(ConversationRegistry.class);


ConversationState conversationState = conversationRegistry.unregister(sesionId);
if (conversationState != null) {
  log.info("Remove conversation state " + sesionId);
  if (conversationState.getAttribute(ConversationState.SUBJECT) != null) {
    Subject subject = (Subject) conversationState.getAttribute(ConversationState.SUBJECT); 
    LoginContext ctx = new LoginContext("exo-domain",  subject);
    ctx.logout();
} else {
  log.warn("Subject was not found in ConversationState attributes.");
}

Note

You can configure the SetCurrentIdentityFilter to re-inject the identity in case it is removed from IdentityRegistry. You should add restoreIdentity parameter to the filter configuration as follows:


<filter>
    <filter-name>SetCurrentIdentityFilter</filter-name>
    <filter-class>org.exoplatform.services.security.web.SetCurrentIdentityFilter</filter-class>
    <init-param>
        <param-name>restoreIdentity</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>

Predefined JAAS login modules

There are several JAAS Login modules included in the eXo Platform sources:

  • org.exoplatform.services.security.jaas.DefaultLoginModule which provides both authentication (using eXo Authenticator based mechanism) and authorization, filling Conversation Registry as described in the previous section. There are also several per-Application Server extensions of this login module in the org.exoplatform.services.security.jaas package, which can be used in appropriate AS. In particular, eXo has dedicated Login modules for Tomcat, JBoss, JOnAS and WebSphere.

  • Besides that, in case when the third-party authentication mechanism is required, org.exoplatform.services.security.jaas.IdentitySetLoginModule catches a login identity from the third-party "authenticating" login module and performs the eXo specific authorization job. In this case, the third-party login module has to put login (user) name to the shared state map under the "javax.security.auth.login.name" key and third-party LM has to be configured before IdentitySetLoginModule like:

    exo {
       com.third.party.LoginModuleImpl required;
       org.exoplatform.services.security.jaas.IdentitySetLoginModule required;
    };

J2EE container authentication

As you know, when a user in JAAS is authenticated, a Subject will be created. This Subject represents the authenticated user. It is important to know and follow the rules regarding Subject filling that are specific for each J2EE server, where eXo Platform is deployed.

To make it work in the particular J2EE server, it is necessary to add specific Principals/Credentials to the Subject to be propagated into the specific J2EE container implementation. The DefaultLoginModule is extended by overloading its commit() method with a dedicated logic, presently available for Tomcat, JBoss and JOnAS application servers.

Furthermore, you can use the optional RolesExtractor which is responsible for mapping primary Subject's principals (userId and a set of groups) to J2EE Roles:

public interface RolesExtractor {

  Set <String> extractRoles(String userId, Set<MembershipEntry> memberships);
}

This component may be used by Authenticator to create the Identity with a particular set of Roles.

Copyright ©. All rights reserved. eXo Platform SAS
blog comments powered byDisqus