mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-07 01:54:22 +00:00
DS-3542: Added custom DSpace AuthenticationEntryPoint in order to return 401 status
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -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));
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user