Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.net.URLEncoder;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
Expand All @@ -19,13 +20,13 @@
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.app.util.Util;
import org.dspace.authenticate.AuthenticationMethod;
import org.dspace.authenticate.factory.AuthenticateServiceFactory;
import org.dspace.authorize.AuthorizeException;
Expand Down Expand Up @@ -247,23 +248,24 @@ public int authenticate(Context context, String username, String password,

// Should we auto register new users.
boolean autoRegister = configurationService.getBooleanProperty("authentication-shibboleth.autoregister", true);
String[] netidHeaders = configurationService.getArrayProperty("authentication-shibboleth.netid-header");

// Four steps to authenticate a user
try {
// Step 1: Identify User
EPerson eperson = findEPerson(context, request);
EPerson eperson = findEPerson(context, request, netidHeaders);

// Step 2: Register New User, if necessary
if (eperson == null && autoRegister && !isDuplicateUser) {
eperson = registerNewEPerson(context, request);
eperson = registerNewEPerson(context, request, netidHeaders);
}

if (eperson == null) {
return AuthenticationMethod.NO_SUCH_USER;
}

// Step 3: Update User's Metadata
updateEPerson(context, request, eperson);
updateEPerson(context, request, eperson, netidHeaders);

// Step 4: Log the user in.
context.setCurrentUser(eperson);
Expand Down Expand Up @@ -540,11 +542,11 @@ public static boolean isEnabled() {
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
protected EPerson findEPerson(Context context, HttpServletRequest request) throws SQLException, AuthorizeException {
protected EPerson findEPerson(Context context, HttpServletRequest request, String[] netidHeaders)
throws SQLException {

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;
Expand All @@ -554,26 +556,10 @@ protected EPerson findEPerson(Context context, HttpServletRequest request) throw


// 1) First, look for a netid header.
if (netidHeader != null) {
String org = shibheaders.get_idp();
String netid = Util.formatNetId(findSingleAttribute(request, netidHeader), org);
if (StringUtils.isEmpty(netid)) {
netid = shibheaders.get_single(netidHeader);
}

if (netid != null) {
if (netidHeaders != null) {
eperson = findEpersonByNetId(netidHeaders, shibheaders, eperson, ePersonService, context, true);
if (eperson != 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 + "'" + ".");
}
}
}

Expand Down Expand Up @@ -656,7 +642,6 @@ protected EPerson findEPerson(Context context, HttpServletRequest request) throw
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
Expand All @@ -677,11 +662,10 @@ protected EPerson findEPerson(Context context, HttpServletRequest request) throw
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
protected EPerson registerNewEPerson(Context context, HttpServletRequest request)
protected EPerson registerNewEPerson(Context context, HttpServletRequest request, String[] netidHeaders)
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");
Expand All @@ -694,15 +678,12 @@ protected EPerson registerNewEPerson(Context context, HttpServletRequest request
// CLARIN

// Header values
String netid = Util.formatNetId(findSingleAttribute(request, netidHeader), org);
String netid = getFirstNetId(netidHeaders);
String email = getEmailAcceptedOrNull(findSingleAttribute(request, emailHeader));
String fname = Headers.updateValueByCharset(findSingleAttribute(request, fnameHeader));
String lname = Headers.updateValueByCharset(findSingleAttribute(request, lnameHeader));

// If the values are not in the request headers try to retrieve it from `shibheaders`.
if (StringUtils.isEmpty(netid)) {
netid = shibheaders.get_single(netidHeader);
}
if (StringUtils.isEmpty(email) && Objects.nonNull(clarinVerificationToken)) {
email = clarinVerificationToken.getEmail();
}
Expand All @@ -718,7 +699,7 @@ protected EPerson registerNewEPerson(Context context, HttpServletRequest request
// 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 += " NetId Header: '" + Arrays.toString(netidHeaders) + "'='" + netid + "' (Optional) \n";
message += " Email Header: '" + emailHeader + "'='" + email + "' \n";
message += " First Name Header: '" + fnameHeader + "'='" + fname + "' \n";
message += " Last Name Header: '" + lnameHeader + "'='" + lname + "'";
Expand Down Expand Up @@ -807,24 +788,20 @@ protected EPerson registerNewEPerson(Context context, HttpServletRequest request
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
protected void updateEPerson(Context context, HttpServletRequest request, EPerson eperson)
protected void updateEPerson(Context context, HttpServletRequest request, EPerson eperson, String[] netidHeaders)
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 = Util.formatNetId(findSingleAttribute(request, netidHeader), shibheaders.get_idp());
String netid = getFirstNetId(netidHeaders);
String email = getEmailAcceptedOrNull(findSingleAttribute(request, emailHeader));
String fname = Headers.updateValueByCharset(findSingleAttribute(request, fnameHeader));
String lname = Headers.updateValueByCharset(findSingleAttribute(request, lnameHeader));

// If the values are not in the request headers try to retrieve it from `shibheaders`.
if (StringUtils.isEmpty(netid)) {
netid = shibheaders.get_single(netidHeader);
}
if (StringUtils.isEmpty(email) && Objects.nonNull(clarinVerificationToken)) {
email = clarinVerificationToken.getEmail();
}
Expand Down Expand Up @@ -858,7 +835,16 @@ protected void updateEPerson(Context context, HttpServletRequest request, EPerso
}
// The email could have changed if using netid based lookup.
if (email != null) {
eperson.setEmail(email.toLowerCase());
String lowerCaseEmail = email.toLowerCase();
// Check the email is unique
EPerson epersonByEmail = ePersonService.findByEmail(context, lowerCaseEmail);
if (epersonByEmail != null && !epersonByEmail.getID().equals(eperson.getID())) {
log.error("Unable to update the eperson's email metadata because the email '{}' is already in use.",
lowerCaseEmail);
throw new AuthorizeException("The email address is already in use.");
} else {
eperson.setEmail(email.toLowerCase());
}
}
if (fname != null) {
eperson.setFirstName(context, fname);
Expand Down Expand Up @@ -1207,29 +1193,11 @@ public 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("\\;", ";");
value = sortEmailsAndGetFirst(value);
}

return value;
}

Expand Down Expand Up @@ -1338,5 +1306,70 @@ public String getEmailAcceptedOrNull(String email) {
}
return email;
}

/**
* Find an EPerson by a NetID header. The method will go through all the netid headers and try to find a user.
*/
public static EPerson findEpersonByNetId(String[] netidHeaders, ShibHeaders shibheaders, EPerson eperson,
EPersonService ePersonService, Context context, boolean logAllowed)
throws SQLException {
// Go through all the netid headers and try to find a user. It could be e.g., `eppn`, `persistent-id`,..
for (String netidHeader : netidHeaders) {
netidHeader = netidHeader.trim();
String netid = shibheaders.get_single(netidHeader);
if (netid == null) {
continue;
}

eperson = ePersonService.findByNetid(context, netid);

if (eperson == null && logAllowed) {
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 + "'" + ".");
}
}
return eperson;
}

/**
* Sort the email addresses and return the first one.
* @param value The email addresses separated by semicolons.
*/
public static String sortEmailsAndGetFirst(String value) {
// 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.
// Step 1: Split the input string into email addresses
List<String> emails = Arrays.stream(value.split("(?<!\\\\);")) // Split by unescaped semicolon
.map(email -> email.replaceAll("\\\\;", ";")) // Unescape semicolons
.collect(Collectors.toList());

// Step 2: Sort the email list alphabetically
emails.sort(String::compareToIgnoreCase);

// Step 3: Get the first sorted email
return emails.get(0);
}

/**
* Get the first netid from the list of netid headers. E.g., eppn, persistent-id,...
* @param netidHeaders list of netid headers loaded from the configuration `authentication-shibboleth.netid-header`
*/
public String getFirstNetId(String[] netidHeaders) {
for (String netidHeader : netidHeaders) {
netidHeader = netidHeader.trim();
String netid = shibheaders.get_single(netidHeader);
if (netid != null) {
//When creating use first match (eppn before targeted-id)
return netid;
}
}
return null;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.Map;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand All @@ -32,7 +33,7 @@ public class ShibHeaders {
// constants
//
private static final String header_separator_ = ";";
private String netIdHeader = "";
private String[] netIdHeaders = null;
ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();

// variables
Expand Down Expand Up @@ -105,7 +106,7 @@ public String get_single(String name) {
List<String> values = get(name);
if (values != null && !values.isEmpty()) {
// Format netId
if (StringUtils.equals(name, this.netIdHeader)) {
if (ArrayUtils.contains(this.netIdHeaders, name)) {
return Util.formatNetId(values.get(0), this.get_idp());
}
return values.get(0);
Expand Down Expand Up @@ -150,6 +151,10 @@ public void log_headers() {
}

private void initializeNetIdHeader() {
this.netIdHeader = configurationService.getProperty("authentication-shibboleth.netid-header");
this.netIdHeaders = configurationService.getArrayProperty("authentication-shibboleth.netid-header");
}

public String[] getNetIdHeaders() {
return this.netIdHeaders;
}
}
2 changes: 1 addition & 1 deletion dspace-api/src/test/data/dspaceFolder/config/local.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ featured.service.teitok.description = A web-based platform for viewing, creating


##### Shibboleth #####
authentication-shibboleth.netid-header = SHIB-NETID
authentication-shibboleth.netid-header = SHIB-NETID,eppn,persistent-id
authentication-shibboleth.email-header = SHIB-MAIL
authentication-shibboleth.firstname-header = SHIB-GIVENNAME
authentication-shibboleth.lastname-header = SHIB-SURNAME
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ public Authentication attemptAuthentication(HttpServletRequest req,

// If the Idp doesn't send the email in the request header, send the redirect order to the FE for the user
// to fill in the email.
String netidHeader = configurationService.getProperty("authentication-shibboleth.netid-header");
String emailHeader = configurationService.getProperty("authentication-shibboleth.email-header");

Context context = ContextUtil.obtainContext(req);
Expand Down Expand Up @@ -154,34 +153,34 @@ public Authentication attemptAuthentication(HttpServletRequest req,
shib_headers = new ShibHeaders(req);
}

// Retrieve the netid and email values from the header.
String netid = shib_headers.get_single(netidHeader);
String idp = shib_headers.get_idp();
// If the clarin verification object is not null load the email from there otherwise from header.
String email = Objects.isNull(clarinVerificationToken) ?
shib_headers.get_single(emailHeader) : clarinVerificationToken.getEmail();
String email;
if (Objects.isNull(clarinVerificationToken)) {
email = shib_headers.get_single(emailHeader);
if (StringUtils.isNotEmpty(email)) {
email = ClarinShibAuthentication.sortEmailsAndGetFirst(email);
}
} else {
email = clarinVerificationToken.getEmail();
}

EPerson ePerson = null;
if (StringUtils.isNotEmpty(netid)) {
try {
ePerson = ClarinShibAuthentication.findEpersonByNetId(shib_headers.getNetIdHeaders(), shib_headers, ePerson,
ePersonService, context, false);
} catch (SQLException e) {
// It is logged in the ClarinShibAuthentication class.
}

if (Objects.isNull(ePerson) && StringUtils.isNotEmpty(email)) {
try {
// If email is null and netid exist try to find the eperson by netid and load its email
if (StringUtils.isEmpty(email)) {
ePerson = ePersonService.findByNetid(context, netid);
email = Objects.isNull(email) ? this.getEpersonEmail(ePerson) : null;
} else {
// Try to get user by the email because of possible duplicate of the user email
ePerson = ePersonService.findByEmail(context, email);
}
} catch (SQLException ignored) {
//
ePerson = ePersonService.findByEmail(context, email);
} catch (SQLException e) {
// It is logged in the ClarinShibAuthentication class.
}
}

// logging
log.info("Shib-Identity-Provider: " + idp);
log.info("authentication-shibboleth.netid-header: " + netidHeader + " with value: " + netid);
log.info("authentication-shibboleth.email-header: " + emailHeader + " with value: " + email);

try {
if (StringUtils.isEmpty(idp)) {
log.error("Cannot load the idp from the request headers.");
Expand Down
Loading