-
Notifications
You must be signed in to change notification settings - Fork 4k
CORS Support Not Working with Spring Boot 1.4.2 + Oauth2 #938
Description
Using the latest version of Spring Boot (1.4.2.RELEASE) and enabling Oauth2 using @EnableAuthorizationServer
, I can't get CORS support to work using either the @CrossOrigin
or the global support via Spring's CorsFilter
as described on the spring.io/blog.
The full example code is in this GitHub repo and can be run with gradle bootRun
.
@SpringBootApplication
public class AuthServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServiceApplication.class, args);
}
}
@Configuration
@EnableAuthorizationServer
class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client1").secret("password")
.scopes("foo-scope")
.autoApprove(true)
.authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code")
.accessTokenValiditySeconds(600)
.refreshTokenValiditySeconds(1800);
}
@Autowired
@Qualifier("authenticationManagerBean")
@SuppressWarnings("SpringJavaAutowiringInspection")
private AuthenticationManager authenticationManager;
@Bean
public TokenStore buildTokenStore() {
return new JwtTokenStore(buildJwtTokenConverter());
}
@Bean
public TokenEnhancer buildExtraFieldsTokenEnhancer() {
return (accessToken, authentication) -> {
DefaultOAuth2AccessToken defaultOAuth2AccessToken = (DefaultOAuth2AccessToken) accessToken;
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("user-uuid", UUID.randomUUID());
defaultOAuth2AccessToken.setAdditionalInformation(additionalInfo);
return defaultOAuth2AccessToken;
};
}
@Bean
protected JwtAccessTokenConverter buildJwtTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("secret");
return converter;
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(buildTokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(buildExtraFieldsTokenEnhancer(), buildJwtTokenConverter()));
endpoints.tokenStore(buildTokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
}
@Configuration
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers("/**").authenticated()
.and()
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user1").password("password").authorities("right1");
}
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}
When I use Postman or Httpie it responds perfectly, for example:
http --auth-type basic --auth client1:password --form POST http://localhost:8080/oauth/token grant_type="password" username="user1" password="password"
HTTP/1.1 200
Cache-Control: no-store
Content-Type: application/json;charset=UTF-8
Date: Fri, 23 Dec 2016 03:15:25 GMT
Pragma: no-cache
Transfer-Encoding: chunked
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyLXV1aWQiOiI3YzNmNTY3My1jZWRlLTRmYjMtOGUzZC05YjdlMGZhODA0OGYiLCJ1c2VyX25hbWUiOiJ1c2VyMSIsInNjb3BlIjpbImZvby1zY29wZSJdLCJleHAiOjE0ODI0NjM1MjUsImF1dGhvcml0aWVzIjpbInJpZ2h0MSJdLCJqdGkiOiI4OTJkM2JhZC01YTFmLTQ0NTUtOWNhNS1jNjM2MWUwMTg2MjYiLCJjbGllbnRfaWQiOiJjbGllbnQxIn0.ofNkDHhfpFwdW5qcgxoFzpoY9wzvs7vwwH2ULxBm4Bc",
"expires_in": 599,
"jti": "892d3bad-5a1f-4455-9ca5-c6361e018626",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyLXV1aWQiOiI3YzNmNTY3My1jZWRlLTRmYjMtOGUzZC05YjdlMGZhODA0OGYiLCJ1c2VyX25hbWUiOiJ1c2VyMSIsInNjb3BlIjpbImZvby1zY29wZSJdLCJhdGkiOiI4OTJkM2JhZC01YTFmLTQ0NTUtOWNhNS1jNjM2MWUwMTg2MjYiLCJleHAiOjE0ODI0NjQ3MjUsImF1dGhvcml0aWVzIjpbInJpZ2h0MSJdLCJqdGkiOiJkNjI2NmNkNS1mZjdkLTRjYzItOWJjMS1jODU2MmEwOTY2ZGIiLCJjbGllbnRfaWQiOiJjbGllbnQxIn0.H_mKswPz8zuEoQajiO4FvrnFXJoVZttqXFG3sP58N4I",
"scope": "foo-scope",
"token_type": "bearer",
"user-uuid": "7c3f5673-cede-4fb3-8e3d-9b7e0fa8048f"
}
But when I use JavaScript in Chrome it fails with a:
XMLHttpRequest cannot load http://localhost:8080/oauth/token. Response for preflight has invalid HTTP status code 401
Here is the full request from Chome:
OPTIONS /oauth/token HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Access-Control-Request-Method: POST
Origin: http://localhost:9000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36
Access-Control-Request-Headers: authorization, cache-control
Accept: */*
Referer: http://localhost:9000/
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
And the full response Chrome receives back:
HTTP/1.1 401
WWW-Authenticate: Basic realm="oauth2/client"
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Access-Control-Allow-Origin: http://localhost:9000
Vary: Origin
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: authorization, cache-control
Access-Control-Allow-Credentials: true
Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
Content-Length: 0
Date: Fri, 23 Dec 2016 03:09:45 GMT
For the JavaScript in Chrome test, I'm simply running Spring Boot on a separate port which hosts this index.html
:
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript">
var form = new FormData();
form.append("grant_type", "password");
form.append("username", "user1");
form.append("password", "password");
var settings = {
"async": true,
"crossDomain": true,
"url": "http://localhost:8080/oauth/token",
"method": "POST",
"headers": {
"authorization": "Basic Y2xpZW50MTpwYXNzd29yZA==",
"cache-control": "no-cache"
},
"processData": false,
"contentType": false,
"mimeType": "multipart/form-data",
"data": form
}
$.ajax(settings).done(function (response) {
console.log(response);
});
</script>
</head>
<body>
Check the console.
</body>
</html>
Note that if I add the following custom filter as described in this stackoverflow response, the JavaScript in Chrome source does work. But this is a brute force filter with side effects and I'd much prefer to use Spring's built in CORS support via @CrossOrigin
or the Spring CorsFilter
.
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
class SimpleCorsFilter implements Filter {
public SimpleCorsFilter() {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with, authorization, cache-control");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}