cancel
Showing results for 
Search instead for 
Did you mean: 

Shibboleth Single Sign-On

n_toscani
Champ in-the-making
Champ in-the-making
Anyone of you guys has integrated Alfresco in a federated Shibboleth scenario?

Thanks.
Nik
11 REPLIES 11

andy
Champ on-the-rise
Champ on-the-rise
Hi

I have not heard of anything.
What is required? Does it just set  some HTTP header with the user name?

Andy

kimmoilppola
Champ in-the-making
Champ in-the-making
I'm working on Shibboleth SSO right now. There's a few different approaches  and I think we will have a go with Tomcat/CAS-filter. I'll release all the code and configuration when finished - some time before september.

kimmoilppola
Champ in-the-making
Champ in-the-making
after stumbling deeper into shibboleth I think we'll handle this with apache instead.

dgenard
Champ on-the-rise
Champ on-the-rise
Hi, we are also interested in the use of Shibboleth with Alfresco.
Have you made some progress on this ?

Denis

kimmoilppola
Champ in-the-making
Champ in-the-making
Hi, we are also interested in the use of Shibboleth with Alfresco.
Have you made some progress on this ?

Denis
Hi Denis,
It's been a while and we have been really busy here but.. I came up with this implementation of SSO with Shibboleths Apache module and Alfresco 2.1. I think my work may need auditing so I sent it to Will but I think he's too busy so here it is:

Shibboleth 1.X does not have a java module for Service Providers so we are stuck with using apache,shibd and mod_jk.
I wont go into detail how to setup apache & shibd as they are specific to Shibboleth and well documented in http://shibboleth.internet2.edu/
The trick is to setup apache and shibd properly, then install mod_jk (alternatively proxy ajp). Next we configure Tomcat (or
whatever application server you are using) to use AJP and disable all other connectors. For Tomcat this is done in conf/server.xml and is
well documented there. After that you have to see that attributes from AAP.xml match those in shibbolethAttributes.properties file that you
place in your classpath. You also have to configure Alfresco web-client to use this ShibbolethHTTPRequestAuthenticationFilter in Alfresco's web.xml.
Additionally if you want to have administrators you configure those as with any other authentication (ie. in authority-services-context.xml)

I know it's a bit of a hack to create/update user preferences in the filter but as Shibboleth doesn't provide any means of synchronization I couldn't figure out a better way to do it. If you want to test it somewhere you will have a hard time learning Shibboleth itself as it basically needs at least a client and a server. I remember there is a testing identity-provider somewhere so you can try it with a minimal installation of a service-provider only (but with limited attributes).

When (if not already) Shibboleth 2 supports Java this should be reimplemented.

ShibbolethHTTPRequestAuthenticationFilter.java

package org.alfresco.web.app.servlet;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.MutableAuthenticationDao;
import org.alfresco.repo.webservice.administration.NewUserDetails;
import org.alfresco.repo.webservice.types.NamedValue;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.web.bean.LoginBean;
import org.alfresco.web.bean.repository.User;
import org.alfresco.web.app.Application;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.transaction.UserTransaction;
import javax.transaction.SystemException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.*;

/**
*
*
* @author Kimmo Ilppola
*/
public class ShibbolethHTTPRequestAuthenticationFilter extends AbstractAuthenticationFilter implements Filter
{
    /** Location of configuration file on classpath */
    private final static String PROPERTY_FILE = "/shibbolethAttributes.properties";
    private static Properties CONFIG_PROPERTIES;
    /* Name of header properties */
    private final static String SHIB_USER = "header.username";
    private final static String FIRSTNAME_HEADER_NAME_PROPERTY = "header.firstname";
    private final static String LASTNAME_HEADER_NAME_PROPERTY = "header.lastname";
    private final static String ORGID_HEADER_NAME_PROPERTY = "header.orgId";
    private final static String EMAIL_HEADER_NAME_PROPERTY = "header.email";
    private final static String LOGOUT_URL_HEADER_NAME_PROPERTY = "header.logout-url";

    private static String shibUserNameHeaderName = "Shib-InetOrgPerson-uid";
    private static String firstNameHeaderName = "Shib-InetOrgPerson-givenName";
    private static String lastNameHeaderName = "Shib-InetOrgPerson-cn";
    private static String emailHeaderName = "Shib-InetOrgPerson-mail";
    private static String orgIdNumHeaderName = "Shib-InetOrgPerson-ou";
    private static String logoutUrlHeaderName = "logout-url";

    private static Log logger = LogFactory.getLog(ShibbolethHTTPRequestAuthenticationFilter.class);

    private ServletContext context;
    private AuthenticationComponent authComponent;
    private AuthenticationService authService;
    private TransactionService transactionService;
    private PersonService personService;
    private NodeService nodeService;
    private MutableAuthenticationDao authenticationDao;
    private SearchService searchService;
    private String loginPage = null;

    public ShibbolethHTTPRequestAuthenticationFilter()
    {
        super();
        if (logger.isDebugEnabled()) {
            logger.debug("Initializing shibboleth authenticator using property file " + PROPERTY_FILE);
        }
        InputStream propsIn = ShibbolethHTTPRequestAuthenticationFilter.class.getResourceAsStream(PROPERTY_FILE);
        CONFIG_PROPERTIES = new Properties();
        try {
            CONFIG_PROPERTIES.load(propsIn);

            shibUserNameHeaderName = CONFIG_PROPERTIES.getProperty(SHIB_USER);
            firstNameHeaderName = CONFIG_PROPERTIES.getProperty(FIRSTNAME_HEADER_NAME_PROPERTY);
            lastNameHeaderName = CONFIG_PROPERTIES.getProperty(LASTNAME_HEADER_NAME_PROPERTY);
            emailHeaderName = CONFIG_PROPERTIES.getProperty(EMAIL_HEADER_NAME_PROPERTY);
            orgIdNumHeaderName = CONFIG_PROPERTIES.getProperty(ORGID_HEADER_NAME_PROPERTY);
            logoutUrlHeaderName = CONFIG_PROPERTIES.getProperty(LOGOUT_URL_HEADER_NAME_PROPERTY);

        } catch (IOException e) {
            logger.warn("ShibbolethHTTPRequestAuthenticationFilter is unable to read properties file, using default properties", e);
        }
    }

    public void destroy()
    {
        // Nothing to do
    }

    /**
     * Run the filter
     *
     * @param sreq
     *            ServletRequest
     * @param sresp
     *            ServletResponse
     * @param chain
     *            FilterChain
     * @exception IOException
     * @exception ServletException
     */
    public void doFilter(ServletRequest sreq, ServletResponse sresp, FilterChain chain) throws IOException,
            ServletException
    {
        // Get the HTTP request/response/session
        HttpServletRequest req = (HttpServletRequest) sreq;
        HttpServletResponse resp = (HttpServletResponse) sresp;
        HttpSession httpSess = req.getSession(true);

        String authHdr = req.getHeader( shibUserNameHeaderName );

        if(logger.isDebugEnabled()) {
            logger.debug("ShibbolethHTTPRequestAuthenticationFilter uri: "+req.getRequestURI()+", authHdr: "+authHdr);
        }

        // Throw an error if we have an unknown authentication
        if ((authHdr == null) || (authHdr.length() < 1))
        {
            resp.sendRedirect(req.getContextPath() + "/jsp/noaccess.jsp");
            BaseServlet.redirectToLoginPage((HttpServletRequest)sreq, (HttpServletResponse)sresp, context);
            chain.doFilter(sreq, sresp);
            return;
        }

        // redirect login/logout to shibboleth logout url
        // todo: this doesn't work yet
        if (req.getRequestURI().endsWith(getLoginPage())) {
            resp.sendRedirect( req.getHeader(logoutUrlHeaderName) );
            return;
        }

        // See if there is a user in the session and test if it matches
        User user = (User) httpSess.getAttribute(AuthenticationHelper.AUTHENTICATION_USER);

        if (user != null)
        {
            try
            {
                // Debug
                if (logger.isDebugEnabled())
                    logger.debug("User " + user.getUserName() + " validate ticket");
                // Validate the user. Shibboleth allready validates the ticket so we just need to check the userName
                // todo : is this validation reduntant?
                if (user.getUserName().equals(authHdr))
                {
                    authComponent.setCurrentUser(user.getUserName());
                    chain.doFilter(sreq, sresp);
                    return;
                }
                else
                {
                    // No match
                    setAuthenticatedUser(req, httpSess, authHdr);
                }
            }
            catch (Throwable ex)
            {
                if (logger.isErrorEnabled())
                    logger.error("Failed to validate user " + user.getUserName(), ex);
            }
        }
        setAuthenticatedUser(req, httpSess, authHdr);
        chain.doFilter(sreq, sresp);
        return;
    }

    /**
     * Set the authenticated user.
     *
     * @param req
     * @param httpSess
     * @param userName
     */
    private void setAuthenticatedUser(HttpServletRequest req, HttpSession httpSess, String userName)
    {
       
        // shibboleth does not provide any other means to import users
        addShibUser(req,httpSess);

        UserTransaction atx = transactionService.getUserTransaction();
        try {
            atx.begin();
            // Set the authentication
            authComponent.setCurrentUser(userName);
            atx.commit();
        } catch (Throwable e) {
            logger.error(e);
            try {
                atx.rollback();
            } catch (SystemException e1) {
                logger.error("Failed to rollback transaction", e);
            }
        }

        // Set up the user information
        // Note that we have to create users here as we have no other way of retrieving user-attributes
        UserTransaction tx = transactionService.getUserTransaction();
        NodeRef homeSpaceRef = null;
        User user;
        try
        {
            tx.begin();
            user = new User(userName, authService.getCurrentTicket(), personService.getPerson(userName));
            homeSpaceRef = (NodeRef) nodeService.getProperty(personService.getPerson(userName),
                    ContentModel.PROP_HOMEFOLDER);
            user.setHomeSpaceId(homeSpaceRef.getId());
            tx.commit();
        }
        catch (Throwable ex)
        {
            logger.error(ex);

            try
            {
                tx.rollback();
            }
            catch (Exception ex2)
            {
                logger.error("Failed to rollback transaction", ex2);
            }

            if(ex instanceof RuntimeException)
            {
                throw (RuntimeException)ex;
            }
            else
            {
                throw new RuntimeException("Failed to set authenticated user", ex);
            }
        }

        // Store the user
        httpSess.setAttribute(AuthenticationHelper.AUTHENTICATION_USER, user);
        httpSess.setAttribute(LoginBean.LOGIN_EXTERNAL_AUTH, Boolean.TRUE);

    }

    protected NodeRef getCompanyHome()
    {
    StoreRef storeRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore");
    ResultSet resultSet = searchService.query(storeRef, SearchService.LANGUAGE_LUCENE, "PATH:\"/app:company_home\"");
    return resultSet.getNodeRef(0);
    }

    /**
     * @return The login page url
     */
    private String getLoginPage()
    {
       if (this.loginPage == null)
       {
          this.loginPage = Application.getLoginPage(this.context);
       }

       return this.loginPage;
    }

    private boolean addShibUser(HttpServletRequest req, HttpSession httpSess) {

        // get all properties from header
        String userName = req.getHeader( shibUserNameHeaderName );
        String orgId = req.getHeader(orgIdNumHeaderName);
        String firstName = req.getHeader(firstNameHeaderName);
        String lastName = req.getHeader(lastNameHeaderName);
        String email = req.getHeader( emailHeaderName );

        // Create a new authentication
        try {
            // todo: passwords should not be stored!
            authService.createAuthentication(userName, "90wy3vgyn0w394vf".toCharArray());
        } catch (AuthenticationException e) {
            logger.info( "User allready exists, authenticating..");
            return false;
        } finally {

            UserTransaction atx = transactionService.getUserTransaction();
            try {
                atx.begin();
                // Set the authentication
                // todo: can we do this otherwise? we are not yet authenticated
                authComponent.setCurrentUser("admin");
                atx.commit();
            } catch (Throwable e) {
                logger.error(e);
                try {
                    atx.rollback();
                } catch (SystemException e1) {
                    logger.error("Failed to rollback transaction", e);
                }
            }


            // todo: use homeFolderProvider
            String homeFolder = "workspace://SpacesStore/"+getCompanyHome().getId();
            // create new userDetails object
            NewUserDetails newUser = new NewUserDetails();

            logger.info("newuser.setUserName :"+userName);
            newUser.setUserName(userName);
            newUser.setPassword("shibboleth");
            newUser.setProperties(
                    createPersonProperties( homeFolder , firstName, "", lastName, email, orgId)
            );

            if(!personService.personExists(newUser.getUserName()))
            {
                atx = transactionService.getUserTransaction();
                try {
                    atx.begin();
                    // Create a new user
                    authenticationDao.createUser(newUser.getUserName(),"shibboleth".toCharArray());
                    // Create a new person
                    Map<QName, Serializable> properties = new HashMap<QName, Serializable>(7);
                    logger.info("properties.put username : "+newUser.getUserName());
                    properties.put(ContentModel.PROP_USERNAME, newUser.getUserName());
                    for (NamedValue namedValue : newUser.getProperties())
                    {
                        logger.info("properties.put "+namedValue.getName()+" : "+namedValue.getValue());
                        properties.put(QName.createQName(namedValue.getName()), namedValue.getValue());
                    }
                    NodeRef personNodeRef = personService.createPerson(properties);
                    atx.commit();
                } catch (Throwable e) {
                    logger.error(e);
                    try {
                        atx.rollback();
                    } catch (SystemException e1) {
                        logger.error("Failed to rollback transaction", e);
                    }
                }
            } else {
                logger.info("User "+userName+" exists, updating..");

                atx = transactionService.getUserTransaction();
                try {
                    atx.begin();

                    Map<QName, Serializable> properties = new HashMap<QName, Serializable>(7);
                    logger.info("properties.put username : "+newUser.getUserName());
                    properties.put(ContentModel.PROP_USERNAME, newUser.getUserName());
                    for (NamedValue namedValue : newUser.getProperties())
                    {
                        logger.info("properties.put "+namedValue.getName()+" : "+namedValue.getValue());
                        properties.put(QName.createQName(namedValue.getName()), namedValue.getValue());
                    }
                    personService.setPersonProperties(userName,properties);
                    atx.commit();

                } catch (Throwable e) {
                    logger.error(e);
                    try {
                        atx.rollback();
                    } catch (SystemException e1) {
                        logger.error("Failed to rollback transaction", e);
                    }
                }


            }
            authService.clearCurrentSecurityContext();

            return true;
        }
    }

    private NamedValue[] createPersonProperties(
            String homeFolder,
            String firstName,
            String middleName,
            String lastName,
            String email,
            String orgId)
    {
        // Create the new user objects
        return new NamedValue[] {
                new NamedValue("{http://www.alfresco.org/model/content/1.0}homeFolder", false, homeFolder, null),
                new NamedValue("{http://www.alfresco.org/model/content/1.0}firstName", false, firstName, null),
                new NamedValue("{http://www.alfresco.org/model/content/1.0}middleName", false, middleName, null),
                new NamedValue("{http://www.alfresco.org/model/content/1.0}lastName", false, lastName, null),
                new NamedValue("{http://www.alfresco.org/model/content/1.0}email", false, email, null),
                new NamedValue("{http://www.alfresco.org/model/content/1.0}organizationId", false, orgId, null) };
    }
   
    public void init(FilterConfig config) throws ServletException
    {
        this.context = config.getServletContext();
        WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(context);
        ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY);
        transactionService = serviceRegistry.getTransactionService();
        nodeService = serviceRegistry.getNodeService();

        authComponent = (AuthenticationComponent) ctx.getBean("AuthenticationComponent");
        authService = (AuthenticationService) ctx.getBean("AuthenticationService");
        personService = (PersonService) ctx.getBean("PersonService");
        authenticationDao = (MutableAuthenticationDao) ctx.getBean("authenticationDao");
        searchService = (SearchService) ctx.getBean("SearchService");
    }
}
shibbolethAttributes.properties
# Take note how you want shibboleth attributess mapped to alfresco
# These need to match parameters defined in AAP.xml (attribute acceptance policy)
# logout-url is the page that should be displayed upon logout (invalidating shibd-session is not yet implemented)
header.username = Shib-InetOrgPerson-uid
header.firstname = Shib-InetOrgPerson-givenName
header.lastname = Shib-InetOrgPerson-cn
header.email = Shib-InetOrgPerson-mail
header.orgId = Shib-InetOrgPerson-ou
header.logout-url = logout-url

dgenard
Champ on-the-rise
Champ on-the-rise
Thanks a lot for your helpful feedback !

Denis

lunchbox
Champ in-the-making
Champ in-the-making
Hi all,

is kimmoilppola's solution still working in the 3.1 release?

regards,
Niels

agbooth
Champ in-the-making
Champ in-the-making
We have been using Alfresco with the Guanxi implementation of the Shibboleth SAML profile. Written entirely in Java by Alistair Young at the University of the Highlands and Islands, all Guanxi modules are fully compatible with the Internet2 implementation and also fully compatible with the UK Access Management Federation and OpenAthens. I have put some notes on our blog at http://www.socketelf.org:8080/roller/amset/entry/shibbing_alfresco.

lunchbox
Champ in-the-making
Champ in-the-making
thanx!
Getting started

Tags


Find what you came for

We want to make your experience in Hyland Connect as valuable as possible, so we put together some helpful links.