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.
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>
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>
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.");
}
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>
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; };
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.