Central Authentication System Integration

Casifying jAPS

Reference Versions:

  • CAS server 3.3
  • CAS client 3.1.5
  • jAPS 2.0 or major

It's required knowledge of Java 2 EE,  Eclipse,  jAPS,  CAS and  Tomcat.

jAPS is a framework for service integration and for implement high customized intranet and internet portals, complying with accessibility requirements of W3C WCAG standards. The Central Autentication System is a service to provide SSO, the main features are: the server is java-based, clients exists for most languages and systems, the protocol is open and well documented. The CAS integration solutions at the time is in production for governments and private subjects.

You can integrate CAS ( http://www.jasig.org/cas) and jAPS installing the plugin jpcasclient (it will be released as soon as possible) or integrate it manually with instructions of this document. You may find more info on the jAPS mailing lists.

The authentication process in jAPS framework is managed by a sub-control service for the front-end and by a dedicated struts2 action for the backend (this action also manages the logout procedure). So the CAS integration is made by extending properly the sub-control service and the action. Actually you need to extend two sub-control services, the one that verified authorization for current request and the one that manage authentication. The first redirects not authorized users to CAS login page. The second one loads the user info into session, and the user info are being read from the result of ticket validation filter (managed by the CAS standard client filter for ticket validation). The logout method must unload the user from session and redirect client/browser to CAS logout page to destroy also his single-sign-on session.

Illustration 1: How works jAPS and CAS integration

The illustration 1 represents schematically how works integration between CAS and jAPS. Suppose you have a base configuration for CAS with the jAPS db serv for user-db.

Image legend:

  1. The control service RequestAuthorizator redirect user not authorized to see required resource to CAS login page.
  1. In case of successfull autentication CAS redirect user to jAPS pages with the ticket param.
  1. The request validation filter validated the ticket and load resulting assertion on user session.
  1. If the control service Authenticator found assertion on user session extract the principal username and load user from the database.

In the following the steeps for integration.

Inserting the standard CAS client validation filter on the jAPS file web.xml

<filter>

<filter-name>CAS Validation Filter</filter-name>

<filter-class>

org.jasig.cas.client.validation.Cas10TicketValidationFilter

</filter-class>

<init-param>

<param-name>casServerUrlPrefix</param-name>

<param-value>http://localhost:8080/cas/</param-value>

</init-param>

<init-param>

<param-name>serverName</param-name>

<param-value>http://localhost:8080</param-value>

</init-param>

<init-param>

<param-name>redirectAfterValidation</param-name>

<param-value>false</param-value>

</init-param>

<init-param>

<param-name>exceptionOnValidationFailure</param-name>

<param-value>false</param-value>

</init-param>

</filter>

<filter-mapping>

<filter-name>CAS Validation Filter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

Redirect to CAS login if user is not authorizated to see required resource is managed by extension of the default jAPS sub-control service RequestAuthorizatorControlService (com.agiletec.aps.system.services.controller.control.RequestAuthorizator).

public int service(RequestContext reqCtx, int status) {
        if (_log.isLoggable(Level.FINEST)) {
                _log.finest("Invocata " + this.getClass().getName());
        }
        int retStatus = ControllerManager.INVALID_STATUS;
        if (status == ControllerManager.ERROR) {
                return status;
        }
        try {
                HttpServletRequest req = reqCtx.getRequest();
                HttpSession session = req.getSession();
            IPage currentPage = 
                (IPage) reqCtx.getExtraParam(SystemConstants.EXTRAPAR_CURRENT_PAGE);
            UserDetails currentUser = 
                (UserDetails) session.getAttribute(SystemConstants.SESSIONPARAM_CURRENT_USER);
            String pageGroup = currentPage.getGroup();
            boolean authorized = this.checkUserPermission(currentUser, pageGroup);
            if (authorized) {
                retStatus = ControllerManager.CONTINUE;
            } else {
                Lang currentLang = 
                        (Lang) reqCtx.getExtraParam(SystemConstants.EXTRAPAR_CURRENT_LANG);
                                String requiredUrl = 
                                        ((URLManager) this.getUrlManager()).createUrl(currentPage, currentLang, null);
                                
                                String loginBaseUrl = 
                                        this.getConfigManager().getParam(CasClientPluginSystemCostants.LOGIN_URL);
                                StringBuffer loginUrl = new StringBuffer(loginBaseUrl);
                                loginUrl.append("?service=");
                                loginUrl.append(requiredUrl);
                
                        reqCtx.addExtraParam(RequestContext.EXTRAPAR_REDIRECT_URL, loginUrl.toString());
                retStatus = ControllerManager.REDIRECT;
            }
        } catch (Throwable t) {
            ApsSystemUtils.logThrowable(t, this, "service", "Error serving request");
            retStatus = ControllerManager.ERROR;
        }
        return retStatus;
    }

You need also to change the default jAPS authenticator sub-control service AuthenticatorControlService (com.agiletec.aps.system.services.controller.control.Authenticator). It retrive CAS assertion of ticket validation from user session. If filter for ticket validation has received a CAS ticket for validate it put the assertion, obtained from ticket validation, in the user session. If assertion contains the Principal username, the authenticator puts the user on the current session. (The service intercept, like in the base implementation, the params with names username and password for example to provide local administrative access to portal also if CAS is not responding).

 public int service(RequestContext reqCtx, int status) {
        String name = null;
        if (_log.isLoggable(Level.FINEST)) {
                _log.finest("Invocata " + this.getClass().getName());
        }
        int retStatus = ControllerManager.INVALID_STATUS;
        if (status == ControllerManager.ERROR) {
                return status;
        }
        try {
            HttpServletRequest req = reqCtx.getRequest();
            
            //Punto 1
            Assertion assertion = 
                      (Assertion) req.getSession().getAttribute(CasClientPluginSystemCostants.CONST_CAS_ASSERTION);
            if (_log.isLoggable(Level.FINEST)) {
                        _log.finest(" Assertion "+assertion);
            }
            if (null != assertion) {
                AttributePrincipal attributePrincipal = assertion.getPrincipal();
                name = attributePrincipal.getName();
                if (_log.isLoggable(Level.FINEST)) {
                        this._log.finest(" Princ " + attributePrincipal);
                        this._log.finest(" Princ - Name " + attributePrincipal.getName());
                }
            }
            
            this._log.finest("Request From User with Principal [CAS tiket validation]: " + name +
 " - info: AuthType " + req.getAuthType() + " " + req.getProtocol() + " " + req.getRemoteAddr() + " " + req.getRemoteHost());
                
            HttpSession session = req.getSession();
            if (null != name) {
                String username = name;
                if (getAuthCommon().hasRealmDomainInformation(name)) {
                         username = getAuthCommon().getUsernameFromPrincipal(name);
                }
                this._log.finest("Request From User with Username: " + username + 
" - info: AuthType " + req.getAuthType() + " " + req.getProtocol() + " " + req.getRemoteAddr() + " " + req.getRemoteHost());
                
                if (username != null) {
                        this._log.finest("user " + username );
                        UserDetails userOnSession = (UserDetails) session.getAttribute(SystemConstants.SESSIONPARAM_CURRENT_USER);
                        //FORSE QUESTO é IL PUNTO
                        
                        if (userOnSession == null || (userOnSession != null && !username.equals(userOnSession.getUsername()))) {
                                UserDetails user = this.getAuthenticationProvider().getUser(username);
                                if (user != null) {
                                        if (!user.isAccountNotExpired()) {
                                                req.setAttribute("accountExpired", new Boolean(true));
                                        } else {
                                                if (userOnSession != null && !userOnSession.getUsername().equals(SystemConstants.GUEST_USER_NAME)) {
                                                        ((AbstractUser) user).setPassword(userOnSession.getPassword());
                                                }
                                                session.setAttribute(SystemConstants.SESSIONPARAM_CURRENT_USER, user);
                                                this._log.finest("New User: " + user.getUsername());
                                        }
                                } else {
                                        req.setAttribute("wrongAccountCredential", new Boolean(true));
                                }
                        }
                        
                }
            } 
            
            //Punto 2
            String userName = req.getParameter("username");
            String password = req.getParameter("password");
            if (userName != null && password != null) {
                _log.finest("user " + userName + " - password ******** ");
                UserDetails user = this.getAuthenticationProvider().getUser(userName, password);
                if (user != null) {
                        if (!user.isAccountNotExpired()) {
                                req.setAttribute("accountExpired", new Boolean(true));
                        } else {
                                session.setAttribute(SystemConstants.SESSIONPARAM_CURRENT_USER, user);
                                _log.finest("Nuovo User: " + user.getUsername());
                        }
                } else {
                        req.setAttribute("wrongAccountCredential", new Boolean(true));
                }
            }
            
            //Punto 3
            if (session.getAttribute(SystemConstants.SESSIONPARAM_CURRENT_USER) == null) {
                UserDetails guestUser = this.getUserManager().getGuestUser();
                session.setAttribute(SystemConstants.SESSIONPARAM_CURRENT_USER, guestUser);
            }
            retStatus = ControllerManager.CONTINUE;
        } catch (Throwable t) {
            ApsSystemUtils.logThrowable(t, this, "service", "Errore in elaborazione richiesta");
            retStatus = ControllerManager.ERROR;
        }
        return retStatus;
    }

It is also necessary to extend AuthenticationProviderManager (com.agiletec.aps.system.services.user.AuthenticationProviderManager) to have a method for loading UserDetails from username.

The user logout in jAPS Plataform is provided by DispatchAction (com.agiletec.apsadmin.common.DispatchAction). So, for logout you need to extends DispatchAction. The logout method must unload the user from session and must redirect client/browser to the CAS logout page to destroy also his single-sign-on session.

public String doLogout() {
                ApsSystemUtils.getLogger().info("Exec Logout from jAPS and from CAS.");
                this.getSession().invalidate();
                String baseApplUrl = 
                        this.getBaseConfigManager().getParam(SystemConstants.PAR_APPL_BASE_URL);
                String logoutBaseUrl = 
                        this.getBaseConfigManager().getParam(CasClientPluginSystemCostants.LOGOUT_URL);
                StringBuffer logoutUrl = new StringBuffer(logoutBaseUrl);
                logoutUrl.append("?url=");
                logoutUrl.append(baseApplUrl);
                ApsSystemUtils.getLogger().fine("Logout url " + logoutUrl);
                try {
                        this.getServletResponse().sendRedirect(logoutUrl.toString());
                } catch (IOException ioe) {
                        ApsSystemUtils.logThrowable(ioe, this, "doLogout", "Error redirecting to CAS logout");
                }
                return null;
        }

The DispatchAction provide also the method to login directly to jAPS backend. You can decide to leave this unchanged as an administrative access to backend or casifying also this. If you decide to casifying also the DispatchAction you need to override the login method. This method have to check the session for the attribute containing the assertion added possibly by the CAS validation filter. If it finds the assertion, it have to load the user corresponding to assertion's principal in the session.

All the classes to extend are spring beans that can be simply configured modifing config files of spring.

Note:

obviously the User must have local roles and groups in jAPS.

Attachments