DS-3542: Added custom DSpace AuthenticationEntryPoint in order to return 401 status

This commit is contained in:
Tom Desair
2018-08-21 16:50:48 +02:00
parent 44a0f9b2de
commit 93bac51ead
15 changed files with 106 additions and 50 deletions

View File

@@ -0,0 +1,41 @@
/**
* 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 javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
/**
* Spring security authentication entry point to return a 401 response for unauthorized requests
* This class is used in the {@link WebSecurityConfiguration} class.
*/
public class DSpace401AuthenticationEntryPoint implements AuthenticationEntryPoint {
private RestAuthenticationService restAuthenticationService;
public DSpace401AuthenticationEntryPoint(RestAuthenticationService restAuthenticationService) {
this.restAuthenticationService = restAuthenticationService;
}
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setHeader("WWW-Authenticate",
restAuthenticationService.getWwwAuthenticateHeaderValue(request, response));
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
authException.getMessage());
}
}

View File

@@ -35,4 +35,13 @@ public interface RestAuthenticationService {
void invalidateAuthenticationData(HttpServletRequest request, Context context) throws Exception;
AuthenticationService getAuthenticationService();
/**
* Return the value that should be passed in the WWWW-Authenticate header for 4xx responses to the client
* @param request The current client request
* @param response The response being build for the client
* @return A string value that should be set in the WWWW-Authenticate header
*/
String getWwwAuthenticateHeaderValue(HttpServletRequest request, HttpServletResponse response);
}

View File

@@ -9,17 +9,11 @@ package org.dspace.app.rest.security;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.authenticate.AuthenticationMethod;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.core.Context;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
@@ -77,31 +71,10 @@ public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
AuthenticationService authenticationService = restAuthenticationService.getAuthenticationService();
String authenticateHeaderValue = restAuthenticationService.getWwwAuthenticateHeaderValue(request, response);
Iterator<AuthenticationMethod> authenticationMethodIterator
= authenticationService.authenticationMethodIterator();
Context context = ContextUtil.obtainContext(request);
StringBuilder wwwAuthenticate = new StringBuilder();
while (authenticationMethodIterator.hasNext()) {
AuthenticationMethod authenticationMethod = authenticationMethodIterator.next();
if (wwwAuthenticate.length() > 0) {
wwwAuthenticate.append(", ");
}
wwwAuthenticate.append(authenticationMethod.getName()).append(" realm=\"DSpace REST API\"");
String loginPageURL = authenticationMethod.loginPageURL(context, request, response);
if (StringUtils.isNotBlank(loginPageURL)) {
// We cannot reply with a 303 code because may browsers handle 3xx response codes transparently. This
// means that the JavaScript client code is not aware of the 303 status and fails to react accordingly.
wwwAuthenticate.append(", location=\"").append(loginPageURL).append("\"");
}
}
response.setHeader("WWW-Authenticate", wwwAuthenticate.toString());
response.setHeader("WWW-Authenticate", authenticateHeaderValue);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, failed.getMessage());
}
}

View File

@@ -77,6 +77,10 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
//Disable CSRF as our API can be used by clients on an other domain, we are also protected against this,
// since we pass the token in a header
.csrf().disable()
//Return 401 on authorization failures
.exceptionHandling().authenticationEntryPoint(
new DSpace401AuthenticationEntryPoint(restAuthenticationService))
.and()
//Logout configuration
.logout()

View File

@@ -10,6 +10,7 @@ package org.dspace.app.rest.security.jwt;
import java.io.IOException;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.Iterator;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -19,6 +20,7 @@ import org.apache.commons.lang.StringUtils;
import org.dspace.app.rest.security.DSpaceAuthentication;
import org.dspace.app.rest.security.RestAuthenticationService;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.authenticate.AuthenticationMethod;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
@@ -111,6 +113,33 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
return authenticationService;
}
@Override
public String getWwwAuthenticateHeaderValue(final HttpServletRequest request, final HttpServletResponse response) {
Iterator<AuthenticationMethod> authenticationMethodIterator
= authenticationService.authenticationMethodIterator();
Context context = ContextUtil.obtainContext(request);
StringBuilder wwwAuthenticate = new StringBuilder();
while (authenticationMethodIterator.hasNext()) {
AuthenticationMethod authenticationMethod = authenticationMethodIterator.next();
if (wwwAuthenticate.length() > 0) {
wwwAuthenticate.append(", ");
}
wwwAuthenticate.append(authenticationMethod.getName()).append(" realm=\"DSpace REST API\"");
String loginPageURL = authenticationMethod.loginPageURL(context, request, response);
if (org.apache.commons.lang3.StringUtils.isNotBlank(loginPageURL)) {
// We cannot reply with a 303 code because may browsers handle 3xx response codes transparently. This
// means that the JavaScript client code is not aware of the 303 status and fails to react accordingly.
wwwAuthenticate.append(", location=\"").append(loginPageURL).append("\"");
}
}
return wwwAuthenticate.toString();
}
private void addTokenToResponse(final HttpServletResponse response, final String token) throws IOException {
response.setHeader(AUTHORIZATION_HEADER, String.format("%s %s", AUTHORIZATION_TYPE, token));
}

View File

@@ -99,7 +99,7 @@ public class RestRepositoryUtils {
*/
public Method getSearchMethod(String searchMethodName, DSpaceRestRepository repository) {
Method searchMethod = null;
Method[] methods = org.springframework.util.ClassUtils.getUserClass(repository.getClass()).getMethods();
Method[] methods = ClassUtils.getUserClass(repository.getClass()).getMethods();
for (Method method : methods) {
SearchRestMethod ann =
AnnotationUtils.findAnnotation(method, SearchRestMethod.class);

View File

@@ -260,7 +260,7 @@ public class BitstreamContentRestControllerIT extends AbstractControllerIntegrat
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))
//** THEN **
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
//An unauthorized request should not log statistics
checkNumberOfStatsRecords(bitstream, 0);
@@ -306,7 +306,7 @@ public class BitstreamContentRestControllerIT extends AbstractControllerIntegrat
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))
//** THEN **
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
//An unauthorized request should not log statistics
checkNumberOfStatsRecords(bitstream, 0);

View File

@@ -177,7 +177,7 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest
)));
getClient().perform(get("/api/core/bitstreams/"))
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
}
//TODO Re-enable test after https://jira.duraspace.org/browse/DS-3774 is fixed

View File

@@ -109,7 +109,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
;
getClient().perform(get("/api/eperson/epersons"))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
;
}
@@ -183,7 +183,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
;
getClient().perform(get("/api/eperson/epersons"))
.andExpect(status().isForbidden())
.andExpect(status().isUnauthorized())
;
}
@@ -264,7 +264,6 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
}
@Test
public void findOneTestWrongUUID() throws Exception {
context.turnOffAuthorisationSystem();

View File

@@ -34,7 +34,7 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
getClient().perform(get("/api/eperson/groups"))
//The status has to be 403 Not Authorized
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
@@ -59,7 +59,7 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
getClient().perform(get("/api/eperson/groups"))
//The status has to be 403 Not Authorized
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);

View File

@@ -932,6 +932,7 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
}
@Test
public void makeUnDiscoverablePatchForbiddenTest() throws Exception {
context.turnOffAuthorisationSystem();
@@ -1201,7 +1202,7 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
//An anonymous user is not allowed to view embargoed items
getClient().perform(get("/api/core/items/" + embargoedItem1.getID()))
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
//An admin user is allowed to access the embargoed item
String token1 = getAuthToken(admin.getEmail(), password);
@@ -1319,7 +1320,7 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
//An anonymous user is not allowed to the restricted item
getClient().perform(get("/api/core/items/" + restrictedItem1.getID()))
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
//An admin user is allowed to access the restricted item
String token1 = getAuthToken(admin.getEmail(), password);

View File

@@ -37,7 +37,7 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra
//When we call the root endpoint as anonymous user
getClient().perform(get("/api/config/submissiondefinitions"))
//The status has to be 403 Not Authorized
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
@@ -67,7 +67,7 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra
getClient().perform(get("/api/config/submissiondefinitions/traditional"))
//The status has to be 403 Not Authorized
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
@@ -98,7 +98,7 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra
.param("uuid", col1.getID().toString()))
//** THEN **
//The status has to be 200
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
@@ -122,7 +122,7 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra
//Match only that a section exists with a submission configuration behind
getClient().perform(get("/api/config/submissiondefinitions/traditional/collections"))
//The status has to be 403 Not Authorized
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
@@ -141,7 +141,7 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra
getClient().perform(get("/api/config/submissiondefinitions/traditional/sections"))
//The status has to be 403 Not Authorized
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);

View File

@@ -31,7 +31,7 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe
//When we call the root endpoint as anonymous user
getClient().perform(get("/api/config/submissionforms"))
//The status has to be 403 Not Authorized
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);
@@ -62,7 +62,7 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe
//When we call the root endpoint as anonymous user
getClient().perform(get("/api/config/submissionforms/traditionalpageone"))
//The status has to be 403 Not Authorized
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);

View File

@@ -30,7 +30,7 @@ public class SubmissionSectionsControllerIT extends AbstractControllerIntegratio
//When we call the root endpoint as anonymous user
getClient().perform(get("/api/config/submissionsections"))
//The status has to be 403 Not Authorized
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);

View File

@@ -30,7 +30,7 @@ public class SubmissionUploadsControllerIT extends AbstractControllerIntegration
//When we call the root endpoint as anonymous user
getClient().perform(get("/api/config/submissionuploads"))
//The status has to be 403 Not Authorized
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized());
String token = getAuthToken(admin.getEmail(), password);