08 May 2014
 

How to get SSO in HippoCMS

Written by massi

As you can imagine this post is about Single Sign On and HippoCMS. In particular, at the end of this post, we will be able to deploy Hippo CMS in Single Sign On in our environment.

First step: authentication manager

First of all we need an authentication manager representing the entry point in our environment. Its purpose, besides the obvious management of the authentication mechanism, is to set an http header attribute with authenticated username. This task is achievable in many different ways because the mechanism is always the same; personally I tried this scenario with:

  • apache + mod_auth;
  • CAS with SPNEGO module.

Apache + mod_auth - use case

<location "/cms">
  AuthType CAS
  Require valid-user
  CASScope /

  ProxyPass http://xxx.tirasa.net:8080/cms
  ProxyPassReverse http://xxx.tirasa.net:8080/cms
  ProxyHTMLExtended On
  ProxyHTMLURLMap /cms /cms

  ProxyPassReverseCookiePath / /
  RewriteEngine On
  RewriteCond %{LA-U:REMOTE_USER} (.+)
  RewriteRule . - [E=RU:%1,NS]
  RequestHeader add X-Forwarded-User %{RU}e
</location>

CAS + SPNEGO authentication module - use case

In this case the authentication on a Windows PC was the entry point so, to obtain the SSO in HippoCMS, we have to add a CAS filter to it. Trying to use the CMS, CAS filter puts the username of the authenticated user into the http-request RemoteUser attribute.

Second step: the theory

In order to et confidence with the authentication and the authorization in HippoCMS I suggest you to read this post on hippo site.

Third step: the practice

Unfortunately following the Hippo documentation I could not achieve the expected result. So, from my point of view, this is the official practice guide to get SSO in HippoCMS:

  1. Replace /hippo:configuration/hippo:frontend/login/login/loginPage/plugin.class console property with: net.tirasa.hippo.cms.sso.SSOPlugin (see code at the end of the post);
  2. Disable the standard authentication by deleting the LoginModule row under this file: ./cms/src/main/webapp/WEB-INF/classes/org/hippoecm/repository/repository.xml;
  3. Add -Djava.security.auth.login.config=/var/tmp/hipposso/hippo_jaas.conf as Java properties of your container (of course you can choose your favorite file path);
  4. Under the chosen file path create the file:
    Jackrabbit {
      net.tirasa.hippo.cms.sso.SSOModule optional;
      org.hippoecm.repository.security.JAASLoginModule required preAuthorized=true;
    };
    
  5. Add this row java.security.auth.login.config = file:///var/tmp/hipposso/hippo_jaas.conf to site/src/main/webapp/WEB-INF/hst-config.properties file;
  6. Enjoy SSO.

Java classes

SSOPlugin

package net.tirasa.hippo.cms.sso;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.servlet.http.Cookie;
import org.apache.jackrabbit.core.security.authentication.CredentialsCallback;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.wicket.RequestCycle;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.protocol.http.WebRequest;
import org.apache.wicket.protocol.http.WebResponse;

import org.apache.wicket.PageParameters;
import org.hippoecm.frontend.PluginPage;
import org.hippoecm.frontend.model.UserCredentials;
import org.hippoecm.frontend.plugin.IPluginContext;
import org.hippoecm.frontend.plugin.config.IPluginConfig;
import org.hippoecm.frontend.plugins.login.LoginPlugin;
import org.hippoecm.frontend.service.render.RenderPlugin;
import org.hippoecm.frontend.session.LoginException;
import org.hippoecm.frontend.session.PluginUserSession;
import org.hippoecm.frontend.util.WebApplicationHelper;
import org.hippoecm.repository.WebCredentials;

public class SSOPlugin extends RenderPlugin implements CallbackHandler {

    private static final long serialVersionUID = 6971843172794119352L;

    private static final Logger log = LoggerFactory.getLogger(SSOPlugin.class);

    @SuppressWarnings("unused")
    private final static String SVN_ID = "$Id$";

    private static final String LOCALE_COOKIE = "loc";

    private DropDownChoice locale;

    public String selectedLocale;

    public SSOPlugin(final IPluginContext context, final IPluginConfig config) throws LoginException {

        super(context, config);
        fromOfficialDocs();

        login();
    }

    private void login() throws LoginException {
        final PluginUserSession userSession = (PluginUserSession) getSession();
        userSession.login(new UserCredentials(this));

        userSession.setLocale(new Locale(selectedLocale));
        userSession.getJcrSession();

        setResponsePage(PluginPage.class, new PageParameters(RequestCycle.get().getRequest().getParameterMap()));
    }

    private void fromOfficialDocs() {
        String[] localeArray = getPluginConfig().getStringArray("locales");
        if (localeArray == null) {
            localeArray = LoginPlugin.LOCALES;
        }
        final List locales = Arrays.asList(localeArray);

        // by default, use the user's browser settings for the locale
        selectedLocale = "en";
        if (locales.contains(getSession().getLocale().getLanguage())) {
            selectedLocale = getSession().getLocale().getLanguage();
        }

        // check if user has previously selected a locale
        Cookie[] cookies = ((WebRequest) RequestCycle.get().getRequest()).getHttpServletRequest().getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (LOCALE_COOKIE.equals(cookie.getName())) {
                    if (locales.contains(cookie.getValue())) {
                        selectedLocale = cookie.getValue();
                        getSession().setLocale(new Locale(selectedLocale));
                    }
                }
            }
        }

        add(locale = new DropDownChoice("locale", new PropertyModel(this, "selectedLocale"), locales));

        locale.add(new AjaxFormComponentUpdatingBehavior("onchange") {
            private static final long serialVersionUID = -1107858522700306810L;

            @Override
            protected void onUpdate(AjaxRequestTarget target) {
                //immediately set the locale when the user changes it
                Cookie localeCookie = new Cookie(LOCALE_COOKIE, selectedLocale);
                localeCookie.setMaxAge(365 * 24 * 3600); // expire one year from now
                ((WebResponse) RequestCycle.get().getResponse()).addCookie(localeCookie);
                getSession().setLocale(new Locale(selectedLocale));
                setResponsePage(this.getFormComponent().getPage());
            }
        });

        add(new FeedbackPanel("feedback").setEscapeModelStrings(false));
        add(new Label("pinger"));
    }

    @Override
    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
        for (Callback callback : callbacks) {
            if (callback instanceof NameCallback) {
                NameCallback nameCallback = (NameCallback) callback;
                String username
                        = (String) WebApplicationHelper.retrieveWebRequest().getHttpServletRequest().getRemoteUser();
                if (username != null) {
                    nameCallback.setName(username);
                }
            } else if (callback instanceof PasswordCallback) {
            } else if (callback instanceof CredentialsCallback) {
                CredentialsCallback credentialsCallback = (CredentialsCallback) callback;
                credentialsCallback.setCredentials(
                        new WebCredentials(((WebRequest) getRequest()).getHttpServletRequest()));
            }
        }
    }
}

SSOModule

package net.tirasa.hippo.cms.sso;

import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.apache.wicket.RequestCycle;

import org.hippoecm.frontend.util.WebApplicationHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SSOModule implements LoginModule {

    private static final Logger log = LoggerFactory.getLogger(SSOModule.class);

    private final boolean validLogin = true;

    @Override
    public void initialize(final Subject subject, final CallbackHandler callbackHandler,
            final Map sharedState, final Map options) {

        if (RequestCycle.get() != null) {
            final String session
                    = WebApplicationHelper.retrieveWebRequest().getHttpServletRequest().getHeader("X-Forwarded-User");

            ((Map) sharedState).put("javax.security.auth.login.name",
                    session != null ? session : "anonymous");

            log.debug("Set username with {}", session != null ? session : "anonymous");
        }
    }

    @Override
    public boolean login() throws LoginException {
        log.debug("LOGIN");
        return validLogin;
    }

    protected String validate(final String key) {
        log.debug("VALIDATE");
        return key;
    }

    @Override
    public boolean commit() throws LoginException {
        log.debug("COMMIT");
        return validLogin;
    }

    @Override
    public boolean abort() throws LoginException {
        log.debug("ABORT");
        return validLogin;
    }

    @Override
    public boolean logout() throws LoginException {
        log.debug("LOGOUT");
        return validLogin;
    }
}
       

« Return