Example: Authentication and Authorization

Version 11

    This topic provides an overview of common customizations for version 2.0 and later. You'll find code samples and common strategies for integrating with third-party authentication systems (integration commonly referred to as single sign-on, or SSO).

     

    Note: This content is valid only up to version 4.0.5.

     

    Note: You'll find the code described in this topic in our public Subversion repository.

    Common Customization Patterns

    Jive customers have commonly made several kinds of customizations. Generally, these customizations are of the following kinds:

    1. Authenticate a principal. This is commonly seen in several incarnations:         
      • Reading HTTP headers on the incoming request to authenticate a user. Common examples of this include Basic Authentication headers or SAMLResponse tokens.
      • Direct integration with a J2EE application server to load user information.
    2. Synchronize user profile data with an external identity provider. For example, this commonly includes web services calls to a system of record, reading SAML assertion values and calling third-party APIs to retrieve user data. In all cases, you should perform user profile synchronization at the point of authentication. This simplifies code and configuration, providing a consistent user experience with up-to-date profile data.
    3. Customizing group membership for authorization purposes. Similar to synchronizing user profile data, this commonly involves either invoking a remote service to retrieve group membership information, or reading values associated with the incoming HTTP request.

     

    Depending on your requirements, you can exclude any of these steps. For example, if your deployment is not intended to integrate with a centralized group management system, step three is unnecessary. Likewise, if you're using the application to manage all user profile information, step two above may not be necessary.

    Customization Via Filter

    The recommended way to perform any of the operations described above is to implement a J2EE Filter. A filter commonly:

    1. Is configured as a Spring-managed resource. As such, the filter can expose setter methods for any Jive Spring resource, including the UserManager and GroupManager implementations.
    2. Replaces the formAuthenticationFilter bean defined in the default installation. The formAuthenticationFilter processes username/password authentication. Commonly, customized solutions wish to disable form-based authentication. As such, customizations replace the formAuthenticationFilter bean with a custom-defined filter managed as a Spring bean.
    3. Authenticates unauthenticated requests. Filters processing requests in the security layer of the system must check criteria of the incoming request when the SecurityContext indicates that the current authentication is null.

     

    The following XML fragment demonstrates how to define a Spring-managed Filter and replace the default formAuthenticationFilter bean with the newly defined filter:

     

    <!--  Alter the default mapping chains, switching formAuthenticationFilter
        with the federatedIdentityAuthFilter defined below. This definition uses the federated identity
        filter for normal app requests and Web Service requests, but leaves the form mechanism in
        place for admin and upgrade purposes. The desired behavior will vary depending on the
        application. -->
    <bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
        <property name="filterInvocationDefinitionSource">
            <value>
                <!-- The URL mappings here are broken for readability, but
                    in your XML file they must be unbroken. -->
         CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
         PATTERN_TYPE_APACHE_ANT
         /upgrade/**=httpSessionContextIntegrationFilter, upgradeAuthenticationFilter, \
             upgradeExceptionTranslationFilter, jiveAuthenticationTranslationFilter
         /post-upgrade/**=httpSessionContextIntegrationFilter, postUpgradeAuthenticationFilter, \
             postUpgradeExceptionTranslationFilter,jiveAuthenticationTranslationFilter
         /admin/**=httpSessionContextIntegrationFilter, adminAuthenticationFilter, \
             adminExceptionTranslationFilter,jiveAuthenticationTranslationFilter
         /rpc/xmlrpc=wsRequireSSLFilter, httpSessionContextIntegrationFilter, federatedIdentityAuthFilter, \
             wsExceptionTranslator, jiveAuthenticationTranslationFilter, wsAccessTypeCheckFilter
         /rpc/rest/**=wsRequireSSLFilter, httpSessionContextIntegrationFilter, federatedIdentityAuthFilter, \
             wsExceptionTranslator, jiveAuthenticationTranslationFilter, wsAccessTypeCheckFilter
         /rpc/soap/**=wsRequireSSLFilter, httpSessionContextIntegrationFilter, federatedIdentityAuthFilter, \
             jiveAuthenticationTranslationFilter
         /**=httpSessionContextIntegrationFilter, federatedIdentityAuthFilter, \
             jiveAuthenticationTranslationFilter
            </value>
        </property>
    </bean>
    
    <!--  Define a new filter for externally-managed users. Inject dependencies as needed. -->
    <bean id="federatedIdentityAuthFilter"
        class="com.jivesoftware.plugins.aaa.FederatedIdentityAuthFilter">
        <property name="userManager" ref="userManagerImpl"/>
        <property name="groupManager" ref="groupManagerImpl"/>
        <property name="userAgent" ref="agent"/>
        <property name="groupAgent" ref="agent"/>
    </bean>
    

    Using Plugins

    As of version 2.5, you can include J2EE Filters in the Jive Spring context by using a plugin. In this way, you can develop custom authentication solutions for without changing the contents of its WAR file or changing the application server class path.

    Sample SSO Plugin

    The following code excerpts are taken from a sample version 2.5 plugin you'll find on the public Jive Subversion repository. The excerpts demonstrate a working (although contrived) implementation of the common customization objectives described above.

    Highlights from the sample code include:

    • spring.xml - The Spring definition of the filter used for authentication and its dependencies, including the APIs used to load user information from an incoming request.
    • com.jivesoftware.plugins.aaa.FederatedIdentityAuthFilter — This class defines common SSO-related behavior such as creation of local user representation when a user does not already exist. Generally, SSO implementations will not need to change this class.
    • com.jivesoftware.plugins.aaa.IdentityProviderUserAgent — A micro-API to be implemented by SSO customizations. the application uses implementations of this interface to extract user information from the incoming request. The FederatedIdentityAuthFilter uses an implementation of this interface to load user information specific to the SSO implementation. SSO implementations must create implementations specific to their requirements and configure their FederatedIdentityAuthFilter bean to use the implementation of this interface.
    • com.jivesoftware.plugins.aaa.IdentityProviderGroupAgent — A second micro-API to optionally be implemented by SSO customizations if they desire to synchronize user-to-group information with an external system, or based on data in the incoming HTTP request (with SAML assertions for example). Implementing this interface is normal and instances of the FederatedItentityAuthFilter will operate normally when a group agent is not configured.
    • com.jivesoftware.plugins.aaa.SampleAgent — A sample implementation of the user and group agents described above. The sample implementation demonstrates how to create an instance of a Jive User object via the UserTemplate class. If deployed in a test environment, this agent will authenticate any request with the doAuthenticate parameter set on the incoming request as demonstrated in the extractUserFromRequest method.

    As with any code sample, the following may be out of date. Be sure to view the code in the Jive public Subversion repository.

     

    Note that while the sample is delivered as a plugin, the code and patterns in the plugin are equally valid for versions after 2.0 and before 2.5. The main difference being older versions require manipulation of the classpath or WAR artifact to deploy the code.

    spring.xml from Sample

    <?xml version="1.0" encoding="UTF-8"?>
    
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xmlns:util="http://www.springframework.org/schema/util"
        xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:dwr="http://www.directwebremoting.org/schema/spring-dwr"
        xmlns:cxf="http://cxf.apache.org/core"
        xmlns:jaxws="http://cxf.apache.org/jaxws"
        xsi:schemaLocation="
            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
            http://www.directwebremoting.org/schema/spring-dwr http://www.directwebremoting.org/schema/spring-dwr-2.0.xsd
            http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
            http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd"        
                default-autowire="no">
    
        <!-- Alter the default mapping chains, switching formAuthenticationFilter
            with the federatedIdentityAuthFilter defined below. This definition uses the federated
            identity filter for normal app requests and web service requests, but leaves the form
            mechanism in place for admin and upgrade purposes. The desired behavior will vary
            depending on the application. -->
        <bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
            <property name="filterInvocationDefinitionSource">
                <value>
            <!-- The URL mappings here are broken for readability, but
                in your XML file they must be unbroken. -->
            CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
            PATTERN_TYPE_APACHE_ANT
            /upgrade/**=httpSessionContextIntegrationFilter, upgradeAuthenticationFilter, \
                upgradeExceptionTranslationFilter, jiveAuthenticationTranslationFilter
            /post-upgrade/**=httpSessionContextIntegrationFilter, postUpgradeAuthenticationFilter, \
                postUpgradeExceptionTranslationFilter,jiveAuthenticationTranslationFilter
            /admin/**=httpSessionContextIntegrationFilter, adminAuthenticationFilter, \
                adminExceptionTranslationFilter,jiveAuthenticationTranslationFilter
            /rpc/xmlrpc=wsRequireSSLFilter, httpSessionContextIntegrationFilter, \
                federatedIdentityAuthFilter, wsExceptionTranslator, jiveAuthenticationTranslationFilter, \
                wsAccessTypeCheckFilter
            /rpc/rest/**=wsRequireSSLFilter, httpSessionContextIntegrationFilter, \
                federatedIdentityAuthFilter, wsExceptionTranslator, jiveAuthenticationTranslationFilter, \
                wsAccessTypeCheckFilter
            /rpc/soap/**=wsRequireSSLFilter, httpSessionContextIntegrationFilter, \
                federatedIdentityAuthFilter, jiveAuthenticationTranslationFilter
            /**=httpSessionContextIntegrationFilter, federatedIdentityAuthFilter, \
                jiveAuthenticationTranslationFilter
                </value>
            </property>
        </bean>
        
        <!--  Define a new filter for externally-managed users. Inject dependencies as needed. -->
        <bean id="federatedIdentityAuthFilter"
            class="com.jivesoftware.plugins.aaa.FederatedIdentityAuthFilter">
            <property name="userManager" ref="userManagerImpl"/>
            <property name="groupManager" ref="groupManagerImpl"/>
            <property name="userAgent" ref="agent"/>
            <property name="groupAgent" ref="agent"/>
        </bean>
    
        <!-- The Sample Agent - Custom implementations would change this to a more
            meaningful implementation. This particular implementation merges group
            and user agents into one class which is not required. -->
        <bean id="agent" class="com.jivesoftware.plugins.aaa.SampleAgent">
            <property name="groupManager" ref="groupManagerImpl"/>
        </bean>    
    </beans>
    

    FederatedIdentityAuthFilter from Sample

    package com.jivesoftware.plugins.aaa;
         
    // Imports omitted for brevity.
        
    public class FederatedIdentityAuthFilter implements Filter {
    
        private static final Logger log = LogManager.getLogger(FederatedIdentityAuthFilter.class);
         
        private IdentityProviderUserAgent userAgent;
        private IdentityProviderGroupAgent groupAgent;
        
        private UserManager userManager;
        private GroupManager groupManager;
         
        private boolean active;
         
        /**
         * Ultimately this filter will do nothing if the request is already authenticated.
         */
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
         
            final SecurityContext context = SecurityContextHolder.getContext();
            final HttpServletRequest servletRequest = (HttpServletRequest)request;
         
            Authentication auth = context.getAuthentication();
         
            if(auth == null || !auth.isAuthenticated() && active) {
                long timer = System.currentTimeMillis();
                //attempt to resolve the user from the agent
                User externalUser = null;
         
                try {
                    externalUser = userAgent.extractUserFromRequest(servletRequest);
                   if(externalUser == null) {
                       log.info("User agent failed to load user.");
                   }
                   else {
                        User coerced = coerceExternalUser(externalUser);
                        if(log.isDebugEnabled()) {
                            log.debug("Loaded and coerced representation for user " + coerced);
                        }
    
                        // Create the user if not currently in the local system
                        User jiveRepresentation = userManager.getUser(coerced);
                        if(jiveRepresentation == null) {
                            jiveRepresentation = userManager.createUser(coerced);
                        }
                        else {
                            jiveRepresentation = userManager.updateUser(coerced);
                        }
         
                        if(groupAgent != null && groupManager != null) {
                            handleGroupMappings(groupAgent, jiveRepresentation, groupManager,
                                servletRequest);
                        }
    
                        // Map the user to the security context
                        setUserToAuthenticationContext(jiveRepresentation, context);
                    }
                }
    
                catch(Exception ex) {
                    log.info("User agent failed to load user with exception.", ex);
                }
         
                if(log.isDebugEnabled()) {
                    log.debug("Federated identity processing time=" +
                        (System.currentTimeMillis() - timer));
                }
            }
            chain.doFilter(request, response);
        }
    
        /**
         * Map a user to system groups given the group membership for a user from
         * the IdentityProviderGroupAgent. This implementation will not attempt to
         * create a group if it does not exist.
         *
         * Subclasses should override this method if different mapping behavior is desired.
         *
         * @param agent
         * @param user
         * @param groupManager
         * @param request
         */
        protected void handleGroupMappings(IdentityProviderGroupAgent agent, User user,
            GroupManager groupManager, HttpServletRequest request) {
            long timer = System.currentTimeMillis();
            long[] groupIDs = agent.getUserGroups(user, request);
            if(null != groupIDs) {
                Arrays.sort(groupIDs);
                Iterable<Group> currentGroups = groupManager.getUserGroups(user);
    
                if(currentGroups != null) {
                    // Put the groups into a more meaningful form
                    Map<Long, Group> groups = new HashMap<Long, Group>();
                    Iterator<Group> groupIterator = currentGroups.iterator();
                    Group workingGroup = null;
                    while(groupIterator.hasNext()) {
                        workingGroup = groupIterator.next();
                        groups.put(new Long(workingGroup.getID()), workingGroup);
                    }
    
                    // For any group the user doesn't belong to, add them.
                    for(long groupID : groupIDs) {
                        if(!groups.containsKey(new Long(groupID))) {
                            try {
                                workingGroup = groupManager.getGroup(groupID);
                                if(!workingGroup.isMember(user)) {
                                    workingGroup.addMember(user);
                                    groupManager.update(workingGroup);
                                }
                            }
                            catch(GroupNotFoundException gnfe) {
                                log.info("Invalid group agent mapping for groupID=" + groupID);
                            }
                            catch(Exception ge) {
                                log.info("Unhandled exception mapping user " + user.getID() +
                                     " to group " + groupID, ge);
                            }
                        }
                    }
         
                    // For any group the user currently belongs to and shouldn't, remove them.
                    for(Long groupID : groups.keySet()) {
                        // Note that the binary search relies on the IDs being sorted above
                        if(Arrays.binarySearch(groupIDs, groupID.longValue()) == -1) {
                            try {
                                workingGroup = groupManager.getGroup(groupID.longValue());
                                workingGroup.removeMember(user);
                                groupManager.update(workingGroup);
                            }
                            catch(Exception ex) {
                                log.info("Unable to unmap user " + user.getID() + " from group " +
                                    groupID.longValue(), ex);
                            }
                        }
                    }
                }
            }
    
            if(log.isDebugEnabled()) {
                log.debug("Federated identity group processing time=" +
                    (System.currentTimeMillis() - timer));
            }
        }
         
        /**
         * Template method to bind a user object to the system security context.
         *
         * Subclasses should override if changes to this behavior are needed.
         * @param user
         * @param context
         */
        protected void setUserToAuthenticationContext(User user, SecurityContext context) {
            if(user != null && context != null) {
                context.setAuthentication(new JiveUserAuthentication(user));
            }
        }
         
        /**
         * Template method to coerce the user returned from an agent into a
         * user for addition or update to the Jive local store.
         *
         * Subclasses may override this method to achieve different behavior.
         * @param user
         * @return
         */
        protected User coerceExternalUser(User user) {
         
            UserTemplate ut = new UserTemplate(user);
         
            // Mark as federated.
            ut.setFederated(true);
         
            // Update last logged time.
            ut.setLastLoggedIn(new Date());
         
            return ut;
        }
    
        /**
         * Establishes a user agent to use for fetching user information from
         * an external identity provider.
         * @param userAgent
         */
        public void setUserAgent(IdentityProviderUserAgent userAgent) {
            this.userAgent = userAgent;
            active = true;
        }
         
        /**
         * Establishes a group agent to use for fetching user information from
         * an external identity provider.
         * @param groupAgent
         */
        public void setGroupAgent(IdentityProviderGroupAgent groupAgent) {
            this.groupAgent = groupAgent;
        }
         
        /**
         * Sets the user manager to use for updating the user login information.
         * @param userManager
         */
        public void setUserManager(UserManager userManager) {
            this.userManager = userManager;
        }
         
        /**
         * Sets the group manager to use for updating the user login information.
         * @param groupManager
         */
        public void setGroupManager(GroupManager groupManager) {
            this.groupManager = groupManager;
        }
         
        public void init(FilterConfig arg0) throws ServletException {
            //no-op
         }
         
        public void destroy() {
            if(userAgent != null) {
                userAgent.destroy();
            }
         
            if(groupAgent != null) {
                groupAgent.destroy();
            }
        }
    }
    

    IdentityProviderUserAgent from Sample

    package com.jivesoftware.plugins.aaa;
    import javax.servlet.http.HttpServletRequest;
    import com.jivesoftware.base.User;
    
    /**
     * Defines the contract between the FederatedIdentityAuthFilter and the
     * identity provider for user information.
     */
    public interface IdentityProviderUserAgent {
    
        /**
         * Invoked by the FederatedidentityAuthFilter to extract a User object
         * from the incoming request. The User returned by this method will
         * be used to load an internal Jive representation of the user, or
         * to create one if an internal representation does not exist.
         *
         * The User returned from this method should be fully populated and
         * should not hold any backing resources -- i.e. field reads should
         * not result in subsequent calls to the identity provider.
         *
         * Implementations if this method should be safe for use across
         * multiple threads.
         *
         * @param request
         * @return
         */
        public User extractUserFromRequest(HttpServletRequest request);
    
    
        /**
         * To be called by the containing filter to allow the agent to properly
         * release any resources.
         */
        public void destroy();
    }
    

    IdentityProviderGroupAgent from Sample

    package com.jivesoftware.plugins.aaa;
    
    import javax.servlet.http.HttpServletRequest;
    import com.jivesoftware.base.User;
         
    /**
     * Contract between the FederatedUserAuthFilter
     * and the identity provider for group information.
     */
    public interface IdentityProviderGroupAgent {
         
        /**
         * Returns a long array of group IDs that the given user should
         * map to in the application. The FederatedIdentityAuthFilter will
         * add the user to each group in the array, and for any group
         * the user currently belongs to not in the array, remove the user.
         *
         * Implementations if this method should be safe for use across
         * multiple threads.
         *
         * @param user
         * @param request
         * @return
         */
        public long[] getUserGroups(User user, HttpServletRequest request);
        
        /**
         * Called by the FederatedIdentityAuthFilter to allow the agent to
         * properly release any resources.
         */
        public void destroy();
         
    }
    

    SampleAgent from Sample

    package com.jivesoftware.plugins.aaa;
         
    import javax.servlet.http.HttpServletRequest;
         
    import org.apache.log4j.LogManager;
    import org.apache.log4j.Logger;
         
    import com.jivesoftware.base.Group;
        
    import com.jivesoftware.base.GroupManager;
    import com.jivesoftware.base.GroupNotFoundException;
    import com.jivesoftware.base.User;
    import com.jivesoftware.base.UserTemplate;
         
    public class SampleAgent implements IdentityProviderUserAgent,
        
        IdentityProviderGroupAgent {
         
        private static final String TEST_GROUP = "Test Group";
        private static final Logger log = LogManager.getLogger(SampleAgent.class);
        
        private long testGroupID = -1;
         
        private GroupManager groupManager;
         
        /**
         * Initializes the agent. Spring will call this method after all dependencies have
         * been injected.
         */
        public void init() {
            log.info("Initializing sample user/group agent.");
            //create sample group
            if(groupManager != null) {
                try {
                    Group testGroup = groupManager.getGroup(TEST_GROUP);
                    testGroupID = testGroup.getID();
                }
                catch(GroupNotFoundException gnfe) {
                    try {
                        Group newGroup = groupManager.createGroup(TEST_GROUP);
                        testGroupID = newGroup.getID();
                        log.info("Test group created.");
                    }
                    catch(Exception ex) {
                        log.error("Failed to create test group.", ex);
                    }
                }
            }
        }
         
        /**
         * Over simplified implementation of this method that
         * always returns a test user effectively resulting
         * in unauthenticated requests always becoming this user
         * when the doAuth parameter is present.
         *
         * Real implementations would do things like reading SAMLResponse
         * headers, looking for SiteMinder headers, NTLM headers, etc.
         */
        public User extractUserFromRequest(HttpServletRequest request) {
         
            String isAuth = request.getParameter("doAuth");
            if(isAuth == null) return null;
         
            UserTemplate ut = new UserTemplate();
            ut.setUsername("testauth");
            ut.setEmail("test@jivesoftware.com");
            ut.setName("Test Auth User");
        
            return ut;
        }
    
        public long[] getUserGroups(User user, HttpServletRequest request) {
            //return the test group
            if(testGroupID != -1) {
                return new long[] { testGroupID };
            }
            return null;
        }
         
        /**
         * Spring-managed dependency.
         * @param groupManager
         */
        public void setGroupManager(GroupManager groupManager) {
            this.groupManager = groupManager;
        }
        
        public void destroy() {
            //no-op
        }
    }
    

    Extending the Sample Plugin

    The sample SSO plugin is designed to be extended by SSO implementations and leverages the Maven2 build and dependency management system. You'll most likely customize this plugin by overriding template methods defined in FederatedIdentityAuthFilter. This class is designed to provide boiler plate code used in the vast majority of SSO customizations. In the case of common extension points, the filter defines overrideable behavior as protected methods that can be redefined in subclasses and changed if necessary.

     

    Implementations of the IdentityProviderUserAgent can perform nearly any action desired by the implementors including making web services calls to remote services, accessing other Jive subsystems or contacting an LDAP directory. In effect, this class is similar to a J2EE filter with the exception that it does not need to be concerned with HTTP response or filter chain management. The implementation of the agent should be packaged as a Spring-managed bean. As such, that agent can expose setters for desired Jive manager objects which it can use to process requests as desired.

     

    Many of the classes and interfaces above extend base Spring Security classes. This gives implementors a wide variety of well tested, vetted code on which to base customizations. For example, Spring Security has several classes to assist with processing X509-based authentication. Implementors are encouraged to consult the Spring Security Documentation for more information and to favor existing code wherever possible.