/* * ShibAuthentication.java * * Version: $Revision: 4637 $ * * Copyright (c) 2002-2009, The DSpace Foundation. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of the DSpace Foundation nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. */ package org.dspace.authenticate; import java.sql.SQLException; import javax.servlet.http.HttpServletRequest; import java.util.Collection; import javax.servlet.http.HttpServletResponse; import org.apache.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.core.Context; import org.dspace.core.ConfigurationManager; import org.dspace.core.LogManager; import org.dspace.authenticate.AuthenticationManager; import org.dspace.authenticate.AuthenticationMethod; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; /** * Shibboleth authentication for DSpace, tested on Shibboleth 1.3.x and * Shibboleth 2.x. Read <a href= * "https://mams.melcoe.mq.edu.au/zope/mams/pubs/Installation/dspace15/view" * >Shib DSpace 1.5</a> for installation procedure. Read dspace.cfg for details * on options available. * * @author <a href="mailto:bliong@melcoe.mq.edu.au">Bruc Liong, MELCOE</a> * @author <a href="mailto:kli@melcoe.mq.edu.au">Xiang Kevin Li, MELCOE</a> * @version $Revision: 4637 $ */ public class ShibAuthentication implements AuthenticationMethod { /** log4j category */ private static Logger log = Logger.getLogger(ShibAuthentication.class); public int authenticate(Context context, String username, String password, String realm, HttpServletRequest request) throws SQLException { if (request == null) { return BAD_ARGS; } log.info("Shibboleth login started..."); java.util.Enumeration names = request.getHeaderNames(); String name; while (names.hasMoreElements()) log.debug("header:" + (name = names.nextElement().toString()) + "=" + request.getHeader(name)); boolean isUsingTomcatUser = ConfigurationManager .getBooleanProperty("authentication.shib.email-use-tomcat-remote-user"); String emailHeader = ConfigurationManager .getProperty("authentication.shib.email-header"); String fnameHeader = ConfigurationManager .getProperty("authentication.shib.firstname-header"); String lnameHeader = ConfigurationManager .getProperty("authentication.shib.lastname-header"); String email = null; String fname = null; String lname = null; if (emailHeader != null) { // try to grab email from the header email = request.getHeader(emailHeader); // fail, try lower case if (email == null) email = request.getHeader(emailHeader.toLowerCase()); } // try to pull the "REMOTE_USER" info instead of the header if (email == null && isUsingTomcatUser) { email = request.getRemoteUser(); log.info("RemoteUser identified as: " + email); } // No email address, perhaps the eperson has been setup, better check it if (email == null) { EPerson p = context.getCurrentUser(); if (p != null) email = p.getEmail(); } if (email == null) { log .error("No email is given, you're denied access by Shib, please release email address"); return AuthenticationMethod.BAD_ARGS; } email = email.toLowerCase(); if (fnameHeader != null) { // try to grab name from the header fname = request.getHeader(fnameHeader); // fail, try lower case if (fname == null) fname = request.getHeader(fnameHeader.toLowerCase()); } if (lnameHeader != null) { // try to grab name from the header lname = request.getHeader(lnameHeader); // fail, try lower case if (lname == null) lname = request.getHeader(lnameHeader.toLowerCase()); } // future version can offer auto-update feature, this needs testing // before inclusion to core code EPerson eperson = null; try { eperson = EPerson.findByEmail(context, email); context.setCurrentUser(eperson); } catch (AuthorizeException e) { log.warn("Fail to locate user with email:" + email, e); eperson = null; } // auto create user if needed if (eperson == null && ConfigurationManager .getBooleanProperty("authentication.shib.autoregister")) { log.info(LogManager.getHeader(context, "autoregister", "email=" + email)); // TEMPORARILY turn off authorisation context.setIgnoreAuthorization(true); try { eperson = EPerson.create(context); eperson.setEmail(email); if (fname != null) eperson.setFirstName(fname); if (lname != null) eperson.setLastName(lname); eperson.setCanLogIn(true); AuthenticationManager.initEPerson(context, request, eperson); eperson.update(); context.commit(); context.setCurrentUser(eperson); } catch (AuthorizeException e) { log.warn("Fail to authorize user with email:" + email, e); eperson = null; } finally { context.setIgnoreAuthorization(false); } } if (eperson == null) { return AuthenticationMethod.NO_SUCH_USER; } else { // the person exists, just return ok context.setCurrentUser(eperson); request.getSession().setAttribute("shib.authenticated", new Boolean("true")); } return AuthenticationMethod.SUCCESS; } /** * Grab the special groups to be automatically provisioned for the current * user. Currently the mapping for the groups is done one-to-one, future * version can consider the usage of regex for such mapping. */ public int[] getSpecialGroups(Context context, HttpServletRequest request) { // no user logged in or user not logged from shibboleth if (request == null || context.getCurrentUser() == null || request.getSession().getAttribute("shib.authenticated") == null) { return new int[0]; } if (request.getSession().getAttribute("shib.specialgroup") != null) { return (int[]) request.getSession().getAttribute( "shib.specialgroup"); } java.util.Set groups = new java.util.HashSet(); String roleHeader = ConfigurationManager .getProperty("authentication.shib.role-header"); boolean roleHeader_ignoreScope = ConfigurationManager .getBooleanProperty("authentication.shib.role-header.ignore-scope"); if (roleHeader == null || roleHeader.trim().length() == 0) roleHeader = "Shib-EP-UnscopedAffiliation"; // fall back to default String affiliations = request.getHeader(roleHeader); // try again with all lower case...maybe has better luck if (affiliations == null) affiliations = request.getHeader(roleHeader.toLowerCase()); // default role when fully authN but not releasing any roles? String defaultRoles = ConfigurationManager .getProperty("authentication.shib.default-roles"); if (affiliations == null && defaultRoles != null) { affiliations = defaultRoles; } if (affiliations != null) { java.util.StringTokenizer st = new java.util.StringTokenizer( affiliations, ";,"); // do the mapping here while (st.hasMoreTokens()) { String affiliation = st.nextToken().trim(); // strip scope if present and roleHeader_ignoreScope if (roleHeader_ignoreScope) { int index = affiliation.indexOf("@"); if (index != -1) affiliation = affiliation.substring(0,index); } // perform mapping here if necessary String groupLabels = ConfigurationManager .getProperty("authentication.shib.role." + affiliation); if (groupLabels == null || groupLabels.trim().length() == 0) groupLabels = ConfigurationManager .getProperty("authentication.shib.role." + affiliation.toLowerCase()); // revert back to original entry when no mapping is provided if (groupLabels == null) groupLabels = affiliation; String[] labels = groupLabels.split(","); for (int i = 0; i < labels.length; i++) addGroup(groups, context, labels[i].trim()); } } int ids[] = new int[groups.size()]; java.util.Iterator it = groups.iterator(); for (int i = 0; it.hasNext(); i++) ids[i] = ((Integer) it.next()).intValue(); // store the special group, if already transformed from headers // since subsequent header may not have the values anymore if (ids.length != 0) { request.getSession().setAttribute("shib.specialgroup", ids); } return ids; } /** Find dspaceGroup in DSpace database, if found, include it into groups */ private void addGroup(Collection groups, Context context, String dspaceGroup) { try { Group g = Group.findByName(context, dspaceGroup); if (g == null) { // oops - no group defined log.warn(LogManager.getHeader(context, dspaceGroup + " group is not found!! Admin needs to create one!", "requiredGroup=" + dspaceGroup)); groups.add(new Integer(0)); } else { groups.add(new Integer(g.getID())); } log.info("Mapping group: " + dspaceGroup + " to groupID: " + (g == null ? 0 : g.getID())); } catch (SQLException e) { log.error("Mapping group:" + dspaceGroup + " failed with error", e); } } /** * Indicate whether or not a particular self-registering user can set * themselves a password in the profile info form. * * @param context * DSpace context * @param request * HTTP request, in case anything in that is used to decide * @param email * e-mail address of user attempting to register * */ public boolean allowSetPassword(Context context, HttpServletRequest request, String email) throws SQLException { // don't use password at all return false; } /** * Predicate, is this an implicit authentication method. An implicit method * gets credentials from the environment (such as an HTTP request or even * Java system properties) rather than the explicit username and password. * For example, a method that reads the X.509 certificates in an HTTPS * request is implicit. * * @return true if this method uses implicit authentication. */ public boolean isImplicit() { return true; } /** * Indicate whether or not a particular user can self-register, based on * e-mail address. * * @param context * DSpace context * @param request * HTTP request, in case anything in that is used to decide * @param email * e-mail address of user attempting to register * */ public boolean canSelfRegister(Context context, HttpServletRequest request, String username) throws SQLException { return true; } /** * Initialise a new e-person record for a self-registered new user. * * @param context * DSpace context * @param request * HTTP request, in case it's needed * @param eperson * newly created EPerson record - email + information from the * registration form will have been filled out. * */ public void initEPerson(Context context, HttpServletRequest request, EPerson eperson) throws SQLException { } /** * Get login page to which to redirect. Returns URL (as string) to which to * redirect to obtain credentials (either password prompt or e.g. HTTPS port * for client cert.); null means no redirect. * * @param context * DSpace context, will be modified (ePerson set) upon success. * * @param request * The HTTP request that started this operation, or null if not * applicable. * * @param response * The HTTP response from the servlet method. * * @return fully-qualified URL or null */ public String loginPageURL(Context context, HttpServletRequest request, HttpServletResponse response) { return response.encodeRedirectURL(request.getContextPath() + "/shibboleth-login"); } /** * Get title of login page to which to redirect. Returns a <i>message * key</i> that gets translated into the title or label for "login page" (or * null, if not implemented) This title may be used to identify the link to * the login page in a selection menu, when there are multiple ways to * login. * * @param context * DSpace context, will be modified (ePerson set) upon success. * * @return title text. */ public String loginPageTitle(Context context) { return "org.dspace.authenticate.ShibAuthentication.title"; } }