Spring security with Oauth2 or Http-Basic authentication for the same resource

35,264

Solution 1

I managed to get this work based on the hints by Michael Ressler's answer but with some tweaks.

My goal was to allow both Basic Auth and Oauth on the same resource endpoints, e.g., /leafcase/123. I was trapped for quite some time due to the ordering of the filterChains (can be inspected in FilterChainProxy.filterChains); the default order is as follows:

  • Oauth authentication server (if enabled in the same project)'s filterChains. default order 0 (see AuthorizationServerSecurityConfiguration)
  • Oauth resource server's filterChains. default order 3 (see ResourceServerConfiguration). It has a request matcher logic that matches anything other than the Oauth authentication endpoints (e.g., /oauth/token, /oauth/authorize, etc. See ResourceServerConfiguration$NotOauthRequestMatcher.matches()).
  • The filterChains that corresponds to config(HttpSecurity http) - default order 100, see WebSecurityConfigurerAdapter.

Since resource server's filterChains ranks higher than the one by WebSecurityConfigurerAdapter configured filterchain, and the former matches practically every resource endpoint, then Oauth resource server logic always kick in for any request to resource endpoints (even if the request uses the Authorization:Basic header). The error you would get is:

{
    "error": "unauthorized",
    "error_description": "Full authentication is required to access this resource"
}

I made 2 changes to get this work:

Firstly, order the WebSecurityConfigurerAdapter higher than the resource server (order 2 is higher than order 3).

@Configuration
@Order(2)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

Secondly, let configure(HttpSecurity) use a customer RequestMatcher that only matches "Authorization: Basic".

@Override
protected void configure(HttpSecurity http) throws Exception {

    http
        .anonymous().disable()
        .requestMatcher(new BasicRequestMatcher())
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .httpBasic()
             .authenticationEntryPoint(oAuth2AuthenticationEntryPoint())
            .and()
        // ... other stuff
 }
 ...
 private static class BasicRequestMatcher implements RequestMatcher {
    @Override
    public boolean matches(HttpServletRequest request) {
        String auth = request.getHeader("Authorization");
        return (auth != null && auth.startsWith("Basic"));
    }
 }

As a result it matches and handles a Basic Auth resource request before the resource server's filterChain has a chance to match it. It also ONLY handles Authorizaiton:Basic resource request, thus any requests with Authorization:Bearer will fall through, and then handled by resource server's filterChain (i.e., Oauth's filter kicks in). Also, it ranks lower than the AuthenticationServer (in case AuthenticationServer is enabled on the same project), so it doesn't prevent AuthenticaitonServer's filterchain from handling the request to /oauth/token, etc.

Solution 2

This may be close to what you were looking for:

@Override
public void configure(HttpSecurity http) throws Exception {
    http.requestMatcher(new OAuthRequestedMatcher())
    .authorizeRequests()
        .anyRequest().authenticated();
}

private static class OAuthRequestedMatcher implements RequestMatcher {
    @Override
    public boolean matches(HttpServletRequest request) {
        String auth = request.getHeader("Authorization");
        // Determine if the client request contained an OAuth Authorization
        return (auth != null) && auth.startsWith("Bearer");
    }
}

The only thing this doesn't provide is a way to "fall back" if the authentication isn't successful.

To me, this approach makes sense. If a User is directly providing authentication to the request via Basic auth, then OAuth is not necessary. If the Client is the one acting, then we need this filter to step in and make sure the request is properly authenticated.

Solution 3

And why not doing this the other way round? Just bypass resource server if there is no token attached, then fallback to normal security filter chain. This is by the way what resource server filter is stopping on.

@Configuration
@EnableResourceServer
class ResourceServerConfig : ResourceServerConfigurerAdapter() {


    @Throws(Exception::class)
    override fun configure(resources: ResourceServerSecurityConfigurer) {
        resources.resourceId("aaa")
    }

    /**
     * Resources exposed via oauth. As we are providing also local user interface they are also accessible from within.
     */
    @Throws(Exception::class)
    override fun configure(http: HttpSecurity) {
        http.requestMatcher(BearerAuthorizationHeaderMatcher())
                .authorizeRequests()
                .anyRequest()
                .authenticated()
    }

    private class BearerAuthorizationHeaderMatcher : RequestMatcher {
        override fun matches(request: HttpServletRequest): Boolean {
            val auth = request.getHeader("Authorization")
            return auth != null && auth.startsWith("Bearer")
        }
    }

}

Solution 4

The solution @kca2ply provided works very well. I noticed the browser wasn't issuing a challenge so I tweaked the code a little to the following:

@Configuration
@Order(2)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {

    // @formatter:off
    http.anonymous().disable()
      .requestMatcher(request -> {
          String auth = request.getHeader(HttpHeaders.AUTHORIZATION);
          return (auth != null && auth.startsWith("Basic"));
      })
      .antMatcher("/**")
      .authorizeRequests().anyRequest().authenticated()
    .and()
      .httpBasic();
    // @formatter:on
  }

  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
    .withUser("user").password("password").roles("USER");
  }
}

Using both requestMatcher() and antMatcher() made things work perfectly. Browsers and HTTP clients will now challenge for basic creds first if not provided already. If no credentials are provided, it falls through to OAuth2.

Solution 5

You can add a BasicAuthenticationFilter to the security filter chain to get OAuth2 OR Basic authentication security on a protected resource. Example config is below...

@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManagerBean;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        final String[] userEndpoints = {
            "/v1/api/airline"
        };

        final String[] adminEndpoints = {
                "/v1/api/jobs**"
            };

        http
            .requestMatchers()
                .antMatchers(userEndpoints)
                .antMatchers(adminEndpoints)
                .antMatchers("/secure/**")
                .and()
            .authorizeRequests()
                .antMatchers("/secure/**").authenticated()
                .antMatchers(userEndpoints).hasRole("USER")
                .antMatchers(adminEndpoints).hasRole("ADMIN");

        // @formatter:on
        http.addFilterBefore(new BasicAuthenticationFilter(authenticationManagerBean),
                UsernamePasswordAuthenticationFilter.class);
    }

}
Share:
35,264

Related videos on Youtube

user3613594
Author by

user3613594

Updated on July 09, 2022

Comments

  • user3613594
    user3613594 almost 2 years

    I'm attempting to implement an API with resources that are protected by either Oauth2 OR Http-Basic authentication.

    When I load the WebSecurityConfigurerAdapter which applies http-basic authentication to the resource first, Oauth2 token authentication is not accepted. And vice-versa.

    Example configurations: This applies http-basic authentication to all /user/** resources

    @Configuration
    @EnableWebMvcSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        private LoginApi loginApi;
    
        @Autowired
        public void setLoginApi(LoginApi loginApi) {
            this.loginApi = loginApi;
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.authenticationProvider(new PortalUserAuthenticationProvider(loginApi));
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .antMatchers("/users/**").authenticated()
                    .and()
                .httpBasic();
        }
    
        @Override
        @Bean
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    }
    

    This applies oauth token protection to the /user/** resource

    @Configuration
    @EnableResourceServer
    protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                .requestMatchers().antMatchers("/users/**")
            .and()
                .authorizeRequests()
                    .antMatchers("/users/**").access("#oauth2.clientHasRole('ROLE_CLIENT') and #oauth2.hasScope('read')");
        }
    }
    

    I'm sure there is some piece of magic code I'm missing which tells spring to attempt both if the first has failed?

    Any assistance would be most appreciated.

  • user3613594
    user3613594 almost 10 years
    Hmm... Any way around this limitation? Or is it just "the way it works"?
  • raonirenosto
    raonirenosto almost 10 years
    Maybe you could achieve it using filters, but I guess it would be overcomplicated. How about using different endpoints? Lets say: /basic/users and /oauth/users.
  • user3613594
    user3613594 almost 10 years
    We are considering exactly that now. Will keep digging and see if I can come up with a way to make it work. Thanks for your feedback and suggestions!
  • raonirenosto
    raonirenosto almost 10 years
    Glad to help. If this answer was useful to you somehow, please accept the answer, thanks.
  • user3613594
    user3613594 over 9 years
    Thanks for the tip! I'm going to try to work on this again soon, and will post back if I'm able to resolve it.
  • jax
    jax almost 7 years
    Where is oAuth2AuthenticationEntryPoint() defined?
  • kboom
    kboom over 6 years
    Anyone knows how to do this properly for session-based authentication? (form login)
  • Max
    Max over 5 years
    BasicAuthenticationFilter would require an AuthenticationManager that could handle UserPasswordAuthentication and there is no such registered by the ResourceServerSecurityConfigurer. It gets a lot more code to make both oauth2 and basic auth work in a single filter chain.
  • AntonioOtero
    AntonioOtero about 4 years
    Answer for @jax .authenticationEntryPoint(new OAuth2AuthenticationEntryPoint()) where OAuth2AuthenticationEntryPoint comes from import org.springframework.security.oauth2.provider.error.OAuth2Aut‌​henticationEntryPoin‌​t;
  • Dave
    Dave over 3 years
    After 5 hours trying to resolve this problem, this solution worked like a treat ;-) I shall be toasting your health with a glass of the finest cider tonight