mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-07 01:54:22 +00:00
Add SAML login filter.
This commit is contained in:
@@ -0,0 +1,750 @@
|
||||
/**
|
||||
* 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.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
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.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.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;
|
||||
|
||||
/**
|
||||
* SAML authentication for DSpace.
|
||||
*
|
||||
* @author Ray Lee
|
||||
*/
|
||||
public class SamlAuthentication implements AuthenticationMethod {
|
||||
private static final Logger log = LogManager.getLogger(SamlAuthentication.class);
|
||||
|
||||
// Additional metadata mappings.
|
||||
protected Map<String, String> metadataHeaderMap = null;
|
||||
|
||||
// Maximum length for ePerson 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
|
||||
* <code>EPerson</code>. If an <code>EPerson</code> is found it is set in
|
||||
* the <code>Context</code> 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 or string. In
|
||||
* SAML, this is referred to as a Name ID.
|
||||
*
|
||||
* There are two ways to supply identity information to DSpace:
|
||||
*
|
||||
* 1) Name ID from SAML attribute (best)
|
||||
*
|
||||
* The Name ID-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 SAML attribute (okay)
|
||||
*
|
||||
* In the case where a Name ID header is not available or not found DSpace
|
||||
* will fall back to identifying a user based upon their email address.
|
||||
*
|
||||
* 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.
|
||||
* Coordinate with the IdP to provide a Name ID in the SAML assertion. When a
|
||||
* user attempts to log in, DSpace will first look for an EPerson with the
|
||||
* passed Name ID. 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.
|
||||
*
|
||||
* DSpace will prevent an account from switching NetIDs. If an account already
|
||||
* has a NetID set, and a user tries to authenticate with the same email but
|
||||
* a different NetID, the authentication will fail.
|
||||
*
|
||||
* @param context DSpace context, will be modified (EPerson set) upon success.
|
||||
* @param username Not used by SAML-based authentication.
|
||||
* @param password Not used by SAML-based authentication.
|
||||
* @param realm Not used by SAML-based authentication.
|
||||
* @param request The HTTP request that started this operation.
|
||||
* @return one of: SUCCESS, NO_SUCH_USER, BAD_ARGS
|
||||
* @throws SQLException if a database error occurs.
|
||||
*/
|
||||
@Override
|
||||
public int authenticate(Context context, String username, String password,
|
||||
String realm, HttpServletRequest request) throws SQLException {
|
||||
|
||||
if (request == null) {
|
||||
log.warn("Unable to authenticate using SAML because the request object is null.");
|
||||
|
||||
return BAD_ARGS;
|
||||
}
|
||||
|
||||
// Initialize additional EPerson metadata mappings.
|
||||
|
||||
initialize(context);
|
||||
|
||||
String nameId = findSingleAttribute(request, getNameIdAttributeName());
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Starting SAML Authentication");
|
||||
log.debug("Received name ID: " + nameId);
|
||||
}
|
||||
|
||||
// Should we auto register new users?
|
||||
|
||||
boolean autoRegister = configurationService.getBooleanProperty("authentication-saml.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;
|
||||
}
|
||||
|
||||
if (!eperson.canLogIn()) {
|
||||
return AuthenticationMethod.BAD_ARGS;
|
||||
}
|
||||
|
||||
// Step 3: Update user's metadata
|
||||
|
||||
updateEPerson(context, request, eperson);
|
||||
|
||||
// Step 4: Log the user in
|
||||
|
||||
context.setCurrentUser(eperson);
|
||||
|
||||
request.setAttribute("saml.authenticated", true);
|
||||
|
||||
AuthenticateServiceFactory.getInstance().getAuthenticationService().initEPerson(context, request, eperson);
|
||||
|
||||
log.info(eperson.getEmail() + " has been authenticated via SAML.");
|
||||
|
||||
return AuthenticationMethod.SUCCESS;
|
||||
} catch (Throwable t) {
|
||||
// Log the error, and undo the authentication before returning a failure.
|
||||
|
||||
log.error("Unable to successfully authenticate using SAML for user because of an exception.", t);
|
||||
|
||||
context.setCurrentUser(null);
|
||||
|
||||
return AuthenticationMethod.NO_SUCH_USER;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Group> getSpecialGroups(Context context, HttpServletRequest request) throws SQLException {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowSetPassword(Context context, HttpServletRequest request, String email) throws SQLException {
|
||||
// SAML authentication doesn't use a password.
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isImplicit() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSelfRegister(Context context, HttpServletRequest request,
|
||||
String username) throws SQLException {
|
||||
|
||||
// SAML 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 SAML.
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initEPerson(Context context, HttpServletRequest request,
|
||||
EPerson eperson) throws SQLException {
|
||||
// We don't do anything because all our work is done in authenticate.
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL in the SAML relying party service that initiates a login with the IdP,
|
||||
* as configured.
|
||||
*
|
||||
* @see AuthenticationMethod#loginPageURL(Context, HttpServletRequest, HttpServletResponse)
|
||||
*/
|
||||
@Override
|
||||
public String loginPageURL(Context context, HttpServletRequest request, HttpServletResponse response) {
|
||||
String samlLoginUrl = configurationService.getProperty("authentication-saml.authenticate-endpoint");
|
||||
|
||||
return response.encodeRedirectURL(samlLoginUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "saml";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the SAML plugin is enabled.
|
||||
*
|
||||
* @return true if enabled, false otherwise
|
||||
*/
|
||||
public static boolean isEnabled() {
|
||||
final String samlPluginName = new SamlAuthentication().getName();
|
||||
boolean samlEnabled = false;
|
||||
|
||||
// Loop through all enabled authentication plugins to see if SAML is one of them.
|
||||
|
||||
Iterator<AuthenticationMethod> authenticationMethodIterator =
|
||||
AuthenticateServiceFactory.getInstance().getAuthenticationService().authenticationMethodIterator();
|
||||
|
||||
while (authenticationMethodIterator.hasNext()) {
|
||||
if (samlPluginName.equals(authenticationMethodIterator.next().getName())) {
|
||||
samlEnabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return samlEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify an existing EPerson based upon the SAML attributes provided on
|
||||
* the request object.
|
||||
*
|
||||
* 1) Name ID from SAML attribute (best)
|
||||
* The Name ID-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 SAML attribute (okay)
|
||||
* In the case where a Name ID header is not available or not found DSpace
|
||||
* will fall back to identifying a user based upon their email address.
|
||||
*
|
||||
* 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 {
|
||||
String nameId = findSingleAttribute(request, getNameIdAttributeName());
|
||||
|
||||
if (nameId != null) {
|
||||
EPerson ePerson = ePersonService.findByNetid(context, nameId);
|
||||
|
||||
if (ePerson == null) {
|
||||
log.info("Unable to identify EPerson by netid (SAML name ID): " + nameId);
|
||||
} else {
|
||||
log.info("Identified EPerson by netid (SAML name ID): " + nameId);
|
||||
|
||||
return ePerson;
|
||||
}
|
||||
}
|
||||
|
||||
String emailAttributeName = getEmailAttributeName();
|
||||
String email = findSingleAttribute(request, emailAttributeName);
|
||||
|
||||
if (email != null) {
|
||||
email = email.toLowerCase();
|
||||
|
||||
EPerson ePerson = ePersonService.findByEmail(context, email);
|
||||
|
||||
if (ePerson == null) {
|
||||
log.info("Unable to identify EPerson by email: " + emailAttributeName + "=" + email);
|
||||
} else {
|
||||
log.info("Identified EPerson by email: " + emailAttributeName + "=" + email);
|
||||
|
||||
if (ePerson.getNetid() == null) {
|
||||
return ePerson;
|
||||
}
|
||||
|
||||
// The user has a netid that differs from the received SAML name ID.
|
||||
|
||||
log.error("SAML authentication identified EPerson by email: " + emailAttributeName + "=" + email);
|
||||
log.error("Received SAML name ID: " + nameId);
|
||||
log.error("EPerson has netid: " + ePerson.getNetid());
|
||||
log.error(
|
||||
"The SAML name ID is expected to be the same as the EPerson netid. " +
|
||||
"This might be a hacking attempt to steal another user's 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.");
|
||||
}
|
||||
}
|
||||
|
||||
if (nameId == null && email == null) {
|
||||
log.error(
|
||||
"SAML authentication did not find a name ID or email in the request from which to indentify a user");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register a new EPerson. 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.
|
||||
*
|
||||
* 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 {
|
||||
|
||||
String nameId = findSingleAttribute(request, getNameIdAttributeName());
|
||||
|
||||
String emailAttributeName = getEmailAttributeName();
|
||||
String firstNameAttributeName = getFirstNameAttributeName();
|
||||
String lastNameAttributeName = getLastNameAttributeName();
|
||||
|
||||
String email = findSingleAttribute(request, emailAttributeName);
|
||||
String firstName = findSingleAttribute(request, firstNameAttributeName);
|
||||
String lastName = findSingleAttribute(request, lastNameAttributeName);
|
||||
|
||||
if (email == null || firstName == null || lastName == null) {
|
||||
// We require that there be an email, first name, and last name.
|
||||
|
||||
String message = "Unable to register new eperson because we are unable to find an email address, " +
|
||||
"first name, and last name for the user.\n";
|
||||
|
||||
message += " name ID: " + nameId + "\n";
|
||||
message += " email: " + emailAttributeName + "=" + email + "\n";
|
||||
message += " first name: " + firstNameAttributeName + "=" + firstName + "\n";
|
||||
message += " last name: " + lastNameAttributeName + "=" + lastName;
|
||||
|
||||
log.error(message);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Truncate values of parameters that are too big.
|
||||
|
||||
if (firstName.length() > NAME_MAX_SIZE) {
|
||||
log.warn(
|
||||
"Truncating eperson's first name because it is longer than " + NAME_MAX_SIZE + ": " + firstName);
|
||||
|
||||
firstName = firstName.substring(0, NAME_MAX_SIZE);
|
||||
}
|
||||
if (lastName.length() > NAME_MAX_SIZE) {
|
||||
log.warn("Truncating eperson's last name because it is longer than " + NAME_MAX_SIZE + ": " + lastName);
|
||||
|
||||
lastName = lastName.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 (nameId != null) {
|
||||
ePerson.setNetid(nameId);
|
||||
}
|
||||
|
||||
ePerson.setEmail(email.toLowerCase());
|
||||
ePerson.setFirstName(context, firstName);
|
||||
ePerson.setLastName(context, lastName);
|
||||
ePerson.setCanLogIn(true);
|
||||
ePerson.setSelfRegistered(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 SAML attributes:\n";
|
||||
|
||||
message += " netid: " + ePerson.getNetid() + "\n";
|
||||
message += " email: " + ePerson.getEmail() + "\n";
|
||||
message += " firstName: " + ePerson.getFirstName() + "\n";
|
||||
message += " lastName: " + ePerson.getLastName();
|
||||
|
||||
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 mappings are defined in configuration.
|
||||
*
|
||||
* @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 {
|
||||
|
||||
String nameId = findSingleAttribute(request, getNameIdAttributeName());
|
||||
|
||||
String emailAttributeName = getEmailAttributeName();
|
||||
String firstNameAttributeName = getFirstNameAttributeName();
|
||||
String lastNameAttributeName = getLastNameAttributeName();
|
||||
|
||||
String email = findSingleAttribute(request, emailAttributeName);
|
||||
String firstName = findSingleAttribute(request, firstNameAttributeName);
|
||||
String lastName = findSingleAttribute(request, lastNameAttributeName);
|
||||
|
||||
// Truncate values of parameters that are too big.
|
||||
|
||||
if (firstName != null && firstName.length() > NAME_MAX_SIZE) {
|
||||
log.warn(
|
||||
"Truncating eperson's first name because it is longer than " + NAME_MAX_SIZE + ": " + firstName);
|
||||
|
||||
firstName = firstName.substring(0, NAME_MAX_SIZE);
|
||||
}
|
||||
|
||||
if (lastName != null && lastName.length() > NAME_MAX_SIZE) {
|
||||
log.warn("Truncating eperson's last name because it is longer than " + NAME_MAX_SIZE + ": " + lastName);
|
||||
|
||||
lastName = lastName.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 (nameId != null && eperson.getNetid() == null) {
|
||||
eperson.setNetid(nameId);
|
||||
}
|
||||
|
||||
// The email could have changed if using netid based lookup.
|
||||
|
||||
if (email != null) {
|
||||
eperson.setEmail(email.toLowerCase());
|
||||
}
|
||||
|
||||
if (firstName != null) {
|
||||
eperson.setFirstName(context, firstName);
|
||||
}
|
||||
|
||||
if (lastName != null) {
|
||||
eperson.setLastName(context, lastName);
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
String message = "Updated the eperson's minimal metadata: \n";
|
||||
|
||||
message += " Email: " + emailAttributeName + "=" + email + "' \n";
|
||||
message += " First name: " + firstNameAttributeName + "=" + firstName + "\n";
|
||||
message += " Last name: " + lastNameAttributeName + "=" + lastName;
|
||||
|
||||
log.debug(message);
|
||||
}
|
||||
|
||||
// 2) Update additional eperson metadata
|
||||
|
||||
for (String attributeName : metadataHeaderMap.keySet()) {
|
||||
String metadataFieldName = metadataHeaderMap.get(attributeName);
|
||||
String value = findSingleAttribute(request, attributeName);
|
||||
|
||||
// Truncate values
|
||||
|
||||
if (value == null) {
|
||||
log.warn("Unable to update the eperson's '{}' metadata"
|
||||
+ " because the attribute '{}' does not exist.", metadataFieldName, attributeName);
|
||||
continue;
|
||||
} else if ("phone".equals(metadataFieldName) && 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 {}: {}",
|
||||
metadataFieldName, METADATA_MAX_SIZE, value);
|
||||
value = value.substring(0, METADATA_MAX_SIZE);
|
||||
}
|
||||
|
||||
ePersonService.setMetadataSingleValue(context, eperson,
|
||||
MetadataSchemaEnum.EPERSON.getName(), metadataFieldName, null, null, value);
|
||||
|
||||
log.debug("Updated the eperson's {} metadata using attribute: {}={}",
|
||||
metadataFieldName, attributeName, value);
|
||||
}
|
||||
|
||||
ePersonService.update(context, eperson);
|
||||
|
||||
context.dispatchEvents();
|
||||
context.restoreAuthSystemState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize SAML Authentication.
|
||||
*
|
||||
* During initalization the mapping of additional EPerson metadata will be loaded from the configuration
|
||||
* 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 method multiple times.
|
||||
*
|
||||
* @param context context
|
||||
* @throws SQLException if database error
|
||||
*/
|
||||
protected synchronized void initialize(Context context) throws SQLException {
|
||||
if (metadataHeaderMap != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HashMap<String, String> map = new HashMap<>();
|
||||
|
||||
String[] mappingString = configurationService.getArrayProperty("authentication-saml.eperson.metadata");
|
||||
|
||||
boolean autoCreate = configurationService
|
||||
.getBooleanProperty("authentication-saml.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-saml.eperson.metadata");
|
||||
|
||||
metadataHeaderMap = map;
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Loading additional eperson metadata from: authentication-saml.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 attributeName = metadataParts[0].trim();
|
||||
String metadataFieldName = metadataParts[1].trim().toLowerCase();
|
||||
|
||||
boolean valid = checkIfEpersonMetadataFieldExists(context, metadataFieldName);
|
||||
|
||||
if (!valid && autoCreate) {
|
||||
valid = autoCreateEPersonMetadataField(context, metadataFieldName);
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
// The eperson field is fine, we can use it.
|
||||
|
||||
log.debug("Loading additional eperson metadata mapping for: {}={}",
|
||||
attributeName, metadataFieldName);
|
||||
|
||||
map.put(attributeName, metadataFieldName);
|
||||
} 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.",
|
||||
attributeName, metadataFieldName);
|
||||
}
|
||||
}
|
||||
|
||||
metadataHeaderMap = map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a metadata field 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 metadata field 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUsed(final Context context, final HttpServletRequest request) {
|
||||
if (request != null &&
|
||||
context.getCurrentUser() != null &&
|
||||
request.getAttribute("saml.authenticated") != null
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canChangePassword(Context context, EPerson ePerson, String currentPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private String findSingleAttribute(HttpServletRequest request, String name) {
|
||||
if (StringUtils.isBlank(name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object value = request.getAttribute(name);
|
||||
|
||||
if (value instanceof List) {
|
||||
List<?> list = (List<?>) value;
|
||||
|
||||
if (list.size() == 0) {
|
||||
value = null;
|
||||
} else {
|
||||
value = list.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
return (value == null ? null : value.toString());
|
||||
}
|
||||
|
||||
private String getNameIdAttributeName() {
|
||||
return configurationService.getProperty("authentication-saml.attribute.name-id");
|
||||
}
|
||||
|
||||
private String getEmailAttributeName() {
|
||||
return configurationService.getProperty("authentication-saml.attribute.email");
|
||||
}
|
||||
|
||||
private String getFirstNameAttributeName() {
|
||||
return configurationService.getProperty("authentication-saml.attribute.first-name");
|
||||
}
|
||||
|
||||
private String getLastNameAttributeName() {
|
||||
return configurationService.getProperty("authentication-saml.attribute.last-name");
|
||||
}
|
||||
}
|
@@ -0,0 +1,589 @@
|
||||
/**
|
||||
* 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.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.dspace.AbstractUnitTest;
|
||||
import org.dspace.builder.AbstractBuilder;
|
||||
import org.dspace.builder.EPersonBuilder;
|
||||
import org.dspace.content.MetadataValue;
|
||||
import org.dspace.eperson.EPerson;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.dspace.services.factory.DSpaceServicesFactory;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
public class SamlAuthenticationTest extends AbstractUnitTest {
|
||||
private static ConfigurationService configurationService;
|
||||
|
||||
private HttpServletRequest request;
|
||||
private SamlAuthentication samlAuth;
|
||||
private EPerson testUser;
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeAll() {
|
||||
configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
|
||||
|
||||
AbstractBuilder.init(); // AbstractUnitTest doesn't do this for us.
|
||||
}
|
||||
|
||||
@Before
|
||||
public void beforeEach() throws Exception {
|
||||
configurationService.setProperty("authentication-saml.autoregister", true);
|
||||
configurationService.setProperty("authentication-saml.eperson.metadata.autocreate", true);
|
||||
|
||||
request = new MockHttpServletRequest();
|
||||
samlAuth = new SamlAuthentication();
|
||||
testUser = null;
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterEach() throws Exception {
|
||||
if (testUser != null) {
|
||||
EPersonBuilder.deleteEPerson(testUser.getID());
|
||||
}
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterAll() {
|
||||
AbstractBuilder.destroy(); // AbstractUnitTest doesn't do this for us.
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticateExistingUserByEmail() throws Exception {
|
||||
context.setCurrentUser(null);
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("alyssa@dspace.org")
|
||||
.withNameInMetadata("Alyssa", "Hacker")
|
||||
.withCanLogin(true)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
request.setAttribute("org.dspace.saml.EMAIL", List.of("alyssa@dspace.org"));
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.SUCCESS, result);
|
||||
|
||||
EPerson user = context.getCurrentUser();
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("alyssa@dspace.org", user.getEmail());
|
||||
assertNull(user.getNetid());
|
||||
assertEquals("Alyssa", user.getFirstName());
|
||||
assertEquals("Hacker", user.getLastName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticateExistingUserByNetId() throws Exception {
|
||||
context.setCurrentUser(null);
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("alyssa@dspace.org")
|
||||
.withNetId("001")
|
||||
.withNameInMetadata("Alyssa", "Hacker")
|
||||
.withCanLogin(true)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "001");
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.SUCCESS, result);
|
||||
|
||||
EPerson user = context.getCurrentUser();
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("alyssa@dspace.org", user.getEmail());
|
||||
assertEquals("001", user.getNetid());
|
||||
assertEquals("Alyssa", user.getFirstName());
|
||||
assertEquals("Hacker", user.getLastName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticateExistingUserByEmailWithUnexpectedNetId() throws Exception {
|
||||
EPerson originalUser = context.getCurrentUser();
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("ben@dspace.org")
|
||||
.withNetId("002")
|
||||
.withNameInMetadata("Ben", "Bitdiddle")
|
||||
.withCanLogin(true)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
request.setAttribute("org.dspace.saml.EMAIL", List.of("ben@dspace.org"));
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "oh-no-its-different-than-the-stored-netid");
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.NO_SUCH_USER, result);
|
||||
assertEquals(originalUser, context.getCurrentUser());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticateExistingUserByEmailUpdatesNullNetId() throws Exception {
|
||||
context.setCurrentUser(null);
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("carrie@dspace.org")
|
||||
.withNameInMetadata("Carrie", "Pragma")
|
||||
.withCanLogin(true)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
request.setAttribute("org.dspace.saml.EMAIL", List.of("carrie@dspace.org"));
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "netid-from-idp");
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.SUCCESS, result);
|
||||
|
||||
EPerson user = context.getCurrentUser();
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("carrie@dspace.org", user.getEmail());
|
||||
assertEquals("netid-from-idp", user.getNetid());
|
||||
assertEquals("Carrie", user.getFirstName());
|
||||
assertEquals("Pragma", user.getLastName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticateExistingUserByNetIdUpdatesEmail() throws Exception {
|
||||
context.setCurrentUser(null);
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("alyssa@dspace.org")
|
||||
.withNetId("001")
|
||||
.withNameInMetadata("Alyssa", "Hacker")
|
||||
.withCanLogin(true)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "001");
|
||||
request.setAttribute("org.dspace.saml.EMAIL", List.of("aphacker@dspace.org"));
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.SUCCESS, result);
|
||||
|
||||
EPerson user = context.getCurrentUser();
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("aphacker@dspace.org", user.getEmail());
|
||||
assertEquals("001", user.getNetid());
|
||||
assertEquals("Alyssa", user.getFirstName());
|
||||
assertEquals("Hacker", user.getLastName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticateExistingUserUpdatesName() throws Exception {
|
||||
context.setCurrentUser(null);
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("alyssa@dspace.org")
|
||||
.withNetId("001")
|
||||
.withNameInMetadata("Alyssa", "Hacker")
|
||||
.withCanLogin(true)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "001");
|
||||
request.setAttribute("org.dspace.saml.GIVEN_NAME", "Liz");
|
||||
request.setAttribute("org.dspace.saml.SURNAME", "Hacker-Bitdiddle");
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.SUCCESS, result);
|
||||
|
||||
EPerson user = context.getCurrentUser();
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("alyssa@dspace.org", user.getEmail());
|
||||
assertEquals("001", user.getNetid());
|
||||
assertEquals("Liz", user.getFirstName());
|
||||
assertEquals("Hacker-Bitdiddle", user.getLastName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdatedNamesAreTruncated() throws Exception {
|
||||
context.setCurrentUser(null);
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("alyssa@dspace.org")
|
||||
.withNetId("001")
|
||||
.withNameInMetadata("Alyssa", "Hacker")
|
||||
.withCanLogin(true)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "001");
|
||||
|
||||
request.setAttribute("org.dspace.saml.GIVEN_NAME",
|
||||
"This name is really very long and in fact its much too long so it must be trucated yes I said truncated");
|
||||
|
||||
request.setAttribute("org.dspace.saml.SURNAME",
|
||||
"What is going on with these long names it's frankly out of control and I can't take it any more");
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.SUCCESS, result);
|
||||
|
||||
EPerson user = context.getCurrentUser();
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("alyssa@dspace.org", user.getEmail());
|
||||
assertEquals("001", user.getNetid());
|
||||
assertEquals("This name is really very long and in fact its much too long so i", user.getFirstName());
|
||||
assertEquals("What is going on with these long names it's frankly out of contr", user.getLastName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticateExistingUserAdditionalMetadata() throws Exception {
|
||||
configurationService.setProperty("authentication-saml.eperson.metadata",
|
||||
"org.dspace.saml.PHONE => phone," +
|
||||
"org.dspace.saml.NICKNAME => nickname");
|
||||
|
||||
context.setCurrentUser(null);
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("alyssa@dspace.org")
|
||||
.withNetId("001")
|
||||
.withNameInMetadata("Alyssa", "Hacker")
|
||||
.withCanLogin(true)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "001");
|
||||
request.setAttribute("org.dspace.saml.PHONE", "123-456-7890");
|
||||
request.setAttribute("org.dspace.saml.NICKNAME", "Liz");
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.SUCCESS, result);
|
||||
|
||||
EPerson user = context.getCurrentUser();
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("alyssa@dspace.org", user.getEmail());
|
||||
assertEquals("001", user.getNetid());
|
||||
assertEquals("Alyssa", user.getFirstName());
|
||||
assertEquals("Hacker", user.getLastName());
|
||||
|
||||
List<MetadataValue> metadata = user.getMetadata();
|
||||
|
||||
assertEquals(4, metadata.size());
|
||||
assertEquals("eperson_phone", metadata.get(2).getMetadataField().toString());
|
||||
assertEquals("123-456-7890", metadata.get(2).getValue());
|
||||
assertEquals("eperson_nickname", metadata.get(3).getMetadataField().toString());
|
||||
assertEquals("Liz", metadata.get(3).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdatedAdditionalMetadataAreTruncated() throws Exception {
|
||||
configurationService.setProperty("authentication-saml.eperson.metadata",
|
||||
"org.dspace.saml.PHONE => phone," +
|
||||
"org.dspace.saml.NICKNAME => nickname");
|
||||
|
||||
context.setCurrentUser(null);
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("alyssa@dspace.org")
|
||||
.withNetId("001")
|
||||
.withNameInMetadata("Alyssa", "Hacker")
|
||||
.withCanLogin(true)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "001");
|
||||
request.setAttribute("org.dspace.saml.PHONE", "1234567890-1234567890-1234567890-1234567890");
|
||||
request.setAttribute("org.dspace.saml.NICKNAME", "this is a long nickname.".repeat(100));
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.SUCCESS, result);
|
||||
|
||||
EPerson user = context.getCurrentUser();
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("alyssa@dspace.org", user.getEmail());
|
||||
assertEquals("001", user.getNetid());
|
||||
assertEquals("Alyssa", user.getFirstName());
|
||||
assertEquals("Hacker", user.getLastName());
|
||||
|
||||
List<MetadataValue> metadata = user.getMetadata();
|
||||
|
||||
assertEquals(4, metadata.size());
|
||||
assertEquals("eperson_phone", metadata.get(2).getMetadataField().toString());
|
||||
assertEquals("1234567890-1234567890-1234567890", metadata.get(2).getValue());
|
||||
assertEquals("eperson_nickname", metadata.get(3).getMetadataField().toString());
|
||||
assertEquals("this is a long nickname.".repeat(42) + "this is a long n", metadata.get(3).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidAdditionalMetadataMappingsAreIgnored() throws Exception {
|
||||
configurationService.setProperty("authentication-saml.eperson.metadata",
|
||||
"oops this is bad," +
|
||||
"org.dspace.saml.NICKNAME => nickname");
|
||||
|
||||
context.setCurrentUser(null);
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("alyssa@dspace.org")
|
||||
.withNetId("001")
|
||||
.withNameInMetadata("Alyssa", "Hacker")
|
||||
.withCanLogin(true)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "001");
|
||||
request.setAttribute("org.dspace.saml.PHONE", "123-456-7890");
|
||||
request.setAttribute("org.dspace.saml.NICKNAME", "Liz");
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.SUCCESS, result);
|
||||
|
||||
EPerson user = context.getCurrentUser();
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("alyssa@dspace.org", user.getEmail());
|
||||
assertEquals("001", user.getNetid());
|
||||
assertEquals("Alyssa", user.getFirstName());
|
||||
assertEquals("Hacker", user.getLastName());
|
||||
|
||||
List<MetadataValue> metadata = user.getMetadata();
|
||||
|
||||
assertEquals(3, metadata.size());
|
||||
assertEquals("eperson_nickname", metadata.get(2).getMetadataField().toString());
|
||||
assertEquals("Liz", metadata.get(2).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticateExistingUserAdditionalMetadataAutocreateDisabled() throws Exception {
|
||||
configurationService.setProperty("authentication-saml.eperson.metadata.autocreate", false);
|
||||
|
||||
configurationService.setProperty("authentication-saml.eperson.metadata",
|
||||
"org.dspace.saml.PHONE => phone," +
|
||||
"org.dspace.saml.DEPARTMENT => department");
|
||||
|
||||
context.setCurrentUser(null);
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("alyssa@dspace.org")
|
||||
.withNetId("001")
|
||||
.withNameInMetadata("Alyssa", "Hacker")
|
||||
.withCanLogin(true)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "001");
|
||||
request.setAttribute("org.dspace.saml.PHONE", "123-456-7890");
|
||||
request.setAttribute("org.dspace.saml.DEPARTMENT", "Library");
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.SUCCESS, result);
|
||||
|
||||
EPerson user = context.getCurrentUser();
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("alyssa@dspace.org", user.getEmail());
|
||||
assertEquals("001", user.getNetid());
|
||||
assertEquals("Alyssa", user.getFirstName());
|
||||
assertEquals("Hacker", user.getLastName());
|
||||
|
||||
List<MetadataValue> metadata = user.getMetadata();
|
||||
|
||||
assertEquals(3, metadata.size());
|
||||
assertEquals("eperson_phone", metadata.get(2).getMetadataField().toString());
|
||||
assertEquals("123-456-7890", metadata.get(2).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdditionalMetadataWithInvalidNameNotAutocreated() throws Exception {
|
||||
configurationService.setProperty("authentication-saml.eperson.metadata",
|
||||
"org.dspace.saml.PHONE => phone," +
|
||||
"org.dspace.saml.DEPARTMENT => (department)"); // parens not allowed
|
||||
|
||||
context.setCurrentUser(null);
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("alyssa@dspace.org")
|
||||
.withNetId("001")
|
||||
.withNameInMetadata("Alyssa", "Hacker")
|
||||
.withCanLogin(true)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "001");
|
||||
request.setAttribute("org.dspace.saml.PHONE", "123-456-7890");
|
||||
request.setAttribute("org.dspace.saml.DEPARTMENT", "Library");
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.SUCCESS, result);
|
||||
|
||||
EPerson user = context.getCurrentUser();
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("alyssa@dspace.org", user.getEmail());
|
||||
assertEquals("001", user.getNetid());
|
||||
assertEquals("Alyssa", user.getFirstName());
|
||||
assertEquals("Hacker", user.getLastName());
|
||||
|
||||
List<MetadataValue> metadata = user.getMetadata();
|
||||
|
||||
assertEquals(3, metadata.size());
|
||||
assertEquals("eperson_phone", metadata.get(2).getMetadataField().toString());
|
||||
assertEquals("123-456-7890", metadata.get(2).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExistingUserLoginDisabled() throws Exception {
|
||||
EPerson originalUser = context.getCurrentUser();
|
||||
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("alyssa@dspace.org")
|
||||
.withNameInMetadata("Alyssa", "Hacker")
|
||||
.withCanLogin(false)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
request.setAttribute("org.dspace.saml.EMAIL", List.of("alyssa@dspace.org"));
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.BAD_ARGS, result);
|
||||
assertEquals(originalUser, context.getCurrentUser());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonExistentUserWithoutEmail() throws Exception {
|
||||
EPerson originalUser = context.getCurrentUser();
|
||||
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "non-existent-netid");
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.NO_SUCH_USER, result);
|
||||
assertEquals(originalUser, context.getCurrentUser());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonExistentUserWithEmailAutoregisterEnabled() throws Exception {
|
||||
context.setCurrentUser(null);
|
||||
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "non-existent-netid");
|
||||
request.setAttribute("org.dspace.saml.EMAIL", List.of("ben@dspace.org"));
|
||||
request.setAttribute("org.dspace.saml.GIVEN_NAME", "Ben");
|
||||
request.setAttribute("org.dspace.saml.SURNAME", "Bitdiddle");
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.SUCCESS, result);
|
||||
|
||||
EPerson user = context.getCurrentUser();
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("ben@dspace.org", user.getEmail());
|
||||
assertEquals("non-existent-netid", user.getNetid());
|
||||
assertEquals("Ben", user.getFirstName());
|
||||
assertEquals("Bitdiddle", user.getLastName());
|
||||
assertTrue(user.canLogIn());
|
||||
assertTrue(user.getSelfRegistered());
|
||||
|
||||
testUser = user; // Make sure the autoregistered user gets deleted.
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonExistentUserWithEmailAutoregisterDisabled() throws Exception {
|
||||
configurationService.setProperty("authentication-saml.autoregister", false);
|
||||
|
||||
EPerson originalUser = context.getCurrentUser();
|
||||
|
||||
request.setAttribute("org.dspace.saml.NAME_ID", "non-existent-netid");
|
||||
request.setAttribute("org.dspace.saml.EMAIL", List.of("ben@dspace.org"));
|
||||
request.setAttribute("org.dspace.saml.GIVEN_NAME", "Ben");
|
||||
request.setAttribute("org.dspace.saml.SURNAME", "Bitdiddle");
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.NO_SUCH_USER, result);
|
||||
assertEquals(originalUser, context.getCurrentUser());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoEmailOrNameIdInRequest() throws Exception {
|
||||
context.setCurrentUser(null);
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
testUser = EPersonBuilder.createEPerson(context)
|
||||
.withEmail("alyssa@dspace.org")
|
||||
.withNameInMetadata("Alyssa", "Hacker")
|
||||
.withCanLogin(true)
|
||||
.build();
|
||||
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, request);
|
||||
|
||||
assertEquals(AuthenticationMethod.NO_SUCH_USER, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestIsNull() throws Exception {
|
||||
EPerson originalUser = context.getCurrentUser();
|
||||
|
||||
int result = samlAuth.authenticate(context, null, null, null, null);
|
||||
|
||||
assertEquals(AuthenticationMethod.BAD_ARGS, result);
|
||||
assertEquals(originalUser, context.getCurrentUser());
|
||||
}
|
||||
}
|
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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.app.rest.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.dspace.authenticate.SamlAuthentication;
|
||||
import org.dspace.core.Utils;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.dspace.services.factory.DSpaceServicesFactory;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderNotFoundException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
/**
|
||||
* A filter that examines requests to see if the user has been authenticated via SAML.
|
||||
* <p>
|
||||
* The overall SAML login process is as follows:
|
||||
* </p>
|
||||
* <ol>
|
||||
* <li>When SAML authentication is enabled, the client/UI receives the URL to the active SAML
|
||||
* relying party's authentication endpoint in the WWW-Authenticate header.
|
||||
* See {@link org.dspace.authenticate.SamlAuthentication#loginPageURL(org.dspace.core.Context, HttpServletRequest, HttpServletResponse)}.</li>
|
||||
* <li>The client sends the user to that URL when they select SAML authentication.</li>
|
||||
* <li>The active SAML relying party sends the client to the login page at the asserting party
|
||||
* (aka identity provider, or IdP).</li>
|
||||
* <li>The user logs in to the asserting party.</li>
|
||||
* <li>If successful, the asserting party sends the client back to the relying party's assertion
|
||||
* consumer endpoint, along with the SAML assertion.</li>
|
||||
* <li>The relying party receives the SAML assertion, extracts attributes from the assertion,
|
||||
* maps them into request attributes, and forwards the request to the path where this filter
|
||||
* is listening.</li>
|
||||
* <li>This filter intercepts the request in order to check for a valid SAML login (see
|
||||
* {@link org.dspace.authenticate.SamlAuthentication#authenticate(org.dspace.core.Context, String, String, String, HttpServletRequest)})
|
||||
* and stores that user info in a JWT. It also saves that JWT in a <em>temporary</em>
|
||||
* authentication cookie.</li>
|
||||
* <li>This filter redirects the user back to the UI (after verifying it's at a trusted URL).</li>
|
||||
* <li>The client reads the JWT from the cookie, and sends it back in a request to
|
||||
* /api/authn/login, which triggers the server-side to destroy the cookie and move the JWT
|
||||
* into a header.</li>
|
||||
* </ol>
|
||||
*
|
||||
* @author Ray Lee
|
||||
*/
|
||||
public class SamlLoginFilter extends StatelessLoginFilter {
|
||||
private static final Logger logger = LogManager.getLogger(SamlLoginFilter.class);
|
||||
|
||||
private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
|
||||
|
||||
public SamlLoginFilter(String url, String httpMethod, AuthenticationManager authenticationManager,
|
||||
RestAuthenticationService restAuthenticationService) {
|
||||
super(url, httpMethod, authenticationManager, restAuthenticationService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
|
||||
throws AuthenticationException {
|
||||
|
||||
if (!SamlAuthentication.isEnabled()) {
|
||||
throw new ProviderNotFoundException("SAML is disabled.");
|
||||
}
|
||||
|
||||
// Because this authentication is implicit, we pass in an empty DSpaceAuthentication.
|
||||
return authenticationManager.authenticate(new DSpaceAuthentication());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
|
||||
Authentication auth) throws IOException, ServletException {
|
||||
|
||||
restAuthenticationService.addAuthenticationDataForUser(request, response, (DSpaceAuthentication) auth, true);
|
||||
|
||||
redirectAfterSuccess(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* After successful login, redirect to the configured UI URL. If that URL is not allowed for
|
||||
* this DSpace site, return a 400 error.
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @throws IOException
|
||||
*/
|
||||
private void redirectAfterSuccess(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
String redirectUrl = configurationService.getProperty("dspace.ui.url");
|
||||
String redirectHostName = Utils.getHostName(redirectUrl);
|
||||
String serverUrl = configurationService.getProperty("dspace.server.url");
|
||||
|
||||
boolean isRedirectAllowed = Stream.concat(
|
||||
Stream.of(serverUrl),
|
||||
Arrays.stream(configurationService.getArrayProperty("rest.cors.allowed-origins")))
|
||||
.map(url -> Utils.getHostName(url))
|
||||
.anyMatch(hostName -> hostName.equalsIgnoreCase(redirectHostName));
|
||||
|
||||
if (isRedirectAllowed) {
|
||||
logger.debug("SAML redirecting to " + redirectUrl);
|
||||
|
||||
response.sendRedirect(redirectUrl);
|
||||
} else {
|
||||
logger.error("SAML redirect URL {} is not allowed" + redirectUrl);
|
||||
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST,"SAML redirect URL not allowed");
|
||||
}
|
||||
}
|
||||
}
|
@@ -161,6 +161,12 @@ public class WebSecurityConfiguration {
|
||||
.addFilterBefore(new OidcLoginFilter("/api/authn/oidc", HttpMethod.GET.name(),
|
||||
authenticationManager, restAuthenticationService),
|
||||
LogoutFilter.class)
|
||||
// Add a filter before our SAML endpoints to do the authentication based on the data in the HTTP request.
|
||||
// This endpoint only responds to GET as the actual authentication is performed by SAML, which then
|
||||
// forwards to this endpoint to pass the authentication data to DSpace.
|
||||
.addFilterBefore(new SamlLoginFilter("/api/authn/saml", HttpMethod.GET.name(),
|
||||
authenticationManager, restAuthenticationService),
|
||||
LogoutFilter.class)
|
||||
// Add a custom Token based authentication filter based on the token previously given to the client
|
||||
// before each URL
|
||||
.addFilterBefore(new StatelessAuthenticationFilter(authenticationManager, restAuthenticationService,
|
||||
|
@@ -245,7 +245,11 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
|
||||
// which destroys this temporary auth cookie. So, the auth cookie only exists a few seconds.
|
||||
if (addCookie) {
|
||||
ResponseCookie cookie = ResponseCookie.from(AUTHORIZATION_COOKIE, token)
|
||||
.httpOnly(true).secure(true).sameSite("None").build();
|
||||
.httpOnly(true)
|
||||
.secure(true)
|
||||
.sameSite("None")
|
||||
.path("/server/api/authn")
|
||||
.build();
|
||||
|
||||
// Write the cookie to the Set-Cookie header in order to send it
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||
|
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* 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.app.rest.security;
|
||||
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.dspace.AbstractDSpaceTest;
|
||||
import org.dspace.servicemanager.config.DSpaceConfigurationService;
|
||||
import org.dspace.services.ConfigurationService;
|
||||
import org.dspace.services.factory.DSpaceServicesFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderNotFoundException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
public class SamlLoginFilterTest extends AbstractDSpaceTest {
|
||||
private static ConfigurationService configurationService;
|
||||
|
||||
private AuthenticationManager authManager;
|
||||
private HttpServletRequest request;
|
||||
private HttpServletResponse response;
|
||||
private RestAuthenticationService restAuthService;
|
||||
private FilterChain filterChain;
|
||||
private SamlLoginFilter filter;
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeAll() {
|
||||
configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void beforeEach() throws Exception {
|
||||
resetConfigurationService();
|
||||
|
||||
authManager = createAuthenticationManager();
|
||||
restAuthService = createRestAuthenticationService();
|
||||
filterChain = Mockito.mock(FilterChain.class);
|
||||
filter = new SamlLoginFilter("/api/authn/saml", HttpMethod.GET.name(), authManager, restAuthService);
|
||||
request = createRequest("/api/authn/saml");
|
||||
response = createResponse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectAfterSuccess() throws Exception {
|
||||
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod",
|
||||
"org.dspace.authenticate.SamlAuthentication");
|
||||
|
||||
configurationService.setProperty("dspace.ui.url","http://dspace.example.org");
|
||||
configurationService.setProperty("dspace.server.url","http://dspace.example.org/server");
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(response).sendRedirect("http://dspace.example.org");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectToRemoteHostNotAllowed() throws Exception {
|
||||
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod",
|
||||
"org.dspace.authenticate.SamlAuthentication");
|
||||
|
||||
configurationService.setProperty("dspace.ui.url","http://different.host.bad");
|
||||
configurationService.setProperty("dspace.server.url","http://dspace.example.org/server");
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(response).sendError(eq(400), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectToRemoteHostCorsAllowed() throws Exception {
|
||||
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod",
|
||||
"org.dspace.authenticate.SamlAuthentication");
|
||||
|
||||
configurationService.setProperty("rest.cors.allowed-origins", "http://different.host.ok");
|
||||
configurationService.setProperty("dspace.ui.url","http://different.host.ok");
|
||||
configurationService.setProperty("dspace.server.url","http://dspace.example.org/server");
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(response).sendRedirect("http://different.host.ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthCookieSaved() throws Exception {
|
||||
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod",
|
||||
"org.dspace.authenticate.SamlAuthentication");
|
||||
|
||||
configurationService.setProperty("dspace.ui.url","http://dspace.example.org");
|
||||
configurationService.setProperty("dspace.server.url","http://dspace.example.org/server");
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(restAuthService).addAuthenticationDataForUser(
|
||||
eq(request), eq(response), any(DSpaceAuthentication.class), eq(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSamlAuthenticationNotEnabled() throws Exception {
|
||||
assertThrows(ProviderNotFoundException.class, () -> filter.attemptAuthentication(request, response));
|
||||
}
|
||||
|
||||
private void resetConfigurationService() {
|
||||
((DSpaceConfigurationService) configurationService).clear();
|
||||
|
||||
configurationService.reloadConfig();
|
||||
}
|
||||
|
||||
private AuthenticationManager createAuthenticationManager() {
|
||||
AuthenticationManager mockAuthManager = Mockito.mock(AuthenticationManager.class);
|
||||
|
||||
when(mockAuthManager.authenticate(any(Authentication.class)))
|
||||
.thenReturn(Mockito.mock(DSpaceAuthentication.class));
|
||||
|
||||
return mockAuthManager;
|
||||
}
|
||||
|
||||
private RestAuthenticationService createRestAuthenticationService() throws Exception {
|
||||
RestAuthenticationService mockRestAuthService = Mockito.mock(RestAuthenticationService.class);
|
||||
|
||||
doNothing().when(mockRestAuthService).addAuthenticationDataForUser(
|
||||
isA(HttpServletRequest.class), isA(HttpServletResponse.class),
|
||||
isA(DSpaceAuthentication.class), isA(Boolean.class));
|
||||
|
||||
return mockRestAuthService;
|
||||
}
|
||||
|
||||
private HttpServletRequest createRequest(String path) {
|
||||
MockHttpServletRequest mockRequest = new MockHttpServletRequest(HttpMethod.GET.name(), path);
|
||||
|
||||
mockRequest.setPathInfo(path);
|
||||
|
||||
return mockRequest;
|
||||
}
|
||||
|
||||
private HttpServletResponse createResponse() throws Exception {
|
||||
HttpServletResponse mockResponse = Mockito.mock(HttpServletResponse.class);
|
||||
|
||||
doNothing().when(mockResponse).sendRedirect(isA(String.class));
|
||||
doNothing().when(mockResponse).sendError(isA(Integer.class), isA(String.class));
|
||||
|
||||
return mockResponse;
|
||||
}
|
||||
}
|
@@ -1647,6 +1647,7 @@ include = ${module_dir}/authentication-ip.cfg
|
||||
include = ${module_dir}/authentication-ldap.cfg
|
||||
include = ${module_dir}/authentication-oidc.cfg
|
||||
include = ${module_dir}/authentication-password.cfg
|
||||
include = ${module_dir}/authentication-saml.cfg
|
||||
include = ${module_dir}/authentication-shibboleth.cfg
|
||||
include = ${module_dir}/authentication-x509.cfg
|
||||
include = ${module_dir}/authority.cfg
|
||||
|
@@ -198,6 +198,9 @@ db.password = dspace
|
||||
# X.509 certificate authentication. See authentication-x509.cfg for default configuration.
|
||||
#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.X509Authentication
|
||||
|
||||
# SAML authentication/authorization. See authenication-saml.cfg for default configuration.
|
||||
#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.SamlAuthentication
|
||||
|
||||
# Authentication by Password (encrypted in DSpace's database). See authentication-password.cfg for default configuration.
|
||||
# Enabled by default in authentication.cfg
|
||||
#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.PasswordAuthentication
|
||||
|
41
dspace/config/modules/authentication-saml.cfg
Normal file
41
dspace/config/modules/authentication-saml.cfg
Normal file
@@ -0,0 +1,41 @@
|
||||
#---------------------------------------------------------------#
|
||||
#---------------SAML AUTHENTICATION CONFIGURATIONS--------------#
|
||||
#---------------------------------------------------------------#
|
||||
# Configuration properties used by the SAML #
|
||||
# Authentication plugin, when it is enabled. #
|
||||
#---------------------------------------------------------------#
|
||||
|
||||
# The ID of the SAML relying party we should use for authentication
|
||||
# authentication-saml.relying-party-id = auth0
|
||||
|
||||
# The base URL of all SAML relying party endpoints
|
||||
authentication-saml.relying-party-url = ${dspace.server.url}/saml2
|
||||
|
||||
# The URL of the authenticate endpoint for the SAML relying party
|
||||
authentication-saml.authenticate-endpoint = ${authentication-saml.relying-party-url}/authenticate/${authentication-saml.relying-party-id}
|
||||
|
||||
# Should we allow new users to be registered automatically?
|
||||
authentication-saml.autoregister = true
|
||||
|
||||
# The request attribute that contains the ID of the SAML relying party that authenticated the user
|
||||
authentication-saml.attribute.relying-party-id = org.dspace.saml.RELYING_PARTY_ID
|
||||
|
||||
# The request attribute that contains the user's SAML name ID
|
||||
authentication-saml.attribute.name-id = org.dspace.saml.NAME_ID
|
||||
|
||||
# The request attribute that contains the user's email
|
||||
authentication-saml.attribute.email = org.dspace.saml.EMAIL
|
||||
|
||||
# The request attribute that contains the user's first name
|
||||
authentication-saml.attribute.first-name = org.dspace.saml.GIVEN_NAME
|
||||
|
||||
# The request attribute that contains the user's last name
|
||||
authentication-saml.attribute.last-name = org.dspace.saml.SURNAME
|
||||
|
||||
# Additional attribute mappings. Multiple attributes may be stored for each user. The left side is
|
||||
# the request attribute, and the right side is the ePerson metadata field to map the attribute to.
|
||||
# authentication-saml.eperson.metadata = \
|
||||
# org.dspace.saml.PHONE => phone
|
||||
|
||||
# If the ePerson metadata field is not found, should it be created automatically?
|
||||
authentication-saml.eperson.metadata.autocreate = true
|
@@ -30,6 +30,9 @@
|
||||
# * OIDC Authentication
|
||||
# Plugin class: org.dspace.authenticate.OidcAuthentication
|
||||
# Configuration file: authentication-oidc.cfg
|
||||
# * SAML Authentication
|
||||
# Plugin class: org.dspace.authenticate.SamlAuthentication
|
||||
# Configuration file: authentication-saml.cfg
|
||||
|
||||
#
|
||||
# One or more of the above plugins can be enabled by listing its plugin class in
|
||||
@@ -58,6 +61,9 @@
|
||||
# OIDC authentication. See authentication-oidc.cfg for default configuration.
|
||||
#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.OidcAuthentication
|
||||
|
||||
# SAML authentication. See authentication-saml.cfg for default configuration.
|
||||
#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.SamlAuthentication
|
||||
|
||||
# Authentication by Password (encrypted in DSpace's database). See authentication-password.cfg for default configuration.
|
||||
# Enabled by default (to disable, either comment out, or define a new list of AuthenticationMethod plugins in your local.cfg)
|
||||
plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.PasswordAuthentication
|
||||
|
Reference in New Issue
Block a user