SEC-2321: Improve Java Config defaults for JavaScript clients
This commit is contained in:
+4
-1
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -235,7 +237,8 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecu
|
||||
if(contentNegotiationStrategy == null) {
|
||||
contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
|
||||
}
|
||||
RequestMatcher preferredMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.APPLICATION_XHTML_XML, new MediaType("image","*"), MediaType.TEXT_HTML, MediaType.TEXT_PLAIN);
|
||||
MediaTypeRequestMatcher preferredMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.APPLICATION_XHTML_XML, new MediaType("image","*"), MediaType.TEXT_HTML, MediaType.TEXT_PLAIN);
|
||||
preferredMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint), preferredMatcher);
|
||||
}
|
||||
|
||||
|
||||
+41
-8
@@ -15,20 +15,28 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.util.MediaTypeRequestMatcher;
|
||||
import org.springframework.security.web.util.RequestHeaderRequestMatcher;
|
||||
import org.springframework.security.web.util.RequestMatcher;
|
||||
import org.springframework.web.accept.ContentNegotiationStrategy;
|
||||
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
|
||||
@@ -66,10 +74,11 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy;
|
||||
* @since 3.2
|
||||
*/
|
||||
public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<HttpBasicConfigurer<B>,B> {
|
||||
private static final String DEFAULT_REALM = "Spring Security Application";
|
||||
private static final String DEFAULT_REALM = "Realm";
|
||||
|
||||
private AuthenticationEntryPoint authenticationEntryPoint = new BasicAuthenticationEntryPoint();
|
||||
private AuthenticationEntryPoint authenticationEntryPoint;
|
||||
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource;
|
||||
private BasicAuthenticationEntryPoint basicAuthEntryPoint = new BasicAuthenticationEntryPoint();
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
@@ -78,12 +87,19 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends
|
||||
*/
|
||||
public HttpBasicConfigurer() throws Exception {
|
||||
realmName(DEFAULT_REALM);
|
||||
|
||||
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>();
|
||||
entryPoints.put(new RequestHeaderRequestMatcher("X-Requested-With"), new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
|
||||
|
||||
DelegatingAuthenticationEntryPoint defaultEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
|
||||
defaultEntryPoint.setDefaultEntryPoint(basicAuthEntryPoint);
|
||||
authenticationEntryPoint = defaultEntryPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for {@link #authenticationEntryPoint(AuthenticationEntryPoint)}
|
||||
* specifying a {@link BasicAuthenticationEntryPoint} with the specified
|
||||
* realm name.
|
||||
* Allows easily changing the realm, but leaving the remaining defaults in
|
||||
* place. If {@link #authenticationEntryPoint(AuthenticationEntryPoint)} has
|
||||
* been invoked, invoking this method will result in an error.
|
||||
*
|
||||
* @param realmName
|
||||
* the HTTP Basic realm to use
|
||||
@@ -91,10 +107,9 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends
|
||||
* @throws Exception
|
||||
*/
|
||||
public HttpBasicConfigurer<B> realmName(String realmName) throws Exception {
|
||||
BasicAuthenticationEntryPoint basicAuthEntryPoint = new BasicAuthenticationEntryPoint();
|
||||
basicAuthEntryPoint.setRealmName(realmName);
|
||||
basicAuthEntryPoint.afterPropertiesSet();
|
||||
return authenticationEntryPoint(basicAuthEntryPoint);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,6 +156,8 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends
|
||||
MediaTypeRequestMatcher preferredMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML, MediaType.MULTIPART_FORM_DATA, MediaType.TEXT_XML);
|
||||
preferredMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint), preferredMatcher);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,4 +170,20 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends
|
||||
basicAuthenticationFilter = postProcess(basicAuthenticationFilter);
|
||||
http.addFilter(basicAuthenticationFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private static class HttpStatusEntryPoint implements AuthenticationEntryPoint {
|
||||
private final HttpStatus httpStatus;
|
||||
|
||||
public HttpStatusEntryPoint(HttpStatus httpStatus) {
|
||||
super();
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
|
||||
public void commence(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException authException) throws IOException,
|
||||
ServletException {
|
||||
response.setStatus(httpStatus.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||
@@ -22,7 +25,9 @@ import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
|
||||
import org.springframework.security.web.util.AndRequestMatcher;
|
||||
import org.springframework.security.web.util.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.MediaTypeRequestMatcher;
|
||||
import org.springframework.security.web.util.NegatedRequestMatcher;
|
||||
import org.springframework.security.web.util.RequestHeaderRequestMatcher;
|
||||
import org.springframework.security.web.util.RequestMatcher;
|
||||
import org.springframework.web.accept.ContentNegotiationStrategy;
|
||||
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
|
||||
@@ -116,6 +121,12 @@ public final class RequestCacheConfigurer<H extends HttpSecurityBuilder<H>> exte
|
||||
}
|
||||
RequestMatcher getRequests = new AntPathRequestMatcher("/**", "GET");
|
||||
RequestMatcher notFavIcon = new NegatedRequestMatcher(new AntPathRequestMatcher("/**/favicon.ico"));
|
||||
return new AndRequestMatcher(getRequests,notFavIcon);
|
||||
|
||||
MediaTypeRequestMatcher jsonRequest = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.APPLICATION_JSON);
|
||||
jsonRequest.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
||||
RequestMatcher notJson = new NegatedRequestMatcher(jsonRequest);
|
||||
|
||||
RequestMatcher notXRequestedWith = new NegatedRequestMatcher(new RequestHeaderRequestMatcher("X-Requested-With"));
|
||||
return new AndRequestMatcher(getRequests, notFavIcon, notJson, notXRequestedWith);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -331,16 +331,21 @@ public class NamespaceHttpTests extends BaseSpringSpec {
|
||||
// http@pattern is not available (instead see the tests http@request-matcher-ref ant or http@request-matcher-ref regex)
|
||||
|
||||
def "http@realm"() {
|
||||
when:
|
||||
setup:
|
||||
loadConfig(RealmConfig)
|
||||
when:
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then:
|
||||
findFilter(BasicAuthenticationFilter).authenticationEntryPoint.realmName == "RealmConfig"
|
||||
response.getHeader("WWW-Authenticate") == 'Basic realm="RealmConfig"'
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class RealmConfig extends BaseWebConfig {
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.httpBasic().realmName("RealmConfig")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -71,7 +71,6 @@ class ExceptionHandlingConfigurerTests extends BaseSpringSpec {
|
||||
response.status == httpStatus
|
||||
where:
|
||||
acceptHeader | httpStatus
|
||||
MediaType.ALL_VALUE | HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
MediaType.APPLICATION_XHTML_XML_VALUE | HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
MediaType.IMAGE_GIF_VALUE | HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
MediaType.IMAGE_JPEG_VALUE | HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
@@ -165,7 +164,7 @@ class ExceptionHandlingConfigurerTests extends BaseSpringSpec {
|
||||
when:
|
||||
loadConfig(BasicAuthenticationEntryPointBeforeFormLoginConf)
|
||||
then:
|
||||
findFilter(ExceptionTranslationFilter).authenticationEntryPoint.defaultEntryPoint.class == BasicAuthenticationEntryPoint
|
||||
findFilter(ExceptionTranslationFilter).authenticationEntryPoint.defaultEntryPoint.defaultEntryPoint.class == BasicAuthenticationEntryPoint
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
|
||||
+8
-2
@@ -48,10 +48,13 @@ class HttpBasicConfigurerTests extends BaseSpringSpec {
|
||||
}
|
||||
|
||||
def "SEC-2198: http.httpBasic() defaults AuthenticationEntryPoint"() {
|
||||
when:
|
||||
setup:
|
||||
loadConfig(DefaultsEntryPointConfig)
|
||||
when:
|
||||
springSecurityFilterChain.doFilter(request, response, chain)
|
||||
then:
|
||||
findFilter(ExceptionTranslationFilter).authenticationEntryPoint.class == BasicAuthenticationEntryPoint
|
||||
response.status == 401
|
||||
response.getHeader("WWW-Authenticate") == 'Basic realm="Realm"'
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@@ -60,6 +63,9 @@ class HttpBasicConfigurerTests extends BaseSpringSpec {
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.httpBasic()
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ public class NamespaceHttpBasicTests extends BaseSpringSpec {
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "unauthorized"
|
||||
response.status == HttpServletResponse.SC_UNAUTHORIZED
|
||||
response.getHeader("WWW-Authenticate") == 'Basic realm="Spring Security Application"'
|
||||
response.getHeader("WWW-Authenticate") == 'Basic realm="Realm"'
|
||||
when: "login success"
|
||||
super.setup()
|
||||
basicLogin()
|
||||
|
||||
+73
@@ -28,6 +28,8 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
|
||||
import org.springframework.security.web.savedrequest.RequestCache
|
||||
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
|
||||
|
||||
import spock.lang.Unroll;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rob Winch
|
||||
@@ -87,6 +89,77 @@ class RequestCacheConfigurerTests extends BaseSpringSpec {
|
||||
response.redirectedUrl == "/"
|
||||
}
|
||||
|
||||
def "SEC-2321: RequestCache disables application/json"() {
|
||||
setup:
|
||||
loadConfig(RequestCacheDefautlsConfig)
|
||||
request.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE)
|
||||
request.method = "GET"
|
||||
request.servletPath = "/messages"
|
||||
request.requestURI = "/messages"
|
||||
when: "request application/json"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to the login page"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "http://localhost/login"
|
||||
when: "authenticate successfully"
|
||||
super.setupWeb(request.session)
|
||||
request.servletPath = "/login"
|
||||
request.setParameter("username","user")
|
||||
request.setParameter("password","password")
|
||||
request.method = "POST"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to default URL since it was application/json. This is desirable since JSON requests are typically not invoked directly from the browser and we don't want the browser to replay them"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "/"
|
||||
}
|
||||
|
||||
def "SEC-2321: RequestCache disables X-Requested-With"() {
|
||||
setup:
|
||||
loadConfig(RequestCacheDefautlsConfig)
|
||||
request.addHeader("X-Requested-With", "XMLHttpRequest")
|
||||
request.method = "GET"
|
||||
request.servletPath = "/messages"
|
||||
request.requestURI = "/messages"
|
||||
when: "request X-Requested-With"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to the login page"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "http://localhost/login"
|
||||
when: "authenticate successfully"
|
||||
super.setupWeb(request.session)
|
||||
request.servletPath = "/login"
|
||||
request.setParameter("username","user")
|
||||
request.setParameter("password","password")
|
||||
request.method = "POST"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to default URL since it was X-Requested-With"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "/"
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def "RequestCache saves Accept: #accept"() {
|
||||
setup:
|
||||
loadConfig(RequestCacheDefautlsConfig)
|
||||
request.addHeader("Accept", accept)
|
||||
request.method = "GET"
|
||||
request.servletPath = "/messages"
|
||||
request.requestURI = "/messages"
|
||||
when: "request content type"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
super.setupWeb(request.session)
|
||||
request.servletPath = "/login"
|
||||
request.setParameter("username","user")
|
||||
request.setParameter("password","password")
|
||||
request.method = "POST"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to saved URL"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "http://localhost/messages"
|
||||
where:
|
||||
accept << [MediaType.ALL_VALUE, MediaType.TEXT_HTML, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"]
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class RequestCacheDefautlsConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
Reference in New Issue
Block a user