27

So I have the following Authorization Server condensed from this example from Dave Syer

@SpringBootApplication
public class AuthserverApplication {

    public static void main(String[] args) {
            SpringApplication.run(AuthserverApplication.class, args);
    }

    /* added later
    @Configuration
    @Order(Ordered.HIGHEST_PRECEDENCE)
    protected static class MyWebSecurity extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http //.csrf().disable() 
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll();
       }
    }*/

    @Configuration
    @EnableAuthorizationServer
    protected static class OAuth2AuthorizationConfig extends
                    AuthorizationServerConfigurerAdapter {

            @Autowired
            private AuthenticationManager authenticationManager;

            @Bean
            public JwtAccessTokenConverter jwtAccessTokenConverter() {
                    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
                    KeyPair keyPair = new KeyStoreKeyFactory(
                                    new ClassPathResource("keystore.jks"), "foobar".toCharArray())
                                    .getKeyPair("test");
                    converter.setKeyPair(keyPair);
                    return converter;
            }

            @Override
            public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
                    clients.inMemory()
                                    .withClient("acme")
                                    //.secret("acmesecret")
                                    .authorizedGrantTypes(//"authorization_code", "refresh_token",
                                                    "password").scopes("openid");
            }

            @Override
            public void configure(AuthorizationServerEndpointsConfigurer endpoints)
                            throws Exception {
                    endpoints.authenticationManager(authenticationManager).accessTokenConverter(
                                    jwtAccessTokenConverter());
            }

            @Override
            public void configure(AuthorizationServerSecurityConfigurer oauthServer)
                            throws Exception {
                    oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess(
                                    "isAuthenticated()");
            }
    }
}

when I run it and test it with curl

curl acme@localhost:8110/oauth/token -d grant_type=password -d client_id=acme -d username=user -d password=password

I get a JWT as respons, but as soon as I try to access the AuthServer from my Frontend (Angular JS on a different port) I get CORS error. Not becauce of missing Headers, but because the OPTION request is rejected and is missing the credentials.

Request URL:http://localhost:8110/oauth/token
Request Method:OPTIONS
Status Code:401 Unauthorized
WWW-Authenticate:Bearer realm="oauth", error="unauthorized", error_description="Full authentication is required to access this resource"

I already knew that I have to add a CorsFilter and additionally found this post where I used the the snippet for the first Answer to let the OPTIONS request access /oauth/token without credentials:

@Order(-1)
public class MyWebSecurity extends WebSecurityConfigurerAdapter {
   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http
          .authorizeRequests()
          .antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll();
   }
}

After that I got with curl the following error:

{"timestamp":1433370068120,"status":403,"error":"Forbidden","message":"Expected CSRF token not found. Has your session expired?","path":"/oauth/token"}

So to make it simple I just added http.csrf().disable() to the configure method of MyWebSecurity class, which solves the Problem with the OPTION request, but therefore the POST request isn't working anymore and I get There is no client authentication. Try adding an appropriate authentication filter. (also with curl).

I tried to find out if I have to somehow connect MyWebSecurity class and the AuthServer, but without any luck. The original example (link in the beginning) injects as well the authenticationManager, but this changed nothing for me.

Community
  • 1
  • 1
Michael K.
  • 2,392
  • 4
  • 22
  • 35

6 Answers6

102

Found the reason for my Problem!

I just needed to end the filterchain and return the result immediatly if a OPTIONS request is processed by the CorsFilter!

SimpleCorsFilter.java

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public 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");

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

After that I could ignore the OPTIONS preflight request in my AuthServer =D

So the Server works as in the snipped above and you can ignore the block comment with MyWebSecurity class in the beginning.

Lorenzo Polidori
  • 10,332
  • 10
  • 51
  • 60
Michael K.
  • 2,392
  • 4
  • 22
  • 35
  • 2
    As an alternative, you can use the `CorsFilter` provided with Spring Framework 4.2+ and Spring Boot 1.3+, see my [answer here](http://stackoverflow.com/a/31748398/1092077) for more details. – Sébastien Deleuze Nov 29 '15 at 15:13
  • 2
    @SébastienDeleuze Spring's CorsFilter seems to be executed after the FilterChain for some reason and still results in unauthorized :( – froginvasion Dec 21 '15 at 12:38
  • 2
    Did you try to change the order of the filters as described [here](http://stackoverflow.com/a/26147788/1092077)? – Sébastien Deleuze Dec 28 '15 at 08:46
  • 1
    This spring doc helped me: [Extend spring web CORSFilter](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cors.html#_filter_based_cors_support) – Nagaraj Tantri Sep 01 '16 at 09:27
  • @SébastienDeleuze how can do. if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); } in CorsFilter in spring 4.2+ – ali akbar azizkhani Oct 19 '16 at 17:32
  • 2
    @SébastienDeleuze I'd really like to use the `@CrossOrigin` annotation, but am ok with the Spring CorsFilter as a backup. The `@CrossOrigin` works perfectly with my `@ResoruceService` but neither option work with my `@EnableAuthorizationServer`. I always get the 401 response to the Chrome preflight OPTIONS requests. A custom filter as is described in this answer does work. How do we get `@CrossOrigin` annotation and/or the Spring CorsFilter to actually work with `@EnableAuthorizationServer`. I've tried all of the suggesting in your responses here and elsewhere that I can find to no avail. – peterl Dec 22 '16 at 09:39
  • @Michael can you please help me on this question https://stackoverflow.com/questions/48806722/can-i-append-some-information-in-oauth-check-token-endpoint-and-retrieve-it-at-a –  Feb 25 '18 at 19:16
  • I searched across the Stack, searching far and wide, and found that this is the only solution that works properly. – alextsil Jul 18 '18 at 08:55
  • @peterl I have the same problem, I have a frontend project with Angular and when I tried to call any service like "Resource Project" ) from springboot, the browser always send "OPTIONS" to backend instead of "GET" or "POST" if you find something about this, please share – Gustavo Tufiño Fernández Jan 23 '19 at 21:25
  • 1
    By setting the filter to HIGHEST_PRIORITY and not allowing the filter chain to continue for OPTIONS requests, you might create an odd behavior because other filters would not run. – Shlomi Uziel Apr 30 '19 at 22:00
  • @Michael K. hi, thanks for solution. but now I am getting status 400 missing grant_type with same solution. It works well in postman but while requesting from angular, I am getting 400 error. I am passing grant_type = password with username and password though. – Chirag Shah Jul 29 '19 at 05:59
  • @ChiragShah Sorry, but thats too long ago! :( did you set the presedence? ```@Order(Ordered.HIGHEST_PRECEDENCE)``` Maybe also try the cors filter @gervasio-amy suggested below – Michael K. Jul 29 '19 at 16:51
26

I found a solution using the solution for the question. But I have another way to describe the solution:

@Configuration
public class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter {
      ....
      @Override
      public void configure(WebSecurity web) throws Exception {
        web.ignoring()
          .antMatchers(HttpMethod.OPTIONS);
      }
      ...
}
Cyril
  • 537
  • 4
  • 11
11

I came across similar issue using following

  • Backend Spring Boot 1.5.8.RELEASE
  • Spring OAuth2 Spring OAuth 2.2.0.RELEASE w
  • Vuejs app using axios ajax request library

With postman everything works! When I started making request from Vuejs app then I got the following errors

OPTIONS http://localhost:8080/springboot/oauth/token 401 ()

and

XMLHttpRequest cannot load http://localhost:8080/springboot/oauth/token. Response for preflight has invalid HTTP status code 401

After reading a bit, I found out that I can instruct my Spring OAuth to ignore the OPTIONS request by overriding configure in my WebSecurityConfigurerAdapter implementation class as follow

@Override
public void configure(WebSecurity web) throws Exception {
   web.ignoring().antMatchers(HttpMethod.OPTIONS);
}

Addition of the above helped but then, I came across the CORS specific error

OPTIONS http://localhost:8080/springboot/oauth/token 403 ()

and

XMLHttpRequest cannot load http://localhost:8080/springboot/oauth/token. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8000' is therefore not allowed access. The response had HTTP status code 403.

And solved the above issue with the help of a CorsConfig as shown below

@Configuration
public class CorsConfig {
    @Bean
    public FilterRegistrationBean corsFilterRegistrationBean() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.applyPermitDefaultValues();
        config.setAllowCredentials(true);
        config.setAllowedOrigins(Arrays.asList("*"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowedMethods(Arrays.asList("*"));
        config.setExposedHeaders(Arrays.asList("content-length"));
        config.setMaxAge(3600L);
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(0);
        return bean;
    }
}

After addition of the above class, it works as expected. Before I go prod I will research consequences of using

web.ignoring().antMatchers(HttpMethod.OPTIONS);

as well as best practices for above Cors configuration. For now * does the job but, definitely not secure for production.

Cyril's answer helped me partially and then I came across the CorsConfig idea in this Github issue.

Raf
  • 7,505
  • 1
  • 42
  • 59
3

well, you're right! that's a solution, and it worked also for me (I had the same issue)

But let me sussgest to use a smarter CORS Filter implementation for Java: http://software.dzhuvinov.com/cors-filter.html

This is very complete solution for Java applications.

Actually, you can see here how your point is resolved.

Gervasio Amy
  • 285
  • 2
  • 8
  • 3
    Spring Framework 4.2+ native `CorsFilter` is probably a better alternative, [see this answer](http://stackoverflow.com/a/31748398/1092077) for more details. – Sébastien Deleuze Nov 29 '15 at 15:17
3

Using Spring Boot 2 here.

I had to do this in my AuthorizationServerConfigurerAdapter

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {

    Map<String, CorsConfiguration> corsConfigMap = new HashMap<>();
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    //TODO: Make configurable
    config.setAllowedOrigins(Collections.singletonList("*"));
    config.setAllowedMethods(Collections.singletonList("*"));
    config.setAllowedHeaders(Collections.singletonList("*"));
    corsConfigMap.put("/oauth/token", config);
    endpoints.getFrameworkEndpointHandlerMapping()
            .setCorsConfigurations(corsConfigMap);

    //additional settings...
}
Sebastiaan van den Broek
  • 5,818
  • 7
  • 40
  • 73
3
I tried different things to solve this issue. I would say that the below was fixed this issue on my side (Using Spring Boot 2)
1-Add the below method to the below method class that extends WebSecurityConfigurerAdapter:
    // CORS settings
     @Override
     public void configure(WebSecurity web) throws Exception {
       web.ignoring()
         .antMatchers(HttpMethod.OPTIONS);
     }
2-Add the below to my class that extends AuthorizationServerConfigurerAdapter
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    // enable cors for "/oauth/token"
    Map<String, CorsConfiguration> corsConfigMap = new HashMap<>();
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
   
    config.setAllowedOrigins(Collections.singletonList("*"));
    config.setAllowedMethods(Collections.singletonList("*"));
    config.setAllowedHeaders(Collections.singletonList("*"));
    corsConfigMap.put("/oauth/token", config);
    endpoints.getFrameworkEndpointHandlerMapping()
            .setCorsConfigurations(corsConfigMap);
    // add the other configuration
}
ayman.mostafa
  • 451
  • 6
  • 5