Search code examples
javaspring-bootspring-securityspring-security-oauth2

Spring Boot Web Oauth2 Side-By-Side Basic Auth Resources


The concise Spring Boot web application protects one resource on path "/resource" with OAuth2/ OIDC using opaque token introspection, while the resource "/helloworld" should be protected with Basic http Authentication. There is an oauth2 credentials server in the demo system, that works and is returning a technical client id that is evaluated after introspection and ends up with disposing the role "oauth". Basic authentication as is spring security standard is mapped to the role "demo".

The "/resource" can then be retrieved by an http request sending the right bearer token, and the oauth2 credentials server probing and returning the matching client id. However, submitting an http request to "/helloworld" with Basic authentication, with credentials username and password "demo", refer application.yaml, the application returns http 401 Unauthorized. The same as, when trying without any authentication, at all.

How to make basic authentication work alongside oauth2 resources in a single Spring Boot application?

DemoApplication.java

@SpringBootApplication
@RestController
public class DemoApplication {

    @PreAuthorize("hasRole('demo')")
    @GetMapping("/helloworld")
    public String hello() {
        return "Hello World!";
    }

    @PreAuthorize("hasRole('oauth2')")
    @GetMapping("/resource")
    public String resource() {
        return "Protected resource";
    }

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

}

DemoSecurityConfiguration.java

public class DemoSecurityConfiguration {

        @Configuration
        @Order(10)
        @EnableWebSecurity
        public static class HelloWorldBasicSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
                @Override
                protected void configure(HttpSecurity http) throws Exception {

                        http.antMatcher("/helloworld")
                                .httpBasic()
                                .and()
                                .authorizeRequests().antMatchers("/helloworld").authenticated()
                        ;
                }
        }

        @Configuration
        @Order(0)
        @EnableWebSecurity
        public static class ResourceSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
                @Autowired
                private Environment environment;

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

                        http.antMatcher("/resource")
                                .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(
                                opaqueToken -> opaqueToken.introspector(new DemoAuthoritiesOpaqueTokenIntrospector())))
                                .authorizeRequests().antMatchers("/resource").authenticated()
                        ;
                }
                private class DemoAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
                        private final OpaqueTokenIntrospector delegate;

                        private final String demoClientId;

                        public DemoAuthoritiesOpaqueTokenIntrospector() {
                                String introSpectionUri = environment
                                        .getProperty("spring.security.oauth2.resourceserver.opaque-token.introspection-uri");
                                String clientId = environment
                                        .getProperty("spring.security.oauth2.resourceserver.opaque-token.client-id");
                                String clientSecret = environment
                                        .getProperty("spring.security.oauth2.resourceserver.opaque-token.client-secret");
                                demoClientId = environment.getProperty("demo.security.oauth2.credentials-grant.client-id");

                                delegate = new NimbusOpaqueTokenIntrospector(introSpectionUri, clientId, clientSecret);
                        }

                        public OAuth2AuthenticatedPrincipal introspect(String token) {
                                OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);

                                return new DefaultOAuth2AuthenticatedPrincipal(principal.getName(), principal.getAttributes(),
                                        extractAuthorities(principal));
                        }

                        private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
                                String userId = principal.getAttribute("client_id");

                                if (demoClientId.equals(userId)) {
                                        return Collections.singleton(new SimpleGrantedAuthority("ROLE_oauth2"));
                                }

                                return Collections.emptySet();
                        }
                }
        }

}

application.yaml

spring:
  security:
    basic:
      enabled: true
    user:
      name: demo
      password: demo
      roles: demo
    oauth2:
      resourceserver:
        opaque-token:
          introspection-uri: "https://...oauth2"
          client-id: "abba"
          client-secret: "secret"

demo:
  security:
    oauth2:
      credentials-grant:
        client-id: "rundmc

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.6'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation("org.springframework.security:spring-security-oauth2-resource-server")
    implementation("org.springframework.security:spring-security-oauth2-jose")
    implementation("org.springframework.security:spring-security-oauth2-client")
}

Solution

  • I think that the properties under spring.security.user won't work because Spring Boot will back off creating the UserDetailService bean for you if you have defined some beans, including OAuth2 ones. The relevant part of the code (you can see the full source code here):

    @ConditionalOnMissingBean(
            value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
                    AuthenticationManagerResolver.class },
            type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
                    "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
                    "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository",
                    "org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" })
    public class UserDetailsServiceAutoConfiguration {
    

    That said, you will have to define the UserDetailsService bean by yourself, one way to do that is:

    @Bean
    public UserDetailsService inMemoryUserDetailsService() {
        UserDetails demo = User.withUsername("demo").password("demo").roles("demo").build();
        return new InMemoryUserDetailsManager(demo);
    }
    

    I'm not sure right now, but maybe the password should be {noop}demo to tell the PasswordEncoder bean that the password is not encoded. Remember that a noop password encoder should only be used for demo purposes.