Merge pull request #2651 from 4Science/dspace-7-shibboleth

Dspace 7 shibboleth (REST)
This commit is contained in:
Tim Donohue
2020-03-27 09:52:11 -05:00
committed by GitHub
17 changed files with 624 additions and 46 deletions

View File

@@ -34,6 +34,7 @@ import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.MetadataFieldService; import org.dspace.content.service.MetadataFieldService;
import org.dspace.content.service.MetadataSchemaService; import org.dspace.content.service.MetadataSchemaService;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.core.Utils;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group; import org.dspace.eperson.Group;
import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.factory.EPersonServiceFactory;
@@ -492,35 +493,22 @@ public class ShibAuthentication implements AuthenticationMethod {
boolean lazySession = configurationService.getBooleanProperty("authentication-shibboleth.lazysession", false); boolean lazySession = configurationService.getBooleanProperty("authentication-shibboleth.lazysession", false);
if ( lazySession ) { if ( lazySession ) {
String shibURL = configurationService.getProperty("authentication-shibboleth.lazysession.loginurl"); String shibURL = getShibURL(request);
boolean forceHTTPS =
configurationService.getBooleanProperty("authentication-shibboleth.lazysession.secure",true);
// Shibboleth authentication initiator // Determine the client redirect URL, where to redirect after authenticating.
if (shibURL == null || shibURL.length() == 0) { String redirectUrl = null;
shibURL = "/Shibboleth.sso/Login"; if (request.getHeader("Referer") != null && StringUtils.isNotBlank(request.getHeader("Referer"))) {
redirectUrl = request.getHeader("Referer");
} else if (request.getHeader("X-Requested-With") != null
&& StringUtils.isNotBlank(request.getHeader("X-Requested-With"))) {
redirectUrl = request.getHeader("X-Requested-With");
} }
shibURL = shibURL.trim();
// Determine the return URL, where shib will send the user after authenticating. We need it to go back // Determine the server return URL, where shib will send the user after authenticating.
// to DSpace's shibboleth-login url so the we will extract the user's information and locally // We need it to go back to DSpace's shibboleth-login url so we will extract the user's information
// authenticate them. // and locally authenticate them.
String host = request.getServerName(); String returnURL = configurationService.getProperty("dspace.server.url") + "/api/authn/shibboleth"
int port = request.getServerPort(); + ((redirectUrl != null) ? "?redirectUrl=" + redirectUrl : "");
String contextPath = request.getContextPath();
String returnURL = request.getHeader("Referer");
if (returnURL == null) {
if (request.isSecure() || forceHTTPS) {
returnURL = "https://";
} else {
returnURL = "http://";
}
returnURL += host;
if (!(port == 443 || port == 80)) {
returnURL += ":" + port;
}
}
try { try {
shibURL += "?target=" + URLEncoder.encode(returnURL, "UTF-8"); shibURL += "?target=" + URLEncoder.encode(returnURL, "UTF-8");
@@ -1258,6 +1246,23 @@ public class ShibAuthentication implements AuthenticationMethod {
return valueList; return valueList;
} }
private String getShibURL(HttpServletRequest request) {
String shibURL = configurationService.getProperty("authentication-shibboleth.lazysession.loginurl",
"/Shibboleth.sso/Login");
boolean forceHTTPS =
configurationService.getBooleanProperty("authentication-shibboleth.lazysession.secure", true);
// Shibboleth url must be absolute
if (shibURL.startsWith("/")) {
String serverUrl = Utils.getBaseUrl(configurationService.getProperty("dspace.server.url"));
shibURL = serverUrl + shibURL;
if ((request.isSecure() || forceHTTPS) && shibURL.startsWith("http://")) {
shibURL = shibURL.replace("http://", "https://");
}
}
return shibURL;
}
} }

View File

@@ -13,8 +13,10 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL;
import java.rmi.dgc.VMID; import java.rmi.dgc.VMID;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@@ -414,6 +416,23 @@ public final class Utils {
} }
} }
/**
* Retrieve the baseurl from a given URL string
* @param urlString URL string
* @return baseurl (without any context path) or null (if URL was invalid)
*/
public static String getBaseUrl(String urlString) {
try {
URL url = new URL(urlString);
String baseUrl = url.getProtocol() + "://" + url.getHost();
if (url.getPort() != -1) {
baseUrl += (":" + url.getPort());
}
return baseUrl;
} catch (MalformedURLException e) {
return null;
}
}
/** /**
* Retrieve the hostname from a given URI string * Retrieve the hostname from a given URI string

View File

@@ -22,6 +22,33 @@ import org.junit.Test;
*/ */
public class UtilsTest extends AbstractUnitTest { public class UtilsTest extends AbstractUnitTest {
/**
* Test of getBaseUrl method, of class Utils
*/
@Test
public void testGetBaseUrl() {
assertEquals("Test remove /server", "http://dspace.org",
Utils.getBaseUrl("http://dspace.org/server"));
assertEquals("Test remove /server/api/core/items", "https://dspace.org",
Utils.getBaseUrl("https://dspace.org/server/api/core/items"));
assertEquals("Test remove trailing slash", "https://dspace.org",
Utils.getBaseUrl("https://dspace.org/"));
assertEquals("Test keep url", "https://demo.dspace.org",
Utils.getBaseUrl("https://demo.dspace.org"));
assertEquals("Test keep url", "http://localhost:8080",
Utils.getBaseUrl("http://localhost:8080"));
assertEquals("Test keep url", "http://localhost:8080",
Utils.getBaseUrl("http://localhost:8080/server"));
// This uses a bunch of reserved URI characters
assertNull("Test invalid URI returns null", Utils.getBaseUrl("&+,?/@="));
}
/** /**
* Test of getHostName method, of class Utils * Test of getHostName method, of class Utils
*/ */

View File

@@ -10,6 +10,7 @@ package org.dspace.app.rest;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Arrays; import java.util.Arrays;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.dspace.app.rest.converter.ConverterService; import org.dspace.app.rest.converter.ConverterService;
import org.dspace.app.rest.converter.EPersonConverter; import org.dspace.app.rest.converter.EPersonConverter;
@@ -20,6 +21,7 @@ import org.dspace.app.rest.model.EPersonRest;
import org.dspace.app.rest.model.hateoas.AuthenticationStatusResource; import org.dspace.app.rest.model.hateoas.AuthenticationStatusResource;
import org.dspace.app.rest.model.hateoas.AuthnResource; import org.dspace.app.rest.model.hateoas.AuthnResource;
import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.projection.Projection;
import org.dspace.app.rest.security.RestAuthenticationService;
import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.app.rest.utils.Utils; import org.dspace.app.rest.utils.Utils;
import org.dspace.core.Context; import org.dspace.core.Context;
@@ -60,6 +62,9 @@ public class AuthenticationRestController implements InitializingBean {
@Autowired @Autowired
private HalLinkService halLinkService; private HalLinkService halLinkService;
@Autowired
private RestAuthenticationService restAuthenticationService;
@Autowired @Autowired
private Utils utils; private Utils utils;
@@ -77,7 +82,8 @@ public class AuthenticationRestController implements InitializingBean {
} }
@RequestMapping(value = "/status", method = RequestMethod.GET) @RequestMapping(value = "/status", method = RequestMethod.GET)
public AuthenticationStatusResource status(HttpServletRequest request) throws SQLException { public AuthenticationStatusResource status(HttpServletRequest request, HttpServletResponse response)
throws SQLException {
Context context = ContextUtil.obtainContext(request); Context context = ContextUtil.obtainContext(request);
EPersonRest ePersonRest = null; EPersonRest ePersonRest = null;
Projection projection = utils.obtainProjection(); Projection projection = utils.obtainProjection();
@@ -86,6 +92,14 @@ public class AuthenticationRestController implements InitializingBean {
} }
AuthenticationStatusRest authenticationStatusRest = new AuthenticationStatusRest(ePersonRest); AuthenticationStatusRest authenticationStatusRest = new AuthenticationStatusRest(ePersonRest);
// Whether authentication status is false add WWW-Authenticate so client can retrieve the available
// authentication methods
if (!authenticationStatusRest.isAuthenticated()) {
String authenticateHeaderValue = restAuthenticationService
.getWwwAuthenticateHeaderValue(request, response);
response.setHeader("WWW-Authenticate", authenticateHeaderValue);
}
authenticationStatusRest.setProjection(projection); authenticationStatusRest.setProjection(projection);
AuthenticationStatusResource authenticationStatusResource = converter.toResource(authenticationStatusRest); AuthenticationStatusResource authenticationStatusResource = converter.toResource(authenticationStatusRest);

View File

@@ -0,0 +1,60 @@
/**
* 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;
import java.io.IOException;
import java.util.Arrays;
import javax.servlet.http.HttpServletResponse;
import org.dspace.app.rest.model.AuthnRest;
import org.dspace.services.ConfigurationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Link;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Rest controller that handles redirect after shibboleth authentication succeded
*
* @author Andrea Bollini (andrea dot bollini at 4science dot it)
* @author Giuseppe Digilio (giuseppe dot digilio at 4science dot it)
*/
@RequestMapping(value = "/api/" + AuthnRest.CATEGORY + "/shibboleth")
@RestController
public class ShibbolethRestController implements InitializingBean {
private static final Logger log = LoggerFactory.getLogger(ShibbolethRestController.class);
@Autowired
ConfigurationService configurationService;
@Autowired
DiscoverableEndpointsService discoverableEndpointsService;
@Override
public void afterPropertiesSet() {
discoverableEndpointsService
.register(this, Arrays.asList(new Link("/api/" + AuthnRest.CATEGORY, "shibboleth")));
}
@RequestMapping(method = RequestMethod.GET)
public void shibboleth(HttpServletResponse response,
@RequestParam(name = "redirectUrl", required = false) String redirectUrl) throws IOException {
if (redirectUrl == null) {
redirectUrl = configurationService.getProperty("dspace.ui.url");
}
log.info("Redirecting to " + redirectUrl);
response.sendRedirect(redirectUrl);
}
}

View File

@@ -35,7 +35,7 @@ public class AuthnHalLinkFactory extends HalLinkFactory<AuthnResource, Authentic
.logout())); .logout()));
list.add(buildLink("status", methodOn list.add(buildLink("status", methodOn
.status(null))); .status(null, null)));
} }
@Override @Override

View File

@@ -45,7 +45,7 @@ public class CustomLogoutHandler implements LogoutHandler {
Authentication authentication) { Authentication authentication) {
try { try {
Context context = ContextUtil.obtainContext(httpServletRequest); Context context = ContextUtil.obtainContext(httpServletRequest);
restAuthenticationService.invalidateAuthenticationData(httpServletRequest, context); restAuthenticationService.invalidateAuthenticationData(httpServletRequest, httpServletResponse, context);
context.commit(); context.commit();
} catch (Exception e) { } catch (Exception e) {

View File

@@ -26,13 +26,14 @@ import org.springframework.stereotype.Service;
public interface RestAuthenticationService { public interface RestAuthenticationService {
void addAuthenticationDataForUser(HttpServletRequest request, HttpServletResponse response, void addAuthenticationDataForUser(HttpServletRequest request, HttpServletResponse response,
DSpaceAuthentication authentication) throws IOException; DSpaceAuthentication authentication, boolean addCookie) throws IOException;
EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context); EPerson getAuthenticatedEPerson(HttpServletRequest request, Context context);
boolean hasAuthenticationData(HttpServletRequest request); boolean hasAuthenticationData(HttpServletRequest request);
void invalidateAuthenticationData(HttpServletRequest request, Context context) throws Exception; void invalidateAuthenticationData(HttpServletRequest request, HttpServletResponse response, Context context)
throws Exception;
AuthenticationService getAuthenticationService(); AuthenticationService getAuthenticationService();
@@ -44,4 +45,6 @@ public interface RestAuthenticationService {
*/ */
String getWwwAuthenticateHeaderValue(HttpServletRequest request, HttpServletResponse response); String getWwwAuthenticateHeaderValue(HttpServletRequest request, HttpServletResponse response);
void invalidateAuthenticationCookie(HttpServletResponse res);
} }

View File

@@ -0,0 +1,53 @@
/**
* 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.ArrayList;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
/**
* This class will filter shibboleth requests to try and authenticate them
*
* @author Giuseppe Digilio (giuseppe dot digilio at 4science dot it)
*/
public class ShibbolethAuthenticationFilter extends StatelessLoginFilter {
public ShibbolethAuthenticationFilter(String url, AuthenticationManager authenticationManager,
RestAuthenticationService restAuthenticationService) {
super(url, authenticationManager, restAuthenticationService);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
return authenticationManager.authenticate(
new DSpaceAuthentication(null, null, new ArrayList<>())
);
}
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
DSpaceAuthentication dSpaceAuthentication = (DSpaceAuthentication) auth;
restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication, true);
chain.doFilter(req, res);
}
}

View File

@@ -61,6 +61,7 @@ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter {
Authentication authentication = getAuthentication(req); Authentication authentication = getAuthentication(req);
if (authentication != null) { if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
restAuthenticationService.invalidateAuthenticationCookie(res);
} }
chain.doFilter(req, res); chain.doFilter(req, res);

View File

@@ -28,9 +28,9 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
*/ */
public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter { public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter {
private AuthenticationManager authenticationManager; protected AuthenticationManager authenticationManager;
private RestAuthenticationService restAuthenticationService; protected RestAuthenticationService restAuthenticationService;
@Override @Override
public void afterPropertiesSet() { public void afterPropertiesSet() {
@@ -63,7 +63,7 @@ public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter
Authentication auth) throws IOException, ServletException { Authentication auth) throws IOException, ServletException {
DSpaceAuthentication dSpaceAuthentication = (DSpaceAuthentication) auth; DSpaceAuthentication dSpaceAuthentication = (DSpaceAuthentication) auth;
restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication); restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication, false);
} }
@Override @Override

View File

@@ -109,6 +109,12 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
restAuthenticationService), restAuthenticationService),
LogoutFilter.class) LogoutFilter.class)
//Add a filter before our shibboleth endpoints to do the authentication based on the data in the
// HTTP request
.addFilterBefore(new ShibbolethAuthenticationFilter("/api/authn/shibboleth", authenticationManager(),
restAuthenticationService),
LogoutFilter.class)
// Add a custom Token based authentication filter based on the token previously given to the client // Add a custom Token based authentication filter based on the token previously given to the client
// before each URL // before each URL
.addFilterBefore(new StatelessAuthenticationFilter(authenticationManager(), restAuthenticationService, .addFilterBefore(new StatelessAuthenticationFilter(authenticationManager(), restAuthenticationService,

View File

@@ -12,6 +12,7 @@ import java.sql.SQLException;
import java.text.ParseException; import java.text.ParseException;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@@ -37,11 +38,13 @@ import org.springframework.stereotype.Component;
* *
* @author Frederic Van Reet (frederic dot vanreet at atmire dot com) * @author Frederic Van Reet (frederic dot vanreet at atmire dot com)
* @author Tom Desair (tom dot desair at atmire dot com) * @author Tom Desair (tom dot desair at atmire dot com)
* @author Giuseppe Digilio (giuseppe dot digilio at 4science dot it)
*/ */
@Component @Component
public class JWTTokenRestAuthenticationServiceImpl implements RestAuthenticationService, InitializingBean { public class JWTTokenRestAuthenticationServiceImpl implements RestAuthenticationService, InitializingBean {
private static final Logger log = LoggerFactory.getLogger(RestAuthenticationService.class); private static final Logger log = LoggerFactory.getLogger(RestAuthenticationService.class);
private static final String AUTHORIZATION_COOKIE = "Authorization-cookie";
private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String AUTHORIZATION_TYPE = "Bearer"; private static final String AUTHORIZATION_TYPE = "Bearer";
@@ -61,7 +64,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
@Override @Override
public void addAuthenticationDataForUser(HttpServletRequest request, HttpServletResponse response, public void addAuthenticationDataForUser(HttpServletRequest request, HttpServletResponse response,
DSpaceAuthentication authentication) throws IOException { DSpaceAuthentication authentication, boolean addCookie) throws IOException {
try { try {
Context context = ContextUtil.obtainContext(request); Context context = ContextUtil.obtainContext(request);
context.setCurrentUser(ePersonService.findByEmail(context, authentication.getName())); context.setCurrentUser(ePersonService.findByEmail(context, authentication.getName()));
@@ -71,7 +74,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
String token = jwtTokenHandler.createTokenForEPerson(context, request, String token = jwtTokenHandler.createTokenForEPerson(context, request,
authentication.getPreviousLoginDate(), groups); authentication.getPreviousLoginDate(), groups);
addTokenToResponse(response, token); addTokenToResponse(response, token, addCookie);
context.commit(); context.commit();
} catch (JOSEException e) { } catch (JOSEException e) {
@@ -99,15 +102,26 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
@Override @Override
public boolean hasAuthenticationData(HttpServletRequest request) { public boolean hasAuthenticationData(HttpServletRequest request) {
return StringUtils.isNotBlank(request.getHeader(AUTHORIZATION_HEADER)); return StringUtils.isNotBlank(request.getHeader(AUTHORIZATION_HEADER))
|| StringUtils.isNotBlank(getAuthorizationCookie(request));
} }
@Override @Override
public void invalidateAuthenticationData(HttpServletRequest request, Context context) throws Exception { public void invalidateAuthenticationData(HttpServletRequest request, HttpServletResponse response,
Context context) throws Exception {
String token = getToken(request); String token = getToken(request);
invalidateAuthenticationCookie(response);
jwtTokenHandler.invalidateToken(token, request, context); jwtTokenHandler.invalidateToken(token, request, context);
} }
@Override
public void invalidateAuthenticationCookie(HttpServletResponse response) {
Cookie cookie = new Cookie(AUTHORIZATION_COOKIE, "");
cookie.setHttpOnly(true);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
@Override @Override
public AuthenticationService getAuthenticationService() { public AuthenticationService getAuthenticationService() {
return authenticationService; return authenticationService;
@@ -140,18 +154,42 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
return wwwAuthenticate.toString(); return wwwAuthenticate.toString();
} }
private void addTokenToResponse(final HttpServletResponse response, final String token) throws IOException { private void addTokenToResponse(final HttpServletResponse response, final String token, final Boolean addCookie)
throws IOException {
// we need authentication cookies because Shibboleth can't use the authentication headers due to the redirects
if (addCookie) {
Cookie cookie = new Cookie(AUTHORIZATION_COOKIE, token);
cookie.setHttpOnly(true);
cookie.setSecure(true);
response.addCookie(cookie);
}
response.setHeader(AUTHORIZATION_HEADER, String.format("%s %s", AUTHORIZATION_TYPE, token)); response.setHeader(AUTHORIZATION_HEADER, String.format("%s %s", AUTHORIZATION_TYPE, token));
} }
private String getToken(HttpServletRequest request) { private String getToken(HttpServletRequest request) {
String tokenValue = null;
String authHeader = request.getHeader(AUTHORIZATION_HEADER); String authHeader = request.getHeader(AUTHORIZATION_HEADER);
String authCookie = getAuthorizationCookie(request);
if (StringUtils.isNotBlank(authHeader)) { if (StringUtils.isNotBlank(authHeader)) {
String tokenValue = authHeader.replace(AUTHORIZATION_TYPE, "").trim(); tokenValue = authHeader.replace(AUTHORIZATION_TYPE, "").trim();
return tokenValue; } else if (StringUtils.isNotBlank(authCookie)) {
} else { tokenValue = authCookie;
return null;
} }
return tokenValue;
}
private String getAuthorizationCookie(HttpServletRequest request) {
String authCookie = "";
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(AUTHORIZATION_COOKIE) && StringUtils.isNotEmpty(cookie.getValue())) {
authCookie = cookie.getValue();
}
}
}
return authCookie;
} }
} }

View File

@@ -21,6 +21,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Base64; import java.util.Base64;
import javax.servlet.http.Cookie;
import org.dspace.app.rest.builder.GroupBuilder; import org.dspace.app.rest.builder.GroupBuilder;
import org.dspace.app.rest.matcher.AuthenticationStatusMatcher; import org.dspace.app.rest.matcher.AuthenticationStatusMatcher;
@@ -40,6 +41,7 @@ import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
* *
* @author Frederic Van Reet (frederic dot vanreet at atmire dot com) * @author Frederic Van Reet (frederic dot vanreet at atmire dot com)
* @author Tom Desair (tom dot desair at atmire dot com) * @author Tom Desair (tom dot desair at atmire dot com)
* @author Giuseppe Digilio (giuseppe dot digilio at 4science dot it)
*/ */
public class AuthenticationRestControllerIT extends AbstractControllerIntegrationTest { public class AuthenticationRestControllerIT extends AbstractControllerIntegrationTest {
@@ -48,6 +50,9 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
public static final String[] PASS_ONLY = {"org.dspace.authenticate.PasswordAuthentication"}; public static final String[] PASS_ONLY = {"org.dspace.authenticate.PasswordAuthentication"};
public static final String[] SHIB_ONLY = {"org.dspace.authenticate.ShibAuthentication"}; public static final String[] SHIB_ONLY = {"org.dspace.authenticate.ShibAuthentication"};
public static final String[] SHIB_AND_PASS =
{"org.dspace.authenticate.ShibAuthentication",
"org.dspace.authenticate.PasswordAuthentication"};
public static final String[] SHIB_AND_IP = public static final String[] SHIB_AND_IP =
{"org.dspace.authenticate.IPAuthentication", {"org.dspace.authenticate.IPAuthentication",
"org.dspace.authenticate.ShibAuthentication"}; "org.dspace.authenticate.ShibAuthentication"};
@@ -97,7 +102,43 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(content().contentType(contentType)) .andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true))) .andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(false))) .andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.type", is("status"))); .andExpect(jsonPath("$.type", is("status")))
.andExpect(header().string("WWW-Authenticate",
"password realm=\"DSpace REST API\""));
}
@Test
public void testStatusAuthenticatedWithCookie() throws Exception {
context.turnOffAuthorisationSystem();
//Enable Shibboleth login
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY);
context.restoreAuthSystemState();
//Simulate that a shibboleth authentication has happened
String token = getClient().perform(post("/api/authn/login")
.requestAttr("SHIB-MAIL", eperson.getEmail())
.requestAttr("SHIB-SCOPED-AFFILIATION", "faculty;staff"))
.andExpect(status().isOk())
.andReturn().getResponse().getHeader(AUTHORIZATION_HEADER).replace("Bearer ", "");
Cookie[] cookies = new Cookie[1];
cookies[0] = new Cookie(AUTHORIZATION_COOKIE, token);
//Check if we are authenticated with a status request with authorization cookie
getClient().perform(get("/api/authn/status")
.secure(true)
.cookie(cookies))
.andExpect(status().isOk())
//We expect the content type to be "application/hal+json;charset=UTF-8"
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")));
//Logout
getClient(token).perform(get("/api/authn/logout"))
.andExpect(status().isNoContent());
} }
@Test @Test
@@ -354,6 +395,108 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(status().isMethodNotAllowed()); .andExpect(status().isMethodNotAllowed());
} }
@Test
public void testShibbolethLoginURLWithDefaultLazyURL() throws Exception {
context.turnOffAuthorisationSystem();
//Enable Shibboleth login
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY);
//Create a reviewers group
Group reviewersGroup = GroupBuilder.createGroup(context)
.withName("Reviewers")
.build();
//Faculty members are assigned to the Reviewers group
configurationService.setProperty("authentication-shibboleth.role.faculty", "Reviewers");
context.restoreAuthSystemState();
getClient().perform(post("/api/authn/login").header("Referer", "http://my.uni.edu"))
.andExpect(status().isUnauthorized())
.andExpect(header().string("WWW-Authenticate",
"shibboleth realm=\"DSpace REST API\", " +
"location=\"https://localhost/Shibboleth.sso/Login?" +
"target=http%3A%2F%2Flocalhost%2Fapi%2Fauthn%2Fshibboleth%3F" +
"redirectUrl%3Dhttp%3A%2F%2Fmy.uni.edu\""));
}
@Test
public void testShibbolethLoginURLWithServerlURLConteiningPort() throws Exception {
context.turnOffAuthorisationSystem();
//Enable Shibboleth login
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY);
configurationService.setProperty("dspace.server.url", "http://localhost:8080/server");
configurationService.setProperty("authentication-shibboleth.lazysession.secure", false);
//Create a reviewers group
Group reviewersGroup = GroupBuilder.createGroup(context)
.withName("Reviewers")
.build();
//Faculty members are assigned to the Reviewers group
configurationService.setProperty("authentication-shibboleth.role.faculty", "Reviewers");
context.restoreAuthSystemState();
getClient().perform(post("/api/authn/login").header("Referer", "http://my.uni.edu"))
.andExpect(status().isUnauthorized())
.andExpect(header().string("WWW-Authenticate",
"shibboleth realm=\"DSpace REST API\", " +
"location=\"http://localhost:8080/Shibboleth.sso/Login?" +
"target=http%3A%2F%2Flocalhost%3A8080%2Fserver%2Fapi%2Fauthn%2Fshibboleth%3F" +
"redirectUrl%3Dhttp%3A%2F%2Fmy.uni.edu\""));
}
@Test
public void testShibbolethLoginURLWithConfiguredLazyURL() throws Exception {
context.turnOffAuthorisationSystem();
//Enable Shibboleth login
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY);
configurationService.setProperty("authentication-shibboleth.lazysession.loginurl",
"http://shibboleth.org/Shibboleth.sso/Login");
//Create a reviewers group
Group reviewersGroup = GroupBuilder.createGroup(context)
.withName("Reviewers")
.build();
//Faculty members are assigned to the Reviewers group
configurationService.setProperty("authentication-shibboleth.role.faculty", "Reviewers");
context.restoreAuthSystemState();
getClient().perform(post("/api/authn/login").header("Referer", "http://my.uni.edu"))
.andExpect(status().isUnauthorized())
.andExpect(header().string("WWW-Authenticate",
"shibboleth realm=\"DSpace REST API\", " +
"location=\"http://shibboleth.org/Shibboleth.sso/Login?" +
"target=http%3A%2F%2Flocalhost%2Fapi%2Fauthn%2Fshibboleth%3F" +
"redirectUrl%3Dhttp%3A%2F%2Fmy.uni.edu\""));
}
@Test
public void testShibbolethLoginURLWithConfiguredLazyURLWithPort() throws Exception {
context.turnOffAuthorisationSystem();
//Enable Shibboleth login
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY);
configurationService.setProperty("authentication-shibboleth.lazysession.loginurl",
"http://shibboleth.org:8080/Shibboleth.sso/Login");
//Create a reviewers group
Group reviewersGroup = GroupBuilder.createGroup(context)
.withName("Reviewers")
.build();
//Faculty members are assigned to the Reviewers group
configurationService.setProperty("authentication-shibboleth.role.faculty", "Reviewers");
context.restoreAuthSystemState();
getClient().perform(post("/api/authn/login").header("Referer", "http://my.uni.edu"))
.andExpect(status().isUnauthorized())
.andExpect(header().string("WWW-Authenticate",
"shibboleth realm=\"DSpace REST API\", " +
"location=\"http://shibboleth.org:8080/Shibboleth.sso/Login?" +
"target=http%3A%2F%2Flocalhost%2Fapi%2Fauthn%2Fshibboleth%3F" +
"redirectUrl%3Dhttp%3A%2F%2Fmy.uni.edu\""));
}
@Test @Test
@Ignore @Ignore
// Ignored until an endpoint is added to return all groups // Ignored until an endpoint is added to return all groups
@@ -375,7 +518,9 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(status().isUnauthorized()) .andExpect(status().isUnauthorized())
.andExpect(header().string("WWW-Authenticate", .andExpect(header().string("WWW-Authenticate",
"shibboleth realm=\"DSpace REST API\", " + "shibboleth realm=\"DSpace REST API\", " +
"location=\"/Shibboleth.sso/Login?target=http%3A%2F%2Fmy.uni.edu\"")); "location=\"https://localhost/Shibboleth.sso/Login?" +
"target=http%3A%2F%2Flocalhost%2Fapi%2Fauthn%2Fshibboleth%3F" +
"redirectUrl%3Dhttp%3A%2F%2Fmy.uni.edu\""));
//Simulate that a shibboleth authentication has happened //Simulate that a shibboleth authentication has happened
@@ -411,7 +556,9 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
.andExpect(status().isUnauthorized()) .andExpect(status().isUnauthorized())
.andExpect(header().string("WWW-Authenticate", .andExpect(header().string("WWW-Authenticate",
"ip realm=\"DSpace REST API\", shibboleth realm=\"DSpace REST API\", " + "ip realm=\"DSpace REST API\", shibboleth realm=\"DSpace REST API\", " +
"location=\"/Shibboleth.sso/Login?target=http%3A%2F%2Fmy.uni.edu\"")); "location=\"https://localhost/Shibboleth.sso/Login?" +
"target=http%3A%2F%2Flocalhost%2Fapi%2Fauthn%2Fshibboleth%3F" +
"redirectUrl%3Dhttp%3A%2F%2Fmy.uni.edu\""));
//Simulate that a shibboleth authentication has happened //Simulate that a shibboleth authentication has happened
String token = getClient().perform(post("/api/authn/login") String token = getClient().perform(post("/api/authn/login")
@@ -454,4 +601,164 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio
EPersonMatcher.matchEPersonWithGroups(eperson.getEmail(), "Anonymous"))); EPersonMatcher.matchEPersonWithGroups(eperson.getEmail(), "Anonymous")));
} }
@Test
public void testShibbolethAndPasswordAuthentication() throws Exception {
context.turnOffAuthorisationSystem();
//Enable Shibboleth and password login
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_AND_PASS);
context.restoreAuthSystemState();
//Check if WWW-Authenticate header contains shibboleth and password
getClient().perform(get("/api/authn/status").header("Referer", "http://my.uni.edu"))
.andExpect(status().isOk())
.andExpect(header().string("WWW-Authenticate",
"shibboleth realm=\"DSpace REST API\", " +
"location=\"https://localhost/Shibboleth.sso/Login?" +
"target=http%3A%2F%2Flocalhost%2Fapi%2Fauthn%2Fshibboleth%3F" +
"redirectUrl%3Dhttp%3A%2F%2Fmy.uni.edu\"" +
", password realm=\"DSpace REST API\""));
//Simulate a password authentication
String token = getAuthToken(eperson.getEmail(), password);
//Check if we have a valid token
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")));
//Logout
getClient(token).perform(get("/api/authn/logout"))
.andExpect(status().isNoContent());
//Check if we are actually logged out
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(false)))
.andExpect(jsonPath("$.type", is("status")));
//Simulate that a shibboleth authentication has happened
token = getClient().perform(post("/api/authn/login")
.requestAttr("SHIB-MAIL", eperson.getEmail())
.requestAttr("SHIB-SCOPED-AFFILIATION", "faculty;staff"))
.andExpect(status().isOk())
.andReturn().getResponse().getHeader(AUTHORIZATION_HEADER).replace("Bearer ", "");
//Check if we have a valid token
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")));
//Logout
getClient(token).perform(get("/api/authn/logout"))
.andExpect(status().isNoContent());
}
@Test
public void testOnlyPasswordAuthenticationWorks() throws Exception {
context.turnOffAuthorisationSystem();
//Enable only password login
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", PASS_ONLY);
context.restoreAuthSystemState();
//Check if WWW-Authenticate header contains only
getClient().perform(get("/api/authn/status").header("Referer", "http://my.uni.edu"))
.andExpect(status().isOk())
.andExpect(header().string("WWW-Authenticate",
"password realm=\"DSpace REST API\""));
//Simulate a password authentication
String token = getAuthToken(eperson.getEmail(), password);
//Check if we have a valid token
getClient(token).perform(get("/api/authn/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.okay", is(true)))
.andExpect(jsonPath("$.authenticated", is(true)))
.andExpect(jsonPath("$.type", is("status")));
//Logout
getClient(token).perform(get("/api/authn/logout"))
.andExpect(status().isNoContent());
}
@Test
public void testShibbolethAuthenticationDoesNotWorkWithPassOnly() throws Exception {
context.turnOffAuthorisationSystem();
//Enable only password login
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", PASS_ONLY);
context.restoreAuthSystemState();
//Check if WWW-Authenticate header contains only password
getClient().perform(get("/api/authn/status").header("Referer", "http://my.uni.edu"))
.andExpect(status().isOk())
.andExpect(header().string("WWW-Authenticate",
"password realm=\"DSpace REST API\""));
//Check if a shibboleth authentication fails
getClient().perform(post("/api/authn/login")
.requestAttr("SHIB-MAIL", eperson.getEmail())
.requestAttr("SHIB-SCOPED-AFFILIATION", "faculty;staff"))
.andExpect(status().isUnauthorized());
}
@Test
public void testOnlyShibbolethAuthenticationWorks() throws Exception {
context.turnOffAuthorisationSystem();
//Enable only Shibboleth login
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY);
context.restoreAuthSystemState();
//Check if WWW-Authenticate header contains only shibboleth
getClient().perform(get("/api/authn/status").header("Referer", "http://my.uni.edu"))
.andExpect(status().isOk())
.andExpect(header().string("WWW-Authenticate",
"shibboleth realm=\"DSpace REST API\", " +
"location=\"https://localhost/Shibboleth.sso/Login?" +
"target=http%3A%2F%2Flocalhost%2Fapi%2Fauthn%2Fshibboleth%3F" +
"redirectUrl%3Dhttp%3A%2F%2Fmy.uni.edu\""));
//Simulate that a shibboleth authentication has happened
String token = getClient().perform(post("/api/authn/login")
.requestAttr("SHIB-MAIL", eperson.getEmail())
.requestAttr("SHIB-SCOPED-AFFILIATION", "faculty;staff"))
.andExpect(status().isOk())
.andReturn().getResponse().getHeader(AUTHORIZATION_HEADER);
//Logout
getClient(token).perform(get("/api/authn/logout"))
.andExpect(status().isNoContent());
}
@Test
public void testPasswordAuthenticationDoesNotWorkWithShibOnly() throws Exception {
context.turnOffAuthorisationSystem();
//Enable only Shibboleth login
configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY);
//Create a reviewers group
Group reviewersGroup = GroupBuilder.createGroup(context)
.withName("Reviewers")
.build();
//Faculty members are assigned to the Reviewers group
configurationService.setProperty("authentication-shibboleth.role.faculty", "Reviewers");
context.restoreAuthSystemState();
getClient().perform(post("/api/authn/login")
.param("user", eperson.getEmail())
.param("password", password))
.andExpect(status().isUnauthorized());
}
} }

View File

@@ -0,0 +1,42 @@
/**
* 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;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.junit.Test;
/**
* Integration test that cover ShibbolethRestController
*
* @author Giuseppe Digilio (giuseppe dot digilio at 4science dot it)
*/
public class ShibbolethRestControllerIT extends AbstractControllerIntegrationTest {
@Test
public void testRedirectToDefaultDspaceUrl() throws Exception {
String token = getAuthToken(eperson.getEmail(), password);
getClient(token).perform(get("/api/authn/shibboleth"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost:3000"));
}
@Test
public void testRedirectToGivenUrl() throws Exception {
String token = getAuthToken(eperson.getEmail(), password);
getClient(token).perform(get("/api/authn/shibboleth")
.param("redirectUrl", "http://dspace.org"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://dspace.org"));
}
}

View File

@@ -71,6 +71,7 @@ import org.springframework.web.context.WebApplicationContext;
public class AbstractControllerIntegrationTest extends AbstractIntegrationTestWithDatabase { public class AbstractControllerIntegrationTest extends AbstractIntegrationTestWithDatabase {
protected static final String AUTHORIZATION_HEADER = "Authorization"; protected static final String AUTHORIZATION_HEADER = "Authorization";
protected static final String AUTHORIZATION_COOKIE = "Authorization-cookie";
//The Authorization header contains a value like "Bearer TOKENVALUE". This constant string represents the part that //The Authorization header contains a value like "Bearer TOKENVALUE". This constant string represents the part that
//sits before the actual authentication token and can be used to easily compose or parse the Authorization header. //sits before the actual authentication token and can be used to easily compose or parse the Authorization header.

View File

@@ -38,6 +38,8 @@
authentication-shibboleth.lazysession = true authentication-shibboleth.lazysession = true
# The url to start a shibboleth session (only for lazy sessions) # The url to start a shibboleth session (only for lazy sessions)
# This must contain an absolute URL (e.g. http://dsapce-org/Shibboleth.sso/Login) or
# a relative path that start with slash (e.g. /Shibboleth.sso/Login)
authentication-shibboleth.lazysession.loginurl = /Shibboleth.sso/Login authentication-shibboleth.lazysession.loginurl = /Shibboleth.sso/Login
# Force HTTPS when authenticating (only for lazy sessions) # Force HTTPS when authenticating (only for lazy sessions)