Skip to content
This repository was archived by the owner on May 31, 2022. It is now read-only.
This repository was archived by the owner on May 31, 2022. It is now read-only.

CORS Support Not Working with Spring Boot 1.4.2 + Oauth2 #938

@pluttrell

Description

@pluttrell

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() {
  }
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions