DaoAuthenticationProvider missing when additionally using OAuth2 for Authentication

Viewed 596

I have detected a behaviour I find confusing, it might be intended, but I could not determine if so.

The DaoAuthenticationprovider does not get created when I use Basic Auth together with specific oauth2 configuration. Without this specific configuration inside of application.yaml everything works. Is it included, I get an internal exception resulting in a 401. I described both cases below.

My question would be: If this is correct behaviour, how do I get a default Basic Auth running without providing a custom AuthenticationProvider etc.? I did not try to achieve this myself, because I do not know how to easily achieve it, and also, I'd rather use default behaviour if possible.

Basic Authentication does not work when

curl --location --request GET 'http://localhost:8080/actuator/info' --header 'Authorization: Basic dXNlcjpwYXNzd29yZA=='

the response is

{"timestamp":"2021-05-25T13:25:30.198+00:00","status":401,"error":"Unauthorized","message":"","path":"/actuator/info"}

inside Spring I get a ProviderNotFoundException:

/actuator/info at position 5 of 11 in additional filter chain; firing Filter: 'BasicAuthenticationFilter'
2021-05-25 15:23:32.727 DEBUG 4216 --- [nio-8080-exec-2] o.s.s.w.a.www.BasicAuthenticationFilter  : Basic Authentication Authorization header found for user 'user'
2021-05-25 15:23:32.730 DEBUG 4216 --- [nio-8080-exec-2] o.s.s.w.a.www.BasicAuthenticationFilter  : Authentication request for failed!
org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:251) ~[spring-security-core-5.3.4.RELEASE.jar:5.3.4.RELEASE]

when debugging I found out that DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); is not called when InitializeUserDetailsBeanManagerConfigurer.java gets configured. Actually it seems InitializeUserDetailsBeanManagerConfigurer is not really called for configuration, I landed instead here GlobalAuthenticationConfigurerAdapter.java.

To make everything work

I only need to remove the following lines from application.yaml:

oauth2:
   resourceserver:
      jwt:
         issuer-uri: https://someissuer
         jwk-set-uri: https://somejwkuri

then I get a 200 from the same curl and this inside spring:

/actuator/info at position 5 of 11 in additional filter chain; firing Filter: 'BasicAuthenticationFilter'
2021-05-25 15:51:21.845 DEBUG 16540 --- [nio-8080-exec-1] o.s.s.w.a.www.BasicAuthenticationFilter  : Basic Authentication Authorization header found for user 'user'
2021-05-25 15:51:21.845 DEBUG 16540 --- [nio-8080-exec-1] o.s.s.authentication.ProviderManager     : Authentication attempt using org.springframework.security.authentication.dao.DaoAuthenticationProvider
2021-05-25 15:51:22.036 DEBUG 16540 --- [nio-8080-exec-1] o.s.s.w.a.www.BasicAuthenticationFilter  : Authentication success: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@ed8ad585: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ENDPOINT_ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ENDPOINT_ADMIN

Code

application.yaml

There is other configuration like flyway, hikari, camunda, but all of these are separate, and do not affect the behaviour I observed.

spring:
   security:
      oauth2:
         resourceserver:
            jwt:
               issuer-uri: https://someissuer
               jwk-set-uri: https://somejwkuri
      user:
         name: user
         password: password
         roles: ENDPOINT_ADMIN

server:
   port: 8080

management:
   endpoints:
      web:
         exposure:
            include: health,info,metrics,prometheus,loggers
         cors:
            allowed-origins: "*"
            allowed-methods: GET,POST
   security:
      enabled: false

logging:
   level:
      org:
         springframework: DEBUG
   pattern:
      console:%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n 

WebSecurityConfigurerAdapter extending class

Its an inner class, because I make use of multiple filter chains, but I excluded the others during test - no impact

public class SecurityConfig {

    @Order(1)
    @Configuration
    public static class BasicAuthSecurityConfig extends WebSecurityConfigurerAdapter {
        // info is excluded on purpose
        private final String[] ENDPOINTS = { "health", "metrics", "prometheus", /* "info" */ }; 
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable().requestMatcher(EndpointRequest.toAnyEndpoint()).authorizeRequests()
                    .requestMatchers(EndpointRequest.to(ENDPOINTS)).permitAll()
                    .requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ENDPOINT_ADMIN").and()
                    .httpBasic();
        }
    }

    // other commented out inner classes for oauth and general handling
}

build.gradle

Very short version, but it should illustrate on what this is built.


plugins {
    //Spring Boot
    id 'org.springframework.boot' version '2.3.3.RELEASE'
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-security'
    compile 'org.springframework.boot:spring-boot-starter-actuator'
    compile 'org.springframework.boot:spring-boot-starter-amqp'
    compile 'org.springframework.boot:spring-boot-starter-data-jpa'
    compile 'org.springframework.boot:spring-boot-starter-data-rest'
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile 'org.springframework.cloud:spring-cloud-starter-kubernetes-config'
    compile 'org.springframework.cloud:spring-cloud-starter-openfeign'
    compile 'org.springframework.security:spring-security-oauth2-resource-server'
    compile 'org.springframework.security:spring-security-oauth2-jose'
    compile 'org.springframework.security:spring-security-config'
}

1 Answers

Quick explantation

In your application.yml you have configured both HTTP basic auth and OAuth2 resource server.
While it is reasonable to have an application that uses both, Spring Boot will not be able to figure out the specific configuration that you need based on those properties.
Furthermore, the Spring Boot auto-configuration is intended for straight forward scenarios. For more complex configurations, like this one, it is best to explicitly create the beans that you need.

In-depth explanation

Here is an overview of what is happening with your current configuration.

Since spring.security.oauth2.resourceserver.jwt.jwk-set-uri is set, Spring Boot understands that your application is an OAuth2 resource server and generates a JwtDecoder bean. (see OAuth2ResourceServerAutoConfiguration)

If you remove that property and only set spring.security.user.name, then Spring Boot generates a default InMemoryUserDetailsManager with the user information you have supplied. (see UserDetailsServiceAutoConfiguration)
However, when a JwtDecoder bean exists, Spring Boot will not create the default InMemoryUserDetailsManager.
For background you can see this issue in the Spring Boot backlog.

Now, your application does not have an InMemoryUserDetailsManager, but it still tries to authentication using HTTP basic auth because you have specified http.httpBasic(). That is why you see the error message.

Solution

Rather than specifying the user credentials in the application.yml, create a UserDetailsService bean. You would want to do this regardless, since InMemoryUserDetailsManager is meant mainly for testing purposes and not recommended for use in production.
I will use InMemoryUserDetailsManager in the example, simply to illustrate the equivalent configuration.

@EnableWebSecurity
public class SecurityConfiguration {
    
    @Order(1)
    @Configuration
    public class HttpBasicAuthSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .antMatcher("/basic/**")
                    .authorizeRequests(authorize -> authorize
                        .anyRequest().authenticated()
                    )
                    .httpBasic(withDefaults());
        }

        // Explicitly create bean, instead of configuring user in application.yml
        @Bean
        public UserDetailsService userDetailsService() {
            UserDetails user = User.withDefaultPasswordEncoder()
                    .username("user")
                    .password("password")
                    .roles("USER")
                    .build();
            return new InMemoryUserDetailsManager(user);
        }
    }

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

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .antMatcher("/oauth2/**")
                    .authorizeRequests(authorize -> authorize
                            .anyRequest().authenticated()
                    )
                .oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(withDefaults())
                );
        }

        // Explicitly create bean, instead of configuring decoder in application.yml
        @Bean
        JwtDecoder jwtDecoder() {
            NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder
                    .withJwkSetUri("https://somejwkuri")
                    .build();
            jwtDecoder
                    .setJwtValidator(JwtValidators.createDefaultWithIssuer("https://someissuer"));
            return jwtDecoder;
        }
    }
}
Related