All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dspace.authenticate.ShibAuthentication Maven / Gradle / Ivy

The newest version!
/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.authenticate;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.authenticate.factory.AuthenticateServiceFactory;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataFieldName;
import org.dspace.content.MetadataSchema;
import org.dspace.content.MetadataSchemaEnum;
import org.dspace.content.NonUniqueMetadataException;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.MetadataFieldService;
import org.dspace.content.service.MetadataSchemaService;
import org.dspace.core.Context;
import org.dspace.core.Utils;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;

/**
 * Shibboleth authentication for DSpace
 *
 * Shibboleth is a distributed authentication system for securely authenticating
 * users and passing attributes about the user from one or more identity
 * providers. In the Shibboleth terminology DSpace is a Service Provider which
 * receives authentication information and then based upon that provides a
 * service to the user. With Shibboleth DSpace will require that you use
 * Apache installed with the mod_shib module acting as a proxy for all HTTP
 * requests for your servlet container (typically Tomcat). DSpace will receive
 * authentication information from the mod_shib module through HTTP headers.
 *
 * See for more information on installing and configuring a Shibboleth
 * Service Provider:
 * https://wiki.shibboleth.net/confluence/display/SHIB2/Installation
 *
 * See the DSpace.cfg or DSpace manual for information on how to configure
 * this authentication module.
 *
 * @author Bruc Liong, MELCOE
 * @author Xiang Kevin Li, MELCOE
 * @author Scott Phillips
 */
public class ShibAuthentication implements AuthenticationMethod {
    /**
     * log4j category
     */
    private static final Logger log = LogManager.getLogger(ShibAuthentication.class);

    /**
     * Additional metadata mappings
     **/
    protected Map metadataHeaderMap = null;

    /**
     * Maximum length for eperson metadata fields
     **/
    protected final int NAME_MAX_SIZE = 64;
    protected final int PHONE_MAX_SIZE = 32;

    /**
     * Maximum length for eperson additional metadata fields
     **/
    protected final int METADATA_MAX_SIZE = 1024;

    protected EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
    protected GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();
    protected MetadataFieldService metadataFieldService = ContentServiceFactory.getInstance().getMetadataFieldService();
    protected MetadataSchemaService metadataSchemaService = ContentServiceFactory.getInstance()
                                                                                 .getMetadataSchemaService();
    protected ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();


    /**
     * Authenticate the given or implicit credentials. This is the heart of the
     * authentication method: test the credentials for authenticity, and if
     * accepted, attempt to match (or optionally, create) an
     * EPerson. If an EPerson is found it is set in
     * the Context that was passed.
     *
     * DSpace supports authentication using NetID, or email address. A user's NetID
     * is a unique identifier from the IdP that identifies a particular user. The
     * NetID can be of almost any form such as a unique integer, string, or with
     * Shibboleth 2.0 you can use "targeted ids". You will need to coordinate with
     * your Shibboleth federation or identity provider. There are three ways to
     * supply identity information to DSpace:
     *
     * 1) NetID from Shibboleth Header (best)
     *
     * The NetID-based method is superior because users may change their email
     * address with the identity provider. When this happens DSpace will not be
     * able to associate their new address with their old account.
     *
     * 2) Email address from Shibboleth Header (okay)
     *
     * In the case where a NetID header is not available or not found DSpace
     * will fall back to identifying a user based-upon their email address.
     *
     * 3) Tomcat's Remote User (worst)
     *
     * In the event that neither Shibboleth headers are found then as a last
     * resort DSpace will look at Tomcat's remote user field. This is the least
     * attractive option because Tomcat has no way to supply additional
     * attributes about a user. Because of this the autoregister option is not
     * supported if this method is used.
     *
     * Identity Scheme Migration Strategies:
     *
     * If you are currently using Email based authentication (either 1 or 2) and
     * want to upgrade to NetID based authentication then there is an easy path.
     * Simply enable Shibboleth to pass the NetID attribute and set the netid-header
     * below to the correct value. When a user attempts to log in to DSpace first
     * DSpace will look for an EPerson with the passed NetID, however when this
     * fails DSpace will fall back to email based authentication. Then DSpace will
     * update the user's EPerson account record to set their netid so all future
     * authentications for this user will be based upon netid. One thing to note
     * is that DSpace will prevent an account from switching NetIDs. If an account
     * already has a NetID set and then they try and authenticate with a
     * different NetID the authentication will fail.
     *
     * @param context  DSpace context, will be modified (ePerson set) upon success.
     * @param username Username (or email address) when method is explicit. Use null
     *                 for implicit method.
     * @param password Password for explicit auth, or null for implicit method.
     * @param realm    Not used by Shibboleth-based authentication
     * @param request  The HTTP request that started this operation, or null if not
     *                 applicable.
     * @return One of: SUCCESS, BAD_CREDENTIALS, CERT_REQUIRED, NO_SUCH_USER,
     * BAD_ARGS
     * 

* Meaning:
* SUCCESS - authenticated OK.
* BAD_CREDENTIALS - user exists, but credentials (e.g. passwd) * don't match
* CERT_REQUIRED - not allowed to login this way without X.509 cert. *
* NO_SUCH_USER - user not found using this method.
* BAD_ARGS - user/pw not appropriate for this method * @throws SQLException if database error */ @Override public int authenticate(Context context, String username, String password, String realm, HttpServletRequest request) throws SQLException { // Check if sword compatibility is allowed, and if so see if we can // authenticate based upon a username and password. This is really helpful // if your repo uses Shibboleth but you want some accounts to be able use // sword. This allows this compatibility without installing the password-based // authentication method which has side effects such as allowing users to login // with a username and password from the webui. boolean swordCompatibility = configurationService .getBooleanProperty("authentication-shibboleth.sword.compatibility", true); if (swordCompatibility && username != null && username.length() > 0 && password != null && password.length() > 0) { return swordCompatibility(context, username, password, request); } if (request == null) { log.warn("Unable to authenticate using Shibboleth because the request object is null."); return BAD_ARGS; } // Initialize the additional EPerson metadata. initialize(context); // Log all headers received if debugging is turned on. This is enormously // helpful when debugging shibboleth related problems. if (log.isDebugEnabled()) { log.debug("Starting Shibboleth Authentication"); String message = "Received the following headers:\n"; @SuppressWarnings("unchecked") Enumeration headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); @SuppressWarnings("unchecked") Enumeration headerValues = request.getHeaders(headerName); while (headerValues.hasMoreElements()) { String headerValue = headerValues.nextElement(); message += "" + headerName + "='" + headerValue + "'\n"; } } log.debug(message); } // Should we auto register new users. boolean autoRegister = configurationService.getBooleanProperty("authentication-shibboleth.autoregister", true); // Four steps to authenticate a user try { // Step 1: Identify User EPerson eperson = findEPerson(context, request); // Step 2: Register New User, if necessary if (eperson == null && autoRegister) { eperson = registerNewEPerson(context, request); } if (eperson == null) { return AuthenticationMethod.NO_SUCH_USER; } // Step 3: Update User's Metadata updateEPerson(context, request, eperson); // Step 4: Log the user in. context.setCurrentUser(eperson); request.setAttribute("shib.authenticated", true); AuthenticateServiceFactory.getInstance().getAuthenticationService().initEPerson(context, request, eperson); log.info(eperson.getEmail() + " has been authenticated via shibboleth."); return AuthenticationMethod.SUCCESS; } catch (Throwable t) { // Log the error, and undo the authentication before returning a failure. log.error("Unable to successfully authenticate using shibboleth for user because of an exception.", t); context.setCurrentUser(null); return AuthenticationMethod.NO_SUCH_USER; } } /** * Get list of extra groups that user implicitly belongs to. Note that this * method will be invoked regardless of the authentication status of the * user (logged-in or not) e.g. a group that depends on the client * network-address. * * DSpace is able to place users into pre-defined groups based upon values * received from Shibboleth. Using this option you can place all faculty members * into a DSpace group when the correct affiliation's attribute is provided. * When DSpace does this they are considered 'special groups', these are really * groups but the user's membership within these groups is not recorded in the * database. Each time a user authenticates they are automatically placed within * the pre-defined DSpace group, so if the user loses their affiliation then the * next time they login they will no longer be in the group. * * Depending upon the shibboleth attributed use in the role-header, it may be * scoped. Scoped is shibboleth terminology for identifying where an attribute * originated from. For example a students affiliation may be encoded as * "[email protected]". The part after the @ sign is the scope, and the preceding * value is the value. You may use the whole value or only the value or scope. * Using this you could generate a role for students and one institution * different than students at another institution. Or if you turn on * ignore-scope you could ignore the institution and place all students into * one group. * * The values extracted (a user may have multiple roles) will be used to look * up which groups to place the user into. The groups are defined as * {@code authentication.shib.role.} which is a comma separated list of * DSpace groups. * * @param context A valid DSpace context. * @param request The request that started this operation, or null if not * applicable. * @return array of EPerson-group IDs, possibly 0-length, but never * null. */ @Override public List getSpecialGroups(Context context, HttpServletRequest request) { try { // User has not successfully authenticated via shibboleth. if (request == null || context.getCurrentUser() == null) { return Collections.EMPTY_LIST; } if (context.getSpecialGroups().size() > 0 ) { log.debug("Returning cached special groups."); return context.getSpecialGroups(); } log.debug("Starting to determine special groups"); String[] defaultRoles = configurationService.getArrayProperty("authentication-shibboleth.default-roles"); String roleHeader = configurationService.getProperty("authentication-shibboleth.role-header"); boolean ignoreScope = configurationService .getBooleanProperty("authentication-shibboleth.role-header.ignore-scope", true); boolean ignoreValue = configurationService .getBooleanProperty("authentication-shibboleth.role-header.ignore-value", false); if (ignoreScope && ignoreValue) { throw new IllegalStateException( "Both config parameters for ignoring an roll attributes scope and value are turned on, this is " + "not a permissible configuration. (Note: ignore-scope defaults to true) The configuration " + "parameters are: 'authentication.shib.role-header.ignore-scope' and 'authentication.shib" + ".role-header.ignore-value'"); } // Get the Shib supplied affiliation or use the default affiliation List affiliations = findMultipleAttributes(request, roleHeader); if (affiliations == null) { if (defaultRoles != null) { affiliations = Arrays.asList(defaultRoles); } log.debug( "Failed to find Shibboleth role header, '" + roleHeader + "', falling back to the default roles: " + "'" + StringUtils .join(defaultRoles, ",") + "'"); } else { log.debug("Found Shibboleth role header: '" + roleHeader + "' = '" + affiliations + "'"); } // Loop through each affiliation Set groups = new HashSet<>(); if (affiliations != null) { for (String affiliation : affiliations) { // If we ignore the affiliation's scope then strip the scope if it exists. if (ignoreScope) { int index = affiliation.indexOf('@'); if (index != -1) { affiliation = affiliation.substring(0, index); } } // If we ignore the value, then strip it out so only the scope remains. if (ignoreValue) { int index = affiliation.indexOf('@'); if (index != -1) { affiliation = affiliation.substring(index + 1, affiliation.length()); } } // Get the group names String[] groupNames = configurationService .getArrayProperty("authentication-shibboleth.role." + affiliation); if (groupNames == null || groupNames.length == 0) { groupNames = configurationService .getArrayProperty("authentication-shibboleth.role." + affiliation.toLowerCase()); } if (groupNames == null) { log.debug( "Unable to find role mapping for the value, '" + affiliation + "', there should be a " + "mapping in config/modules/authentication-shibboleth.cfg: role." + affiliation + " =" + " "); continue; } else { log.debug( "Mapping role affiliation to DSpace group: '" + StringUtils.join(groupNames, ",") + "'"); } // Add each group to the list. for (int i = 0; i < groupNames.length; i++) { try { Group group = groupService.findByName(context, groupNames[i].trim()); if (group != null) { groups.add(group); } else { log.debug("Unable to find group: '" + groupNames[i].trim() + "'"); } } catch (SQLException sqle) { log.error( "Exception thrown while trying to lookup affiliation role for group name: '" + groupNames[i] .trim() + "'", sqle); } } // for each groupNames } // foreach affiliations } // if affiliations log.info("Added current EPerson to special groups: " + groups); return new ArrayList<>(groups); } catch (Throwable t) { log.error("Unable to validate any special groups this user may belong too because of an exception.", t); return Collections.EMPTY_LIST; } } /** * 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 * @throws SQLException if database error */ @Override 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. */ @Override public boolean isImplicit() { return false; } /** * 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 username e-mail address of user attempting to register * @throws SQLException if database error */ @Override public boolean canSelfRegister(Context context, HttpServletRequest request, String username) throws SQLException { // Shibboleth will auto create accounts if configured to do so, but that is not // the same as self register. Self register means that the user can sign up for // an account from the web. This is not supported with shibboleth. return false; } /** * Initialize 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. * @throws SQLException if database error */ @Override public void initEPerson(Context context, HttpServletRequest request, EPerson eperson) throws SQLException { // We don't do anything because all our work is done authenticate and special groups. } /** * 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. *

* For Shibboleth, this URL looks like (note 'target' param is URL encoded, but shown as unencoded in this example) * [shibURL]?target=[dspace.server.url]/api/authn/shibboleth?redirectUrl=[dspace.ui.url] *

* This URL is used by the client to redirect directly to Shibboleth for authentication. The "target" param * is then the location (in REST API) where Shibboleth redirects back to. The "redirectUrl" is the path/URL in the * client (e.g. Angular UI) which the REST API redirects the user to (after capturing/storing any auth info from * Shibboleth). * @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 */ @Override public String loginPageURL(Context context, HttpServletRequest request, HttpServletResponse response) { // If this server is configured for lazy sessions then use this to // login, otherwise default to the protected shibboleth url. boolean lazySession = configurationService.getBooleanProperty("authentication-shibboleth.lazysession", false); if ( lazySession ) { String shibURL = getShibURL(request); // Determine the client redirect URL, where to redirect after authenticating. String redirectUrl = null; if (request.getHeader("Referer") != null && StringUtils.isNotBlank(request.getHeader("Referer"))) { redirectUrl = request.getHeader("Referer"); } else if (request.getHeader("X-Requested-With") != null && StringUtils.isNotBlank(request.getHeader("X-Requested-With"))) { redirectUrl = request.getHeader("X-Requested-With"); } // Determine the server return URL, where shib will send the user after authenticating. // We need it to trigger DSpace's ShibbolethLoginFilter so we will extract the user's information, // locally authenticate them & then redirect back to the UI. String returnURL = configurationService.getProperty("dspace.server.url") + "/api/authn/shibboleth" + ((redirectUrl != null) ? "?redirectUrl=" + redirectUrl : ""); try { shibURL += "?target=" + URLEncoder.encode(returnURL, "UTF-8"); } catch (UnsupportedEncodingException uee) { log.error("Unable to generate lazysession authentication",uee); } log.debug("Redirecting user to Shibboleth initiator: " + shibURL); return response.encodeRedirectURL(shibURL); } else { // If we are not using lazy sessions rely on the protected URL. return response.encodeRedirectURL(request.getContextPath() + "/shibboleth-login"); } } @Override public String getName() { return "shibboleth"; } /** * Check if Shibboleth plugin is enabled * @return true if enabled, false otherwise */ public static boolean isEnabled() { final String shibPluginName = new ShibAuthentication().getName(); boolean shibEnabled = false; // Loop through all enabled authentication plugins to see if Shibboleth is one of them. Iterator authenticationMethodIterator = AuthenticateServiceFactory.getInstance().getAuthenticationService().authenticationMethodIterator(); while (authenticationMethodIterator.hasNext()) { if (shibPluginName.equals(authenticationMethodIterator.next().getName())) { shibEnabled = true; break; } } return shibEnabled; } /** * Identify an existing EPerson based upon the shibboleth attributes provided on * the request object. There are three cases where this can occur, each as * a fallback for the previous method. * * 1) NetID from Shibboleth Header (best) * The NetID-based method is superior because users may change their email * address with the identity provider. When this happens DSpace will not be * able to associate their new address with their old account. * * 2) Email address from Shibboleth Header (okay) * In the case where a NetID header is not available or not found DSpace * will fall back to identifying a user based upon their email address. * * 3) Tomcat's Remote User (worst) * In the event that neither Shibboleth headers are found then as a last * resort DSpace will look at Tomcat's remote user field. This is the least * attractive option because Tomcat has no way to supply additional * attributes about a user. Because of this the autoregister option is not * supported if this method is used. * * If successful then the identified EPerson will be returned, otherwise null. * * @param context The DSpace database context * @param request The current HTTP Request * @return The EPerson identified or null. * @throws SQLException if database error * @throws AuthorizeException if authorization error */ protected EPerson findEPerson(Context context, HttpServletRequest request) throws SQLException, AuthorizeException { boolean isUsingTomcatUser = configurationService .getBooleanProperty("authentication-shibboleth.email-use-tomcat-remote-user"); String netidHeader = configurationService.getProperty("authentication-shibboleth.netid-header"); String emailHeader = configurationService.getProperty("authentication-shibboleth.email-header"); EPerson eperson = null; boolean foundNetID = false; boolean foundEmail = false; boolean foundRemoteUser = false; // 1) First, look for a netid header. if (netidHeader != null) { String netid = findSingleAttribute(request, netidHeader); if (netid != null) { foundNetID = true; eperson = ePersonService.findByNetid(context, netid); if (eperson == null) { log.info( "Unable to identify EPerson based upon Shibboleth netid header: '" + netidHeader + "'='" + netid + "'."); } else { log.debug( "Identified EPerson based upon Shibboleth netid header: '" + netidHeader + "'='" + netid + "'" + "."); } } } // 2) Second, look for an email header. if (eperson == null && emailHeader != null) { String email = findSingleAttribute(request, emailHeader); if (email != null) { foundEmail = true; email = email.toLowerCase(); eperson = ePersonService.findByEmail(context, email); if (eperson == null) { log.info( "Unable to identify EPerson based upon Shibboleth email header: '" + emailHeader + "'='" + email + "'."); } else { log.info( "Identified EPerson based upon Shibboleth email header: '" + emailHeader + "'='" + email + "'" + "."); } if (eperson != null && eperson.getNetid() != null) { // If the user has a netID it has been locked to that netid, don't let anyone else try and steal // the account. log.error( "The identified EPerson based upon Shibboleth email header, '" + emailHeader + "'='" + email + "', is locked to another netid: '" + eperson .getNetid() + "'. This might be a possible hacking attempt to steal another users " + "credentials. If the user's netid has changed you will need to manually change it to the " + "correct value or unset it in the database."); eperson = null; } } } // 3) Last, check to see if tomcat is passing a user. if (eperson == null && isUsingTomcatUser) { String email = request.getRemoteUser(); if (email != null) { foundRemoteUser = true; email = email.toLowerCase(); eperson = ePersonService.findByEmail(context, email); if (eperson == null) { log.info("Unable to identify EPerson based upon Tomcat's remote user: '" + email + "'."); } else { log.info("Identified EPerson based upon Tomcat's remote user: '" + email + "'."); } if (eperson != null && eperson.getNetid() != null) { // If the user has a netID it has been locked to that netid, don't let anyone else try and steal // the account. log.error( "The identified EPerson based upon Tomcat's remote user, '" + email + "', is locked to " + "another netid: '" + eperson .getNetid() + "'. This might be a possible hacking attempt to steal another users " + "credentials. If the user's netid has changed you will need to manually change it to the " + "correct value or unset it in the database."); eperson = null; } } } if (!foundNetID && !foundEmail && !foundRemoteUser) { log.error( "Shibboleth authentication was not able to find a NetId, Email, or Tomcat Remote user for which to " + "identify a user from."); } return eperson; } /** * Register a new eperson object. This method is called when no existing user was * found for the NetID or Email and autoregister is enabled. When these conditions * are met this method will create a new eperson object. * * In order to create a new eperson object there is a minimal set of metadata * required: Email, First Name, and Last Name. If we don't have access to these * three pieces of information then we will be unable to create a new eperson * object, such as the case when Tomcat's Remote User field is used to identify * a particular user. * * Note, that this method only adds the minimal metadata. Any additional metadata * will need to be added by the updateEPerson method. * * @param context The current DSpace database context * @param request The current HTTP Request * @return A new eperson object or null if unable to create a new eperson. * @throws SQLException if database error * @throws AuthorizeException if authorization error */ protected EPerson registerNewEPerson(Context context, HttpServletRequest request) throws SQLException, AuthorizeException { // Header names String netidHeader = configurationService.getProperty("authentication-shibboleth.netid-header"); String emailHeader = configurationService.getProperty("authentication-shibboleth.email-header"); String fnameHeader = configurationService.getProperty("authentication-shibboleth.firstname-header"); String lnameHeader = configurationService.getProperty("authentication-shibboleth.lastname-header"); // Header values String netid = findSingleAttribute(request, netidHeader); String email = findSingleAttribute(request, emailHeader); String fname = findSingleAttribute(request, fnameHeader); String lname = findSingleAttribute(request, lnameHeader); if (email == null || (fnameHeader != null && fname == null) || (lnameHeader != null && lname == null)) { // We require that there be an email, first name, and last name. If we // don't have at least these three pieces of information then we fail. String message = "Unable to register new eperson because we are unable to find an email address along " + "with first and last name for the user.\n"; message += " NetId Header: '" + netidHeader + "'='" + netid + "' (Optional) \n"; message += " Email Header: '" + emailHeader + "'='" + email + "' \n"; message += " First Name Header: '" + fnameHeader + "'='" + fname + "' \n"; message += " Last Name Header: '" + lnameHeader + "'='" + lname + "'"; log.error(message); return null; // TODO should this throw an exception? } // Truncate values of parameters that are too big. if (fname != null && fname.length() > NAME_MAX_SIZE) { log.warn( "Truncating eperson's first name because it is longer than " + NAME_MAX_SIZE + ": '" + fname + "'"); fname = fname.substring(0, NAME_MAX_SIZE); } if (lname != null && lname.length() > NAME_MAX_SIZE) { log.warn("Truncating eperson's last name because it is longer than " + NAME_MAX_SIZE + ": '" + lname + "'"); lname = lname.substring(0, NAME_MAX_SIZE); } // Turn off authorizations to create a new user context.turnOffAuthorisationSystem(); EPerson eperson = ePersonService.create(context); // Set the minimum attributes for the new eperson if (netid != null) { eperson.setNetid(netid); } eperson.setEmail(email.toLowerCase()); if (fname != null) { eperson.setFirstName(context, fname); } if (lname != null) { eperson.setLastName(context, lname); } eperson.setCanLogIn(true); // Commit the new eperson AuthenticateServiceFactory.getInstance().getAuthenticationService().initEPerson(context, request, eperson); ePersonService.update(context, eperson); context.dispatchEvents(); // Turn authorizations back on. context.restoreAuthSystemState(); if (log.isInfoEnabled()) { String message = "Auto registered new eperson using Shibboleth-based attributes:"; if (netid != null) { message += " NetId: '" + netid + "'\n"; } message += " Email: '" + email + "' \n"; message += " First Name: '" + fname + "' \n"; message += " Last Name: '" + lname + "'"; log.info(message); } return eperson; } /** * After we successfully authenticated a user, this method will update the user's attributes. The * user's email, name, or other attribute may have been changed since the last time they * logged into DSpace. This method will update the database with their most recent information. * * This method handles the basic DSpace metadata (email, first name, last name) along with * additional metadata set using the setMetadata() methods on the eperson object. The * additional metadata are defined by a mapping created in the dspace.cfg. * * @param context The current DSpace database context * @param request The current HTTP Request * @param eperson The eperson object to update. * @throws SQLException if database error * @throws AuthorizeException if authorization error */ protected void updateEPerson(Context context, HttpServletRequest request, EPerson eperson) throws SQLException, AuthorizeException { // Header names & values String netidHeader = configurationService.getProperty("authentication-shibboleth.netid-header"); String emailHeader = configurationService.getProperty("authentication-shibboleth.email-header"); String fnameHeader = configurationService.getProperty("authentication-shibboleth.firstname-header"); String lnameHeader = configurationService.getProperty("authentication-shibboleth.lastname-header"); String netid = findSingleAttribute(request, netidHeader); String email = findSingleAttribute(request, emailHeader); String fname = findSingleAttribute(request, fnameHeader); String lname = findSingleAttribute(request, lnameHeader); // Truncate values of parameters that are too big. if (fname != null && fname.length() > NAME_MAX_SIZE) { log.warn( "Truncating eperson's first name because it is longer than " + NAME_MAX_SIZE + ": '" + fname + "'"); fname = fname.substring(0, NAME_MAX_SIZE); } if (lname != null && lname.length() > NAME_MAX_SIZE) { log.warn("Truncating eperson's last name because it is longer than " + NAME_MAX_SIZE + ": '" + lname + "'"); lname = lname.substring(0, NAME_MAX_SIZE); } context.turnOffAuthorisationSystem(); // 1) Update the minimum metadata // Only update the netid if none has been previously set. This can occur when a repo switches // to netid based authentication. The current users do not have netids and fall back to email-based // identification but once they login we update their record and lock the account to a particular netid. if (netid != null && eperson.getNetid() == null) { eperson.setNetid(netid); } // The email could have changed if using netid based lookup. if (email != null) { eperson.setEmail(email.toLowerCase()); } if (fname != null) { eperson.setFirstName(context, fname); } if (lname != null) { eperson.setLastName(context, lname); } if (log.isDebugEnabled()) { String message = "Updated the eperson's minimal metadata: \n"; message += " Email Header: '" + emailHeader + "' = '" + email + "' \n"; message += " First Name Header: '" + fnameHeader + "' = '" + fname + "' \n"; message += " Last Name Header: '" + fnameHeader + "' = '" + lname + "'"; log.debug(message); } // 2) Update additional eperson metadata for (String header : metadataHeaderMap.keySet()) { String field = metadataHeaderMap.get(header); String value = findSingleAttribute(request, header); // Truncate values if (value == null) { log.warn("Unable to update the eperson's '{}' metadata" + " because the header '{}' does not exist.", field, header); continue; } else if ("phone".equals(field) && value.length() > PHONE_MAX_SIZE) { log.warn("Truncating eperson phone metadata because it is longer than {}: '{}'", PHONE_MAX_SIZE, value); value = value.substring(0, PHONE_MAX_SIZE); } else if (value.length() > METADATA_MAX_SIZE) { log.warn("Truncating eperson {} metadata because it is longer than {}: '{}'", field, METADATA_MAX_SIZE, value); value = value.substring(0, METADATA_MAX_SIZE); } String[] nameParts = MetadataFieldName.parse(field); ePersonService.setMetadataSingleValue(context, eperson, nameParts[0], nameParts[1], nameParts[2], value, null); log.debug("Updated the eperson's '{}' metadata using header: '{}' = '{}'.", field, header, value); } ePersonService.update(context, eperson); context.dispatchEvents(); context.restoreAuthSystemState(); } /** * Provide password-based authentication to enable sword compatibility. * * Sword compatibility will allow this authentication method to work when using * sword. Sword relies on username and password based authentication and is * entirely incapable of supporting shibboleth. This option allows you to * authenticate username and passwords for sword sessions without adding * another authentication method onto the stack. You will need to ensure that * a user has a password. One way to do that is to create the user via the * create-administrator command line command and then edit their permissions. * * @param context The DSpace database context * @param username The username * @param password The password * @param request The HTTP Request * @return A valid DSpace Authentication Method status code. * @throws SQLException if database error */ protected int swordCompatibility(Context context, String username, String password, HttpServletRequest request) throws SQLException { log.debug("Shibboleth Sword compatibility activated."); EPerson eperson = ePersonService.findByEmail(context, username.toLowerCase()); if (eperson == null) { // lookup failed. log.error( "Shibboleth-based password authentication failed for user " + username + " because no such user " + "exists."); return NO_SUCH_USER; } else if (!eperson.canLogIn()) { // cannot login this way log.error( "Shibboleth-based password authentication failed for user " + username + " because the eperson object" + " is not allowed to login."); return BAD_ARGS; } else if (eperson.getRequireCertificate()) { // this user can only login with x.509 certificate log.error( "Shibboleth-based password authentication failed for user " + username + " because the eperson object" + " requires a certificate to authenticate.."); return CERT_REQUIRED; } else if (ePersonService.checkPassword(context, eperson, password)) { // Password matched AuthenticateServiceFactory.getInstance().getAuthenticationService().initEPerson(context, request, eperson); context.setCurrentUser(eperson); log.info(eperson .getEmail() + " has been authenticated via shibboleth using password-based sword " + "compatibility mode."); return SUCCESS; } else { // Password failure log.error( "Shibboleth-based password authentication failed for user " + username + " because a bad password was" + " supplied."); return BAD_CREDENTIALS; } } /** * Initialize Shibboleth Authentication. * * During initialization the mapping of additional eperson metadata will be loaded from the DSpace.cfg * and cached. While loading the metadata mapping this method will check the EPerson object to see * if it supports the metadata field. If the field is not supported and autocreate is turned on then * the field will be automatically created. * * It is safe to call this methods multiple times. * * @param context context * @throws SQLException if database error */ protected synchronized void initialize(Context context) throws SQLException { if (metadataHeaderMap != null) { return; } HashMap map = new HashMap<>(); String[] mappingString = configurationService.getArrayProperty("authentication-shibboleth.eperson.metadata"); boolean autoCreate = configurationService .getBooleanProperty("authentication-shibboleth.eperson.metadata.autocreate", true); // Bail out if not set, returning an empty map. if (mappingString == null || mappingString.length == 0) { log.debug("No additional eperson metadata mapping found: authentication.shib.eperson.metadata"); metadataHeaderMap = map; return; } log.debug("Loading additional eperson metadata from: 'authentication.shib.eperson.metadata' = '" + StringUtils .join(mappingString, ",") + "'"); for (String metadataString : mappingString) { metadataString = metadataString.trim(); String[] metadataParts = metadataString.split("=>"); if (metadataParts.length != 2) { log.error("Unable to parse metadata mapping string: '" + metadataString + "'"); continue; } String header = metadataParts[0].trim(); String name = metadataParts[1].trim().toLowerCase(); boolean valid = checkIfEpersonMetadataFieldExists(context, name); if (!valid && autoCreate) { valid = autoCreateEpersonMetadataField(context, name); } if (valid) { // The eperson field is fine, we can use it. log.debug("Loading additional eperson metadata mapping for: '{}' = '{}'", header, name); map.put(header, name); } else { // The field doesn't exist, and we can't use it. log.error("Skipping the additional eperson metadata mapping for: '{}' = '{}'" + " because the field is not supported by the current configuration.", header, name); } } // foreach metadataStringList metadataHeaderMap = map; } /** * Check if a MetadataField for an eperson is available. * * @param metadataName The name of the metadata field. * @param context context * @return True if a valid metadata field, otherwise false. * @throws SQLException if database error */ protected synchronized boolean checkIfEpersonMetadataFieldExists(Context context, String metadataName) throws SQLException { if (metadataName == null) { return false; } MetadataField metadataField = metadataFieldService.findByElement(context, MetadataSchemaEnum.EPERSON.getName(), metadataName, null); return metadataField != null; } /** * Validate Postgres Column Names */ protected final String COLUMN_NAME_REGEX = "^[_A-Za-z0-9]+$"; /** * Automatically create a new metadataField for an eperson * * @param context context * @param metadataName The name of the new metadata field. * @return True if successful, otherwise false. * @throws SQLException if database error */ protected synchronized boolean autoCreateEpersonMetadataField(Context context, String metadataName) throws SQLException { if (metadataName == null) { return false; } // The phone is a predefined field if ("phone".equals(metadataName)) { return true; } if (!metadataName.matches(COLUMN_NAME_REGEX)) { return false; } MetadataSchema epersonSchema = metadataSchemaService.find(context, "eperson"); MetadataField metadataField = null; try { context.turnOffAuthorisationSystem(); metadataField = metadataFieldService.create(context, epersonSchema, metadataName, null, null); } catch (AuthorizeException | NonUniqueMetadataException e) { log.error(e.getMessage(), e); return false; } finally { context.restoreAuthSystemState(); } return metadataField != null; } /** * Find a particular Shibboleth header value and return the all values. * The header name uses a bit of fuzzy logic, so it will first try case * sensitive, then it will try lowercase, and finally it will try uppercase. * * This method will not interpret the header value in any way. * * This method will return null if value is empty. * * @param request The HTTP request to look for values in. * @param name The name of the attribute or header * @return The value of the attribute or header requested, or null if none found. */ protected String findAttribute(HttpServletRequest request, String name) { if (name == null) { return null; } // First try to get the value from the attribute String value = (String) request.getAttribute(name); if (StringUtils.isEmpty(value)) { value = (String) request.getAttribute(name.toLowerCase()); } if (StringUtils.isEmpty(value)) { value = (String) request.getAttribute(name.toUpperCase()); } // Second try to get the value from the header if (StringUtils.isEmpty(value)) { value = request.getHeader(name); } if (StringUtils.isEmpty(value)) { value = request.getHeader(name.toLowerCase()); } if (StringUtils.isEmpty(value)) { value = request.getHeader(name.toUpperCase()); } // Added extra check for empty value of an attribute. // In case that value is Empty, it should not be returned, return 'null' instead. // This prevents passing empty value to other methods, stops the authentication process // and prevents creation of 'empty' DSpace EPerson if autoregister == true and it subsequent // authentication. if (StringUtils.isEmpty(value)) { log.debug("ShibAuthentication - attribute " + name + " is empty!"); return null; } boolean reconvertAttributes = configurationService.getBooleanProperty( "authentication-shibboleth.reconvert.attributes", false); if (!StringUtils.isEmpty(value) && reconvertAttributes) { try { value = new String(value.getBytes("ISO-8859-1"), "UTF-8"); } catch (UnsupportedEncodingException ex) { log.warn("Failed to reconvert shibboleth attribute (" + name + ").", ex); } } return value; } /** * Find a particular Shibboleth header value and return the first value. * The header name uses a bit of fuzzy logic, so it will first try case * sensitive, then it will try lowercase, and finally it will try uppercase. * * Shibboleth attributes may contain multiple values separated by a * semicolon. This method will return the first value in the attribute. If * you need multiple values use findMultipleAttributes instead. * * If no attribute is found then null is returned. * * @param request The HTTP request to look for headers values on. * @param name The name of the header * @return The value of the header requested, or null if none found. */ protected String findSingleAttribute(HttpServletRequest request, String name) { if (name == null) { return null; } String value = findAttribute(request, name); if (value != null) { // If there are multiple values encoded in the shibboleth attribute // they are separated by a semicolon, and any semicolons in the // attribute are escaped with a backslash. For this case we are just // looking for the first attribute so we scan the value until we find // the first unescaped semicolon and chop off everything else. int idx = 0; do { idx = value.indexOf(';', idx); if (idx != -1 && value.charAt(idx - 1) != '\\') { value = value.substring(0, idx); break; } } while (idx >= 0); // Unescape the semicolon after splitting value = value.replaceAll("\\;", ";"); } return value; } /** * Find a particular Shibboleth hattributeeader value and return the values. * The attribute name uses a bit of fuzzy logic, so it will first try case * sensitive, then it will try lowercase, and finally it will try uppercase. * * Shibboleth attributes may contain multiple values separated by a * semicolon and semicolons are escaped with a backslash. This method will * split all the attributes into a list and unescape semicolons. * * If no attributes are found then null is returned. * * @param request The HTTP request to look for headers values on. * @param name The name of the attribute * @return The list of values found, or null if none found. */ protected List findMultipleAttributes(HttpServletRequest request, String name) { String values = findAttribute(request, name); if (values == null) { return null; } // Shibboleth attributes are separated by semicolons (and semicolons are // escaped with a backslash). So here we will scan through the string and // split on any unescaped semicolons. List valueList = new ArrayList<>(); int idx = 0; do { idx = values.indexOf(';', idx); if (idx == 0) { // if the string starts with a semicolon just remove it. This will // prevent an endless loop in an error condition. values = values.substring(1, values.length()); } else if (idx > 0 && values.charAt(idx - 1) == '\\') { // The attribute starts with an escaped semicolon idx++; } else if (idx > 0) { // First extract the value and store it on the list. String value = values.substring(0, idx); value = value.replaceAll("\\\\;", ";"); valueList.add(value); // Next, remove the value from the string and continue to scan. values = values.substring(idx + 1, values.length()); idx = 0; } } while (idx >= 0); // The last attribute will still be left on the values string, put it // into the list. if (values.length() > 0) { values = values.replaceAll("\\\\;", ";"); valueList.add(values); } return valueList; } private String getShibURL(HttpServletRequest request) { String shibURL = configurationService.getProperty("authentication-shibboleth.lazysession.loginurl", "/Shibboleth.sso/Login"); boolean forceHTTPS = configurationService.getBooleanProperty("authentication-shibboleth.lazysession.secure", true); // Shibboleth url must be absolute if (shibURL.startsWith("/")) { String serverUrl = Utils.getBaseUrl(configurationService.getProperty("dspace.server.url")); shibURL = serverUrl + shibURL; if ((request.isSecure() || forceHTTPS) && shibURL.startsWith("http://")) { shibURL = shibURL.replace("http://", "https://"); } } return shibURL; } @Override public boolean isUsed(final Context context, final HttpServletRequest request) { if (request != null && context.getCurrentUser() != null && request.getAttribute("shib.authenticated") != null) { return true; } return false; } @Override public boolean canChangePassword(Context context, EPerson ePerson, String currentPassword) { return false; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy