1
0
mirror of synced 2026-08-06 02:08:01 +00:00

SEC-2321: Improve Java Config defaults for JavaScript clients

This commit is contained in:
Rob Winch
2013-10-11 11:16:51 -05:00
parent 7d99436740
commit 4ef0460ef6
29 changed files with 19939 additions and 18 deletions
@@ -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);
}
@@ -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());
}
}
}
@@ -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);
}
}
}
@@ -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")
}
}
@@ -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
@@ -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()
}
@@ -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()
@@ -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 {