Allow OPTIONS HTTP Method for oauth/token request

42,703

Solution 1

@EnableAuthorizationServer is adding http security configuration for endpoints like /oauth/token, /oauth/token_key etc at order 0. So what you should do is to define a http security rule for /oauth/token endpoint only for the OPTIONS http method which is at a higher order.

Something like this:

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

Solution 2

I was using the solution proposed by idursun. The OPTION call started to work, but still had problems with Access-Control-Allow-Origin.

This filter implementation definitively worked for me:

Standalone Spring OAuth2 JWT Authorization Server + CORS

Solution 3

I just add

@Order(Ordered.HIGHEST_PRECEDENCE)

in

public class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter {....}

and config the support of spring

@Bean
public CorsConfigurationSource corsConfigurationSource() {
  CorsConfiguration configuration = new CorsConfiguration();
  configuration.setAllowedOrigins(Arrays.asList("*"));
  configuration.setAllowedMethods(Arrays.asList("*"));
  configuration.setAllowedHeaders(Arrays.asList("*"));

  UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  source.registerCorsConfiguration("/**", configuration);
  return source;
}

Worked for me.

Solution 4

Same problem with Spring-Boot 1.4.7.RELEASE

My WebSecurityConfigurerAdapter was using SecurityProperties.ACCESS_OVERRIDE_ORDER so, selected answer did not work.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class AuthServerSecurityConfig extends WebSecurityConfigurerAdapter 

Thus, I added the following filter configuration with preceding order:

  @Bean
  public FilterRegistrationBean corsFilter() {
    FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(corsConfigurationSource()));
    bean.setOrder(SecurityProperties.DEFAULT_FILTER_ORDER);
    return bean;
  }

  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");
    source.registerCorsConfiguration("/**", config);
    return source;
  }

and it got the job done.

Note: equivalent result can be achieved with a javax.servlet.Filter bean with @Order(SecurityProperties.DEFAULT_FILTER_ORDER) annotation as below:

@Component
@Order(SecurityProperties.DEFAULT_FILTER_ORDER)
public class CorsFilter implements Filter {

  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    final HttpServletResponse response = (HttpServletResponse) res;

    response.setHeader("Access-Control-Allow-Origin"  , "*"                               );
    response.setHeader("Access-Control-Allow-Methods" , "POST, PUT, GET, OPTIONS, DELETE" );
    response.setHeader("Access-Control-Allow-Headers" , "Authorization, Content-Type"     );
    response.setHeader("Access-Control-Max-Age"       , "3600"                            );

    if("OPTIONS".equalsIgnoreCase(((HttpServletRequest) req).getMethod())) {
      response.setStatus(HttpServletResponse.SC_OK);
    }
    else {
      chain.doFilter(req, res);
    }
  }
  // ...
}

Solution 5

The following works for Spring Boot 2. It does not pick up other CORS configurations otherwise.

@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    // this is a Spring ConfigurationProperty use any way to get the CORS values
    @Autowired
    private CorsProperties corsProperties;

    // other things
    //...

    @Override
    public void configure(
            AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .tokenStore(tokenStore())
                .authenticationManager(authenticationManager);
        if (corsProperties.getAllowedOrigins() != null) {
            Map<String, CorsConfiguration> corsConfigMap = new HashMap<>();
            Arrays.asList(corsProperties.getAllowedOrigins().split(",")).stream()
                    .filter(StringUtils::isNotBlank).forEach(s -> {
                CorsConfiguration config = new CorsConfiguration();
                config.setAllowCredentials(true);
                config.addAllowedOrigin(s.trim());
                if (corsProperties.getAllowedMethods() != null) {
                    config.setAllowedMethods(Arrays.asList(corsProperties.getAllowedMethods().split(",")));
                }
                if (corsProperties.getAllowedHeaders() != null) {
                    config.setAllowedHeaders(Arrays.asList(corsProperties.getAllowedHeaders().split(",")));
                }
                // here the /oauth/token is used
                corsConfigMap.put("/oauth/token", config);
            });
            endpoints.getFrameworkEndpointHandlerMapping()
                    .setCorsConfigurations(corsConfigMap);
        }
    }


}

And in addition the already mentioned allowance of the OPTIONS request:

@Order(-1)
@Configuration
public class MyWebSecurity extends WebSecurityConfigurerAdapter {
   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http
          authorizeRequests()
            .antMatchers("/**/oauth/token").permitAll()
            .and().httpBasic().realmName(securityRealm)
            // would throw a 403 otherwise
            .and().csrf().disable()
            // optional, but with a token a sesion is not needed anymore
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
   }
}
Share:
42,703
Wojtek Wysocki
Author by

Wojtek Wysocki

Updated on August 28, 2020

Comments

  • Wojtek Wysocki
    Wojtek Wysocki over 3 years

    I'm trying to enable oauth2 token fetching for my angular application. My configuration is working fine (authentication is working correctly for all requests, token fetching is working fine as well) but there is one problem.

    CORS requests require that before GET an OPTIONS request is sent to the server. To make it worse, that request does not contain any authentication headers. I would like to have this request always returning with 200 status without any authentication done on the server. Is it possible? Maybe I'm missing something

    my spring security config:

    @Configuration
    @EnableWebSecurity
    @EnableAuthorizationServer
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
    
    @Inject
    private UserService userService;
    
    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }
    
    @Bean
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        return defaultTokenServices;
    }
    
    @Bean
    public WebResponseExceptionTranslator webResponseExceptionTranslator() {
        return new DefaultWebResponseExceptionTranslator() {
    
            @Override
            public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
                ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
                OAuth2Exception body = responseEntity.getBody();
                HttpHeaders headers = new HttpHeaders();
                headers.setAll(responseEntity.getHeaders().toSingleValueMap());
                headers.set("Access-Control-Allow-Origin", "*");
                headers.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
                headers.set("Access-Control-Max-Age", "3600");
                headers.set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
                return new ResponseEntity<>(body, headers, responseEntity.getStatusCode());
            }
    
        };
    }
    
    @Bean
    public AuthorizationServerConfigurer authorizationServerConfigurer() {
        return new AuthorizationServerConfigurer() {
    
            @Override
            public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
                OAuth2AuthenticationEntryPoint oAuth2AuthenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
                oAuth2AuthenticationEntryPoint.setExceptionTranslator(webResponseExceptionTranslator());
                security.authenticationEntryPoint(oAuth2AuthenticationEntryPoint);
            }
    
            @Override
            public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
                clients.inMemory()
                        .withClient("secret-client")
                        .secret("secret")
                        .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
                        .authorities("ROLE_LOGIN")
                        .scopes("read", "write", "trust")
                        .accessTokenValiditySeconds(60 * 60 * 12); // 12 hours
            }
    
            @Override
            public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
                endpoints.tokenServices(tokenServices());
                endpoints.authenticationManager(authenticationManager());
            }
        };
    }
    
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return new AuthenticationManager() {
    
            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                log.warn("FIX ME: REMOVE AFTER DEBUG!!!!!!!!!!!!");                
                log.debug("authenticate: " + authentication.getPrincipal() + ":" + authentication.getCredentials());
                final Collection<GrantedAuthority> authorities = new ArrayList<>();
                WomarUser user = userService.findUser(authentication.getPrincipal().toString(), authentication.getCredentials().toString());
                for (UserRole userRole : user.getRoles()) {
                    authorities.add(new SimpleGrantedAuthority(userRole.getName()));
    
                }
                return new UsernamePasswordAuthenticationToken(user.getLogin(), user.getPassword(), authorities);
            }
    
        };
    }
    
    @Bean
    public OAuth2AuthenticationManager auth2AuthenticationManager() {
        OAuth2AuthenticationManager oAuth2AuthenticationManager = new OAuth2AuthenticationManager();
        oAuth2AuthenticationManager.setTokenServices(tokenServices());
        return oAuth2AuthenticationManager;
    }
    
    @Bean
    public OAuth2AuthenticationProcessingFilter auth2AuthenticationProcessingFilter() throws Exception {
        OAuth2AuthenticationProcessingFilter oAuth2AuthenticationProcessingFilter = new OAuth2AuthenticationProcessingFilter();
        oAuth2AuthenticationProcessingFilter.setAuthenticationManager(auth2AuthenticationManager());
        return oAuth2AuthenticationProcessingFilter;
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
        OAuth2AuthenticationEntryPoint oAuth2AuthenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
        oAuth2AuthenticationEntryPoint.setRealmName("realmName");
        oAuth2AuthenticationEntryPoint.setTypeName("Basic");
        oAuth2AuthenticationEntryPoint.setExceptionTranslator(webResponseExceptionTranslator());
        http
                .antMatcher("/**").httpBasic()
                .authenticationEntryPoint(oAuth2AuthenticationEntryPoint)
                .and().addFilterBefore(auth2AuthenticationProcessingFilter(), BasicAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/rest/womar/admin/**").hasRole("ADMIN")
                .antMatchers("/rest/womar/**").hasRole("USER");
    }
    

    }

    angular request:

    var config = {
    params: {
        grant_type: 'password',
        username: login,
        password: password
    
    },
    headers: {
        Authorization: 'Basic ' + Base64.encode('secret-client' + ':' + 'secret')
    }
    };
    $http.get("http://localhost:8080/oauth/token", config)
        .success(function(data, status) {
            $log.log('success');
            $log.log(data);
            $log.log(status);
        })
        .error(function(data, status) {
            $log.log('error');
            $log.log(data);
            $log.log(status);
        });
    
  • idursun
    idursun over 9 years
    Hi, You can take a look at my github repository where I do some experimental work with angular and spring. My token handling is working ok there. github.com/idursun/spring-and-angular
  • Michael K.
    Michael K. almost 9 years
    I had the same Problem but this solution did just generate another problem. Solution to my problem was to stop processing the Filter Chain of the request so the AuthServer doesn't see the OPTIONS preflight request at all! Here is you can see my Solution: stackoverflow.com/questions/30632200/…
  • Andreas Lundgren
    Andreas Lundgren over 7 years
    Agree, also that proposal above also made my custom AuthenticationProvider not getting registered. The CORS-filter needs to get implemented sooner or later anyway, may just as well handle the OPTIONS issue there.
  • N.A
    N.A almost 7 years
    Hi having same problem.even this solution didnt authorize/allow OPTIONS request to oauth/token
  • OlegMax
    OlegMax over 6 years
    Looks like a correct way to intercept OPTIONS handling is by requestMatchers method of HttpSecurity. This way we won't affect other requests unlike filter in authorizeRequests(): protected void configure(HttpSecurity http) throws Exception { http .requestMatchers() .antMatchers(HttpMethod.OPTIONS, "/oauth/token") .and().cors() .and().csrf().disable(); }
  • Maksim Gumerov
    Maksim Gumerov over 5 years
    Funny thing is, AuthorizationServerSecurityConfigurer.configure(Authorizatio‌​nServerSecurityConfi‌​gurer) is supposed to allow us to configure access to that endpoint, however it does not distinguish between HTTP methods.
  • AntonioOtero
    AntonioOtero about 4 years
    This solution worked for me. I added corsFilter() and corsConfigurationSource() beans. Just for fun, I tried with the CorsFilter class and that didn't work. Thanks a lot!