Skip to content

Commit 4ef0460

Browse files
committed
SEC-2321: Improve Java Config defaults for JavaScript clients
1 parent 7d99436 commit 4ef0460

File tree

29 files changed

+19939
-18
lines changed

29 files changed

+19939
-18
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.security.config.annotation.web.configurers;
1717

18+
import java.util.Collections;
19+
1820
import javax.servlet.http.HttpServletRequest;
1921

2022
import org.springframework.http.MediaType;
@@ -235,7 +237,8 @@ private void registerDefaultAuthenticationEntryPoint(B http) {
235237
if(contentNegotiationStrategy == null) {
236238
contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
237239
}
238-
RequestMatcher preferredMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.APPLICATION_XHTML_XML, new MediaType("image","*"), MediaType.TEXT_HTML, MediaType.TEXT_PLAIN);
240+
MediaTypeRequestMatcher preferredMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.APPLICATION_XHTML_XML, new MediaType("image","*"), MediaType.TEXT_HTML, MediaType.TEXT_PLAIN);
241+
preferredMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
239242
exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint), preferredMatcher);
240243
}
241244

config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,28 @@
1515
*/
1616
package org.springframework.security.config.annotation.web.configurers;
1717

18+
import java.io.IOException;
1819
import java.util.Collections;
20+
import java.util.LinkedHashMap;
1921

22+
import javax.servlet.ServletException;
2023
import javax.servlet.http.HttpServletRequest;
24+
import javax.servlet.http.HttpServletResponse;
2125

26+
import org.springframework.http.HttpStatus;
2227
import org.springframework.http.MediaType;
2328
import org.springframework.security.authentication.AuthenticationDetailsSource;
2429
import org.springframework.security.authentication.AuthenticationManager;
2530
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2631
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
32+
import org.springframework.security.core.AuthenticationException;
2733
import org.springframework.security.web.AuthenticationEntryPoint;
34+
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
2835
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
2936
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
3037
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
3138
import org.springframework.security.web.util.MediaTypeRequestMatcher;
39+
import org.springframework.security.web.util.RequestHeaderRequestMatcher;
3240
import org.springframework.security.web.util.RequestMatcher;
3341
import org.springframework.web.accept.ContentNegotiationStrategy;
3442
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
@@ -66,10 +74,11 @@
6674
* @since 3.2
6775
*/
6876
public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<HttpBasicConfigurer<B>,B> {
69-
private static final String DEFAULT_REALM = "Spring Security Application";
77+
private static final String DEFAULT_REALM = "Realm";
7078

71-
private AuthenticationEntryPoint authenticationEntryPoint = new BasicAuthenticationEntryPoint();
79+
private AuthenticationEntryPoint authenticationEntryPoint;
7280
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource;
81+
private BasicAuthenticationEntryPoint basicAuthEntryPoint = new BasicAuthenticationEntryPoint();
7382

7483
/**
7584
* Creates a new instance
@@ -78,23 +87,29 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends
7887
*/
7988
public HttpBasicConfigurer() throws Exception {
8089
realmName(DEFAULT_REALM);
90+
91+
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>();
92+
entryPoints.put(new RequestHeaderRequestMatcher("X-Requested-With"), new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
93+
94+
DelegatingAuthenticationEntryPoint defaultEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
95+
defaultEntryPoint.setDefaultEntryPoint(basicAuthEntryPoint);
96+
authenticationEntryPoint = defaultEntryPoint;
8197
}
8298

8399
/**
84-
* Shortcut for {@link #authenticationEntryPoint(AuthenticationEntryPoint)}
85-
* specifying a {@link BasicAuthenticationEntryPoint} with the specified
86-
* realm name.
100+
* Allows easily changing the realm, but leaving the remaining defaults in
101+
* place. If {@link #authenticationEntryPoint(AuthenticationEntryPoint)} has
102+
* been invoked, invoking this method will result in an error.
87103
*
88104
* @param realmName
89105
* the HTTP Basic realm to use
90106
* @return {@link HttpBasicConfigurer} for additional customization
91107
* @throws Exception
92108
*/
93109
public HttpBasicConfigurer<B> realmName(String realmName) throws Exception {
94-
BasicAuthenticationEntryPoint basicAuthEntryPoint = new BasicAuthenticationEntryPoint();
95110
basicAuthEntryPoint.setRealmName(realmName);
96111
basicAuthEntryPoint.afterPropertiesSet();
97-
return authenticationEntryPoint(basicAuthEntryPoint);
112+
return this;
98113
}
99114

100115
/**
@@ -141,6 +156,8 @@ private void registerDefaultAuthenticationEntryPoint(B http) {
141156
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);
142157
preferredMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
143158
exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint), preferredMatcher);
159+
160+
144161
}
145162

146163
@Override
@@ -153,4 +170,20 @@ public void configure(B http) throws Exception {
153170
basicAuthenticationFilter = postProcess(basicAuthenticationFilter);
154171
http.addFilter(basicAuthenticationFilter);
155172
}
156-
}
173+
174+
private static class HttpStatusEntryPoint implements AuthenticationEntryPoint {
175+
private final HttpStatus httpStatus;
176+
177+
public HttpStatusEntryPoint(HttpStatus httpStatus) {
178+
super();
179+
this.httpStatus = httpStatus;
180+
}
181+
182+
public void commence(HttpServletRequest request,
183+
HttpServletResponse response,
184+
AuthenticationException authException) throws IOException,
185+
ServletException {
186+
response.setStatus(httpStatus.value());
187+
}
188+
}
189+
}

config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,19 @@
1515
*/
1616
package org.springframework.security.config.annotation.web.configurers;
1717

18+
import java.util.Collections;
19+
20+
import org.springframework.http.MediaType;
1821
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
1922
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
2023
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
2124
import org.springframework.security.web.savedrequest.RequestCache;
2225
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
2326
import org.springframework.security.web.util.AndRequestMatcher;
2427
import org.springframework.security.web.util.AntPathRequestMatcher;
28+
import org.springframework.security.web.util.MediaTypeRequestMatcher;
2529
import org.springframework.security.web.util.NegatedRequestMatcher;
30+
import org.springframework.security.web.util.RequestHeaderRequestMatcher;
2631
import org.springframework.security.web.util.RequestMatcher;
2732
import org.springframework.web.accept.ContentNegotiationStrategy;
2833
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
@@ -116,6 +121,12 @@ private RequestMatcher createDefaultSavedRequestMatcher(H http) {
116121
}
117122
RequestMatcher getRequests = new AntPathRequestMatcher("/**", "GET");
118123
RequestMatcher notFavIcon = new NegatedRequestMatcher(new AntPathRequestMatcher("/**/favicon.ico"));
119-
return new AndRequestMatcher(getRequests,notFavIcon);
124+
125+
MediaTypeRequestMatcher jsonRequest = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.APPLICATION_JSON);
126+
jsonRequest.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
127+
RequestMatcher notJson = new NegatedRequestMatcher(jsonRequest);
128+
129+
RequestMatcher notXRequestedWith = new NegatedRequestMatcher(new RequestHeaderRequestMatcher("X-Requested-With"));
130+
return new AndRequestMatcher(getRequests, notFavIcon, notJson, notXRequestedWith);
120131
}
121-
}
132+
}

config/src/test/groovy/org/springframework/security/config/annotation/web/builders/NamespaceHttpTests.groovy

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,16 +331,21 @@ public class NamespaceHttpTests extends BaseSpringSpec {
331331
// http@pattern is not available (instead see the tests http@request-matcher-ref ant or http@request-matcher-ref regex)
332332

333333
def "http@realm"() {
334-
when:
334+
setup:
335335
loadConfig(RealmConfig)
336+
when:
337+
springSecurityFilterChain.doFilter(request,response,chain)
336338
then:
337-
findFilter(BasicAuthenticationFilter).authenticationEntryPoint.realmName == "RealmConfig"
339+
response.getHeader("WWW-Authenticate") == 'Basic realm="RealmConfig"'
338340
}
339341

340342
@Configuration
341343
static class RealmConfig extends BaseWebConfig {
342344
protected void configure(HttpSecurity http) throws Exception {
343345
http
346+
.authorizeRequests()
347+
.anyRequest().authenticated()
348+
.and()
344349
.httpBasic().realmName("RealmConfig")
345350
}
346351
}

config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.groovy

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ class ExceptionHandlingConfigurerTests extends BaseSpringSpec {
7171
response.status == httpStatus
7272
where:
7373
acceptHeader | httpStatus
74-
MediaType.ALL_VALUE | HttpServletResponse.SC_MOVED_TEMPORARILY
7574
MediaType.APPLICATION_XHTML_XML_VALUE | HttpServletResponse.SC_MOVED_TEMPORARILY
7675
MediaType.IMAGE_GIF_VALUE | HttpServletResponse.SC_MOVED_TEMPORARILY
7776
MediaType.IMAGE_JPEG_VALUE | HttpServletResponse.SC_MOVED_TEMPORARILY
@@ -165,7 +164,7 @@ class ExceptionHandlingConfigurerTests extends BaseSpringSpec {
165164
when:
166165
loadConfig(BasicAuthenticationEntryPointBeforeFormLoginConf)
167166
then:
168-
findFilter(ExceptionTranslationFilter).authenticationEntryPoint.defaultEntryPoint.class == BasicAuthenticationEntryPoint
167+
findFilter(ExceptionTranslationFilter).authenticationEntryPoint.defaultEntryPoint.defaultEntryPoint.class == BasicAuthenticationEntryPoint
169168
}
170169

171170
@EnableWebSecurity

config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.groovy

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,13 @@ class HttpBasicConfigurerTests extends BaseSpringSpec {
4848
}
4949

5050
def "SEC-2198: http.httpBasic() defaults AuthenticationEntryPoint"() {
51-
when:
51+
setup:
5252
loadConfig(DefaultsEntryPointConfig)
53+
when:
54+
springSecurityFilterChain.doFilter(request, response, chain)
5355
then:
54-
findFilter(ExceptionTranslationFilter).authenticationEntryPoint.class == BasicAuthenticationEntryPoint
56+
response.status == 401
57+
response.getHeader("WWW-Authenticate") == 'Basic realm="Realm"'
5558
}
5659

5760
@EnableWebSecurity
@@ -60,6 +63,9 @@ class HttpBasicConfigurerTests extends BaseSpringSpec {
6063
@Override
6164
protected void configure(HttpSecurity http) throws Exception {
6265
http
66+
.authorizeRequests()
67+
.anyRequest().authenticated()
68+
.and()
6369
.httpBasic()
6470
}
6571

config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public class NamespaceHttpBasicTests extends BaseSpringSpec {
5353
springSecurityFilterChain.doFilter(request,response,chain)
5454
then: "unauthorized"
5555
response.status == HttpServletResponse.SC_UNAUTHORIZED
56-
response.getHeader("WWW-Authenticate") == 'Basic realm="Spring Security Application"'
56+
response.getHeader("WWW-Authenticate") == 'Basic realm="Realm"'
5757
when: "login success"
5858
super.setup()
5959
basicLogin()

config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.groovy

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
2828
import org.springframework.security.web.savedrequest.RequestCache
2929
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
3030

31+
import spock.lang.Unroll;
32+
3133
/**
3234
*
3335
* @author Rob Winch
@@ -87,6 +89,77 @@ class RequestCacheConfigurerTests extends BaseSpringSpec {
8789
response.redirectedUrl == "/"
8890
}
8991

92+
def "SEC-2321: RequestCache disables application/json"() {
93+
setup:
94+
loadConfig(RequestCacheDefautlsConfig)
95+
request.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE)
96+
request.method = "GET"
97+
request.servletPath = "/messages"
98+
request.requestURI = "/messages"
99+
when: "request application/json"
100+
springSecurityFilterChain.doFilter(request,response,chain)
101+
then: "sent to the login page"
102+
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
103+
response.redirectedUrl == "http://localhost/login"
104+
when: "authenticate successfully"
105+
super.setupWeb(request.session)
106+
request.servletPath = "/login"
107+
request.setParameter("username","user")
108+
request.setParameter("password","password")
109+
request.method = "POST"
110+
springSecurityFilterChain.doFilter(request,response,chain)
111+
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"
112+
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
113+
response.redirectedUrl == "/"
114+
}
115+
116+
def "SEC-2321: RequestCache disables X-Requested-With"() {
117+
setup:
118+
loadConfig(RequestCacheDefautlsConfig)
119+
request.addHeader("X-Requested-With", "XMLHttpRequest")
120+
request.method = "GET"
121+
request.servletPath = "/messages"
122+
request.requestURI = "/messages"
123+
when: "request X-Requested-With"
124+
springSecurityFilterChain.doFilter(request,response,chain)
125+
then: "sent to the login page"
126+
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
127+
response.redirectedUrl == "http://localhost/login"
128+
when: "authenticate successfully"
129+
super.setupWeb(request.session)
130+
request.servletPath = "/login"
131+
request.setParameter("username","user")
132+
request.setParameter("password","password")
133+
request.method = "POST"
134+
springSecurityFilterChain.doFilter(request,response,chain)
135+
then: "sent to default URL since it was X-Requested-With"
136+
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
137+
response.redirectedUrl == "/"
138+
}
139+
140+
@Unroll
141+
def "RequestCache saves Accept: #accept"() {
142+
setup:
143+
loadConfig(RequestCacheDefautlsConfig)
144+
request.addHeader("Accept", accept)
145+
request.method = "GET"
146+
request.servletPath = "/messages"
147+
request.requestURI = "/messages"
148+
when: "request content type"
149+
springSecurityFilterChain.doFilter(request,response,chain)
150+
super.setupWeb(request.session)
151+
request.servletPath = "/login"
152+
request.setParameter("username","user")
153+
request.setParameter("password","password")
154+
request.method = "POST"
155+
springSecurityFilterChain.doFilter(request,response,chain)
156+
then: "sent to saved URL"
157+
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
158+
response.redirectedUrl == "http://localhost/messages"
159+
where:
160+
accept << [MediaType.ALL_VALUE, MediaType.TEXT_HTML, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"]
161+
}
162+
90163
@Configuration
91164
@EnableWebSecurity
92165
static class RequestCacheDefautlsConfig extends WebSecurityConfigurerAdapter {

samples/hellojs-jc/build.gradle

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
apply from: WAR_SAMPLE_GRADLE
2+
3+
dependencies {
4+
5+
providedCompile "javax.servlet:javax.servlet-api:3.0.1",
6+
'javax.servlet.jsp:jsp-api:2.1'
7+
8+
compile project(":spring-security-config"),
9+
project(":spring-security-samples-messages-jc"),
10+
project(":spring-security-core"),
11+
project(":spring-security-web"),
12+
"org.springframework:spring-webmvc:$springVersion",
13+
"org.springframework:spring-jdbc:$springVersion",
14+
"org.slf4j:slf4j-api:$slf4jVersion",
15+
"org.slf4j:log4j-over-slf4j:$slf4jVersion",
16+
"org.slf4j:jul-to-slf4j:$slf4jVersion",
17+
"org.slf4j:jcl-over-slf4j:$slf4jVersion",
18+
"javax.servlet:jstl:1.2",
19+
"javax.validation:validation-api:1.0.0.GA",
20+
"org.hibernate:hibernate-validator:4.2.0.Final",
21+
"com.fasterxml.jackson.core:jackson-databind:2.2.1"
22+
23+
runtime "opensymphony:sitemesh:2.4.2",
24+
'cglib:cglib-nodep:2.2.2',
25+
'ch.qos.logback:logback-classic:0.9.30'
26+
}

0 commit comments

Comments
 (0)