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

org.dspace.authenticate.LDAPAuthentication 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 static org.dspace.eperson.service.EPersonService.MD_PHONE;

import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.StartTlsRequest;
import javax.naming.ldap.StartTlsResponse;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.authenticate.factory.AuthenticateServiceFactory;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
import org.dspace.core.LogHelper;
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;

/**
 * This combined LDAP authentication method supersedes both the 'LDAPAuthentication'
 * and the 'LDAPHierarchicalAuthentication' methods. It's capable of both:
 * 
    *
  • authentication against a flat LDAP tree where all users are in the same unit * (if {@code search.user} or {@code search.password} is not set)
  • *
  • authentication against structured hierarchical LDAP trees of users.
  • *
* An initial bind is required using a user name and password in order to * search the tree and find the DN of the user. A second bind is then required to * check the credentials of the user by binding directly to their DN. * * @author Stuart Lewis * @author Chris Yates * @author Alex Barbieri * @author Flavio Botelho * @author Reuben Pasquini * @author Samuel Ottenhoff * @author Ivan Masár * @author Michael Plate */ public class LDAPAuthentication implements AuthenticationMethod { private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(LDAPAuthentication.class); protected AuthenticationService authenticationService = AuthenticateServiceFactory.getInstance().getAuthenticationService(); protected EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); protected GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); protected ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); private static final String LDAP_AUTHENTICATED = "ldap.authenticated"; /** * Let a real auth method return true if it wants. * * @throws SQLException if database error */ @Override public boolean canSelfRegister(Context context, HttpServletRequest request, String username) throws SQLException { // Looks to see if autoregister is set or not return configurationService.getBooleanProperty("authentication-ldap.autoregister"); } /** * Nothing here, initialization is done when auto-registering. * * @throws SQLException if database error */ @Override public void initEPerson(Context context, HttpServletRequest request, EPerson eperson) throws SQLException { // XXX should we try to initialize netid based on email addr, // XXX for eperson created by some other method?? } /** * Cannot change LDAP password through dspace, right? * * @throws SQLException if database error */ @Override public boolean allowSetPassword(Context context, HttpServletRequest request, String username) throws SQLException { // XXX is this right? return false; } /** * This is an explicit method. */ @Override public boolean isImplicit() { return false; } /** * Add authenticated users to the group defined in dspace.cfg by * the login.specialgroup key. */ @Override public List getSpecialGroups(Context context, HttpServletRequest request) { // Prevents anonymous users from being added to this group, and the second check // ensures they are LDAP users try { // without a logged in user, this method should return an empty list if (context.getCurrentUser() == null) { return Collections.EMPTY_LIST; } // if the logged in user does not have a netid, it's not an LDAP user // and this method should return an empty list if (context.getCurrentUser().getNetid() == null) { return Collections.EMPTY_LIST; } if (!context.getCurrentUser().getNetid().equals("")) { String groupName = configurationService.getProperty("authentication-ldap.login.specialgroup"); if ((groupName != null) && (!groupName.trim().equals(""))) { Group ldapGroup = groupService.findByName(context, groupName); if (ldapGroup == null) { // Oops - the group isn't there. log.warn(LogHelper.getHeader(context, "ldap_specialgroup", "Group defined in login.specialgroup does not exist")); return Collections.EMPTY_LIST; } else { return Arrays.asList(ldapGroup); } } } } catch (SQLException ex) { // The user is not an LDAP user, so we don't need to worry about them } return Collections.EMPTY_LIST; } /** * Authenticate the given 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. * * @param context * DSpace context, will be modified (ePerson set) upon success. * * @param netid * 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 * Realm is an extra parameter used by some authentication methods, leave null if * not applicable. * * @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 */ @Override public int authenticate(Context context, String netid, String password, String realm, HttpServletRequest request) throws SQLException { log.info(LogHelper.getHeader(context, "auth", "attempting trivial auth of user=" + netid)); // Skip out when no netid or password is given. if (netid == null || password == null) { return BAD_ARGS; } // Locate the eperson EPerson eperson = null; try { eperson = ePersonService.findByNetid(context, netid.toLowerCase()); } catch (SQLException e) { // ignore } SpeakerToLDAP ldap = new SpeakerToLDAP(log); // Get the DN of the user boolean anonymousSearch = configurationService.getBooleanProperty("authentication-ldap.search.anonymous"); String adminUser = configurationService.getProperty("authentication-ldap.search.user"); String adminPassword = configurationService.getProperty("authentication-ldap.search.password"); String objectContext = configurationService.getProperty("authentication-ldap.object_context"); String idField = configurationService.getProperty("authentication-ldap.id_field"); String dn = ""; // If adminUser is blank and anonymous search is not allowed, then we can't search so construct the DN // instead of searching it if ((StringUtils.isBlank(adminUser) || StringUtils.isBlank(adminPassword)) && !anonymousSearch) { dn = idField + "=" + netid + "," + objectContext; } else { dn = ldap.getDNOfUser(adminUser, adminPassword, context, netid); } // Check a DN was found if (StringUtils.isBlank(dn)) { log.info(LogHelper .getHeader(context, "failed_login", "no DN found for user " + netid)); return BAD_CREDENTIALS; } // if they entered a netid that matches an eperson if (eperson != null) { // e-mail address corresponds to active account if (eperson.getRequireCertificate()) { return CERT_REQUIRED; } else if (!eperson.canLogIn()) { return BAD_ARGS; } if (ldap.ldapAuthenticate(dn, password, context)) { context.setCurrentUser(eperson); request.setAttribute(LDAP_AUTHENTICATED, true); // update eperson's attributes context.turnOffAuthorisationSystem(); setEpersonAttributes(context, eperson, ldap, Optional.empty()); try { ePersonService.update(context, eperson); context.dispatchEvents(); } catch (AuthorizeException e) { log.warn("update of eperson " + eperson.getID() + " failed", e); } finally { context.restoreAuthSystemState(); } // assign user to groups based on ldap dn assignGroups(dn, ldap.ldapGroup, context); log.info(LogHelper .getHeader(context, "authenticate", "type=ldap")); return SUCCESS; } else { return BAD_CREDENTIALS; } } else { // the user does not already exist so try and authenticate them // with ldap and create an eperson for them if (ldap.ldapAuthenticate(dn, password, context)) { // Register the new user automatically log.info(LogHelper.getHeader(context, "autoregister", "netid=" + netid)); String email = ldap.ldapEmail; // Check if we were able to determine an email address from LDAP if (StringUtils.isEmpty(email)) { // If no email, check if we have a "netid_email_domain". If so, append it to the netid to create // email if (configurationService.hasProperty("authentication-ldap.netid_email_domain")) { email = netid + configurationService.getProperty("authentication-ldap.netid_email_domain"); } else { // We don't have a valid email address. We'll default it to 'netid' but log a warning log.warn(LogHelper.getHeader(context, "autoregister", "Unable to locate email address for account '" + netid + "', so" + " it has been set to '" + netid + "'. " + "Please check the LDAP 'email_field' OR consider " + "configuring 'netid_email_domain'.")); email = netid; } } if (StringUtils.isNotEmpty(email)) { try { eperson = ePersonService.findByEmail(context, email); if (eperson != null) { log.info(LogHelper.getHeader(context, "type=ldap-login", "type=ldap_but_already_email")); context.turnOffAuthorisationSystem(); setEpersonAttributes(context, eperson, ldap, Optional.of(netid)); ePersonService.update(context, eperson); context.dispatchEvents(); context.restoreAuthSystemState(); context.setCurrentUser(eperson); request.setAttribute(LDAP_AUTHENTICATED, true); // assign user to groups based on ldap dn assignGroups(dn, ldap.ldapGroup, context); return SUCCESS; } else { if (canSelfRegister(context, request, netid)) { // TEMPORARILY turn off authorisation try { context.turnOffAuthorisationSystem(); eperson = ePersonService.create(context); setEpersonAttributes(context, eperson, ldap, Optional.of(netid)); eperson.setCanLogIn(true); authenticationService.initEPerson(context, request, eperson); ePersonService.update(context, eperson); context.dispatchEvents(); context.setCurrentUser(eperson); request.setAttribute(LDAP_AUTHENTICATED, true); // assign user to groups based on ldap dn assignGroups(dn, ldap.ldapGroup, context); } catch (AuthorizeException e) { return NO_SUCH_USER; } finally { context.restoreAuthSystemState(); } log.info(LogHelper.getHeader(context, "authenticate", "type=ldap-login, created ePerson")); return SUCCESS; } else { // No auto-registration for valid certs log.info(LogHelper.getHeader(context, "failed_login", "type=ldap_but_no_record")); return NO_SUCH_USER; } } } catch (AuthorizeException e) { eperson = null; } finally { context.restoreAuthSystemState(); } } } } return BAD_ARGS; } /** * Update eperson's attributes */ private void setEpersonAttributes(Context context, EPerson eperson, SpeakerToLDAP ldap, Optional netid) throws SQLException { if (StringUtils.isNotEmpty(ldap.ldapEmail)) { eperson.setEmail(ldap.ldapEmail); } if (StringUtils.isNotEmpty(ldap.ldapGivenName)) { eperson.setFirstName(context, ldap.ldapGivenName); } if (StringUtils.isNotEmpty(ldap.ldapSurname)) { eperson.setLastName(context, ldap.ldapSurname); } if (StringUtils.isNotEmpty(ldap.ldapPhone)) { ePersonService.setMetadataSingleValue(context, eperson, MD_PHONE, ldap.ldapPhone, null); } if (netid.isPresent()) { eperson.setNetid(netid.get().toLowerCase()); } } /** * Internal class to manage LDAP query and results, mainly * because there are multiple values to return. */ private static class SpeakerToLDAP { private Logger log = null; protected String ldapEmail = null; protected String ldapGivenName = null; protected String ldapSurname = null; protected String ldapPhone = null; protected ArrayList ldapGroup = null; /** * LDAP settings */ final String ldap_provider_url; final String ldap_id_field; final String ldap_search_context; final String ldap_search_scope; final String ldap_email_field; final String ldap_givenname_field; final String ldap_surname_field; final String ldap_phone_field; final String ldap_group_field; final boolean useTLS; SpeakerToLDAP(Logger thelog) { ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); log = thelog; ldap_provider_url = configurationService.getProperty("authentication-ldap.provider_url"); ldap_id_field = configurationService.getProperty("authentication-ldap.id_field"); ldap_search_context = configurationService.getProperty("authentication-ldap.search_context"); ldap_search_scope = configurationService.getProperty("authentication-ldap.search_scope"); ldap_email_field = configurationService.getProperty("authentication-ldap.email_field"); ldap_givenname_field = configurationService.getProperty("authentication-ldap.givenname_field"); ldap_surname_field = configurationService.getProperty("authentication-ldap.surname_field"); ldap_phone_field = configurationService.getProperty("authentication-ldap.phone_field"); ldap_group_field = configurationService.getProperty("authentication-ldap.login.groupmap.attribute"); useTLS = configurationService.getBooleanProperty("authentication-ldap.starttls", false); } protected String getDNOfUser(String adminUser, String adminPassword, Context context, String netid) { // The resultant DN String resultDN; // The search scope to use (default to 0) int ldap_search_scope_value = 0; try { ldap_search_scope_value = Integer.parseInt(ldap_search_scope.trim()); } catch (NumberFormatException e) { // Log the error if it has been set but is invalid if (ldap_search_scope != null) { log.warn(LogHelper.getHeader(context, "ldap_authentication", "invalid search scope: " + ldap_search_scope)); } } // Set up environment for creating initial context @SuppressWarnings("UseOfObsoleteCollectionType") Hashtable env = new Hashtable<>(); env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(javax.naming.Context.PROVIDER_URL, ldap_provider_url); LdapContext ctx = null; StartTlsResponse startTLSResponse = null; try { if ((adminUser != null) && (!adminUser.trim().equals("")) && (adminPassword != null) && (!adminPassword.trim().equals(""))) { if (useTLS) { ctx = new InitialLdapContext(env, null); // start TLS startTLSResponse = (StartTlsResponse) ctx .extendedOperation(new StartTlsRequest()); startTLSResponse.negotiate(); // perform simple client authentication ctx.addToEnvironment(javax.naming.Context.SECURITY_AUTHENTICATION, "simple"); ctx.addToEnvironment(javax.naming.Context.SECURITY_PRINCIPAL, adminUser); ctx.addToEnvironment(javax.naming.Context.SECURITY_CREDENTIALS, adminPassword); } else { // Use admin credentials for search// Authenticate env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "simple"); env.put(javax.naming.Context.SECURITY_PRINCIPAL, adminUser); env.put(javax.naming.Context.SECURITY_CREDENTIALS, adminPassword); } } else { // Use anonymous authentication env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "none"); } if (ctx == null) { // Create initial context ctx = new InitialLdapContext(env, null); } Attributes matchAttrs = new BasicAttributes(true); matchAttrs.put(new BasicAttribute(ldap_id_field, netid)); // look up attributes try { SearchControls ctrls = new SearchControls(); ctrls.setSearchScope(ldap_search_scope_value); // Fetch both user attributes '*' (eg. uid, cn) and operational attributes '+' (eg. memberOf) ctrls.setReturningAttributes(new String[] {"*", "+"}); String searchName; if (useTLS) { searchName = ldap_search_context; } else { searchName = ldap_provider_url + ldap_search_context; } @SuppressWarnings("BanJNDI") NamingEnumeration answer = ctx.search( searchName, "(&({0}={1}))", new Object[] {ldap_id_field, netid}, ctrls); while (answer.hasMoreElements()) { SearchResult sr = answer.next(); if (StringUtils.isEmpty(ldap_search_context)) { resultDN = sr.getName(); } else { resultDN = (sr.getName() + "," + ldap_search_context); } String attlist[] = {ldap_email_field, ldap_givenname_field, ldap_surname_field, ldap_phone_field, ldap_group_field}; Attributes atts = sr.getAttributes(); Attribute att; if (attlist[0] != null) { att = atts.get(attlist[0]); if (att != null) { ldapEmail = (String) att.get(); } } if (attlist[1] != null) { att = atts.get(attlist[1]); if (att != null) { ldapGivenName = (String) att.get(); } } if (attlist[2] != null) { att = atts.get(attlist[2]); if (att != null) { ldapSurname = (String) att.get(); } } if (attlist[3] != null) { att = atts.get(attlist[3]); if (att != null) { ldapPhone = (String) att.get(); } } if (attlist[4] != null) { att = atts.get(attlist[4]); if (att != null) { // loop through all groups returned by LDAP ldapGroup = new ArrayList<>(); for (NamingEnumeration val = att.getAll(); val.hasMoreElements(); ) { ldapGroup.add((String) val.next()); } } } if (answer.hasMoreElements()) { // Oh dear - more than one match // Ambiguous user, can't continue } else { log.debug(LogHelper.getHeader(context, "got DN", resultDN)); return resultDN; } } } catch (NamingException e) { // if the lookup fails go ahead and create a new record for them because the authentication // succeeded log.warn(LogHelper.getHeader(context, "ldap_attribute_lookup", "type=failed_search " + e)); } } catch (NamingException | IOException e) { log.warn(LogHelper.getHeader(context, "ldap_authentication", "type=failed_auth " + e)); } finally { // Close the context when we're done try { if (startTLSResponse != null) { startTLSResponse.close(); } if (ctx != null) { ctx.close(); } } catch (NamingException | IOException e) { // ignore } } // No DN match found return null; } /** * contact the ldap server and attempt to authenticate */ protected boolean ldapAuthenticate(String netid, String password, Context context) { if (!password.equals("")) { LdapContext ctx = null; StartTlsResponse startTLSResponse = null; // Set up environment for creating initial context @SuppressWarnings("UseOfObsoleteCollectionType") Hashtable env = new Hashtable<>(); env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(javax.naming.Context.PROVIDER_URL, ldap_provider_url); try { if (useTLS) { ctx = new InitialLdapContext(env, null); // start TLS startTLSResponse = (StartTlsResponse) ctx .extendedOperation(new StartTlsRequest()); startTLSResponse.negotiate(); // perform simple client authentication ctx.addToEnvironment(javax.naming.Context.SECURITY_AUTHENTICATION, "simple"); ctx.addToEnvironment(javax.naming.Context.SECURITY_PRINCIPAL, netid); ctx.addToEnvironment(javax.naming.Context.SECURITY_CREDENTIALS, password); ctx.addToEnvironment(javax.naming.Context.AUTHORITATIVE, "true"); ctx.addToEnvironment(javax.naming.Context.REFERRAL, "follow"); // dummy operation to check if authentication has succeeded @SuppressWarnings("BanJNDI") Attributes trash = ctx.getAttributes(""); } else if (!useTLS) { // Authenticate env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "Simple"); env.put(javax.naming.Context.SECURITY_PRINCIPAL, netid); env.put(javax.naming.Context.SECURITY_CREDENTIALS, password); env.put(javax.naming.Context.AUTHORITATIVE, "true"); env.put(javax.naming.Context.REFERRAL, "follow"); // Try to bind ctx = new InitialLdapContext(env, null); } } catch (NamingException | IOException e) { // something went wrong (like wrong password) so return false log.warn(LogHelper.getHeader(context, "ldap_authentication", "type=failed_auth " + e)); return false; } finally { // Close the context when we're done try { if (startTLSResponse != null) { startTLSResponse.close(); } if (ctx != null) { ctx.close(); } } catch (NamingException | IOException e) { // ignore } } } else { return false; } return true; } } /** * Returns the URL of an external login page which is not applicable for this authn method. * * Note: Prior to DSpace 7, this method return the page of login servlet. * * @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 */ @Override public String loginPageURL(Context context, HttpServletRequest request, HttpServletResponse response) { return null; } @Override public String getName() { return "ldap"; } /** * Add authenticated users to the group defined in dspace.cfg by * the authentication-ldap.login.groupmap.* key. * * @param dn * The string containing distinguished name of the user * * @param group * List of strings with LDAP dn of groups * * @param context * DSpace context */ private void assignGroups(String dn, ArrayList group, Context context) { if (StringUtils.isNotBlank(dn)) { System.out.println("dn:" + dn); int groupmapIndex = 1; String groupMap = configurationService.getProperty("authentication-ldap.login.groupmap." + groupmapIndex); boolean cmp; // groupmap contains the mapping of LDAP groups to DSpace groups // outer loop with the DSpace groups while (groupMap != null) { String t[] = groupMap.split(":"); String ldapSearchString = t[0]; String dspaceGroupName = t[1]; if (group == null) { cmp = StringUtils.containsIgnoreCase(dn, ldapSearchString + ","); if (cmp) { assignGroup(context, groupmapIndex, dspaceGroupName); } } else { // list of strings with dn from LDAP groups // inner loop Iterator groupIterator = group.iterator(); while (groupIterator.hasNext()) { // save the current entry from iterator for further use String currentGroup = groupIterator.next(); // very much the old code from DSpace <= 7.5 if (currentGroup == null) { cmp = StringUtils.containsIgnoreCase(dn, ldapSearchString + ","); } else { cmp = StringUtils.equalsIgnoreCase(currentGroup, ldapSearchString); } if (cmp) { assignGroup(context, groupmapIndex, dspaceGroupName); } } } groupMap = configurationService.getProperty("authentication-ldap.login.groupmap." + ++groupmapIndex); } } } /** * Add the current authenticated user to the specified group * * @param context * DSpace context * * @param groupmapIndex * authentication-ldap.login.groupmap.* key index defined in dspace.cfg * * @param dspaceGroupName * The DSpace group to add the user to */ private void assignGroup(Context context, int groupmapIndex, String dspaceGroupName) { try { Group ldapGroup = groupService.findByName(context, dspaceGroupName); if (ldapGroup != null) { groupService.addMember(context, ldapGroup, context.getCurrentUser()); groupService.update(context, ldapGroup); } else { // The group does not exist log.warn(LogHelper.getHeader(context, "ldap_assignGroupsBasedOnLdapDn", "Group defined in authentication-ldap.login.groupmap." + groupmapIndex + " does not exist :: " + dspaceGroupName)); } } catch (AuthorizeException ae) { log.debug(LogHelper.getHeader(context, "assignGroupsBasedOnLdapDn could not authorize addition to " + "group", dspaceGroupName)); } catch (SQLException e) { log.debug(LogHelper.getHeader(context, "assignGroupsBasedOnLdapDn could not find group", dspaceGroupName)); } } @Override public boolean isUsed(final Context context, final HttpServletRequest request) { if (request != null && context.getCurrentUser() != null && request.getAttribute(LDAP_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