Search code examples
javaspringspring-bootspring-securityoauth-2.0

OAUTH2 user service with Custom Authentication Providers


I am new to Spring Security and Oauth2. In my spring boot application, I have implemented authentication with Oauth2 with following set of changes:

Custom Ouath2 User service is as follows:

  @Component
  public class CustomOAuth2UserService extends DefaultOAuth2UserService {

     private UserRepository userRepository;

     @Autowired
     public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
     }

    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        ...
    }
 }

Security Configuration is as follows:

@EnableWebSecurity
@Import(SecurityProblemSupport.class)
@ConditionalOnProperty(
        value = "myapp.authentication.type",
        havingValue = "oauth",
        matchIfMissing = true
)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  private final CustomOAuth2UserService customOAuth2UserService;
    
  public SecurityConfiguration(CustomOAuth2UserService customOAuth2UserService) {
    this.customOAuth2UserService = customOAuth2UserService;
  }

  @Override
  public void configure(WebSecurity web) {
    web.ignoring()
        .antMatchers(HttpMethod.OPTIONS, "/**")
        .antMatchers("/app/**/*.{js,html}")
        .antMatchers("/bundle.js")
        .antMatchers("/slds-icons/**")
        .antMatchers("/assets/**")
        .antMatchers("/i18n/**")
        .antMatchers("/content/**")
        .antMatchers("/swagger-ui/**")
        .antMatchers("/swagger-resources")
        .antMatchers("/v2/api-docs")
        .antMatchers("/api/redirectToHome")
        .antMatchers("/test/**");
  }

  public void configure(HttpSecurity http) throws Exception {
    RequestMatcher csrfRequestMatcher = new RequestMatcher() {
      private RegexRequestMatcher requestMatcher =
          new RegexRequestMatcher("/api/", null);

      @Override
      public boolean matches(HttpServletRequest request) {
        return requestMatcher.matches(request);
      }
    };

    http.csrf()
        .requireCsrfProtectionMatcher(csrfRequestMatcher)
        .and()
        .authorizeRequests()
        .antMatchers("/login**").permitAll()
        .antMatchers("/manage/**").permitAll()
        .antMatchers("/api/auth-info").permitAll()
        .antMatchers("/api/**").authenticated()
        .antMatchers("/management/health").permitAll()
        .antMatchers("/management/info").permitAll()
        .antMatchers("/management/prometheus").permitAll()
        .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
        .anyRequest().authenticated()//.and().oauth2ResourceServer().jwt()
        .and()
        .oauth2Login()
        .redirectionEndpoint()
        .baseUri("/oauth2**")
        .and()
        .failureUrl("/api/redirectToHome")
        .userInfoEndpoint().userService(oauth2UserService())
    ;
    http.cors().disable();
  }


  private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
    return customOAuth2UserService;
  }
}

Content of application.properties is as follows:

spring.security.oauth2.client.registration.keycloak.client-id=abcd
spring.security.oauth2.client.registration.keycloak.client-name=Auth Server
spring.security.oauth2.client.registration.keycloak.scope=api
spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.client-authentication-method=basic
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
myapp.oauth2.path=https://internal.authprovider.com/oauth2/
spring.security.oauth2.client.provider.keycloak.token-uri=${myapp.oauth2.path}token
spring.security.oauth2.client.provider.keycloak.authorization-uri=${myapp.oauth2.path}authorize
spring.security.oauth2.client.provider.keycloak.user-info-uri=${myapp.oauth2.path}userinfo
spring.security.oauth2.client.provider.keycloak.user-name-attribute=name
myapp.authentication.type=oauth

Now, with the existing authentication mechanism, I would like to add support for multiple authentication providers: LDAP, Form-Login, etc.

In this regard, I have gone through a few articles:

  1. https://www.baeldung.com/spring-security-multiple-auth-providers
  2. Custom Authentication provider with Spring Security and Java Config

But, I am not getting any concrete idea regarding what changes should I do in the existing code base in order to achieve this.

Could anyone please help here? Thanks.


Solution

  • I've created a simplified setup starting from your code with support for both OAuth2 and Basic Auth.

    /tenant2/** will start a basic authentication. /** (everything else) triggers an OAuth2 Authorization Code authentication.

    The key to achieve this is to have one @Configuration class per authentication type.

    Let's start with the controllers:

    Tenant1HomeController

    @Controller
    public class Tenant1HomeController {
    
        @GetMapping("/tenant1/home")
        public String home() {
            return "tenant1Home";
        }
    
    }
    

    Tenant2HomeController

    @Controller
    public class Tenant2HomeController {
    
        @GetMapping("/tenant2/home")
        public String home() {
            return "tenant2Home";
        }
    
    }
    

    Now, the configuration classes:

    Tenant1SecurityConfiguration

    @Configuration
    public class Tenant1SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()
                    .authorizeRequests()
                    .antMatchers("/login**").permitAll()
                    .antMatchers("/manage/**").permitAll()
                    .antMatchers("/api/auth-info").permitAll()
                    .antMatchers("/api/**").authenticated()
                    .antMatchers("/management/health").permitAll()
                    .antMatchers("/management/info").permitAll()
                    .antMatchers("/management/prometheus").permitAll()
                    .antMatchers("/management/**").hasAuthority("ADMIN")
                    .antMatchers("/tenant1/**").authenticated()
                    .and()
                    .oauth2Login()
                    .and()
                    .cors()
                    .disable();
        }
    }
    

    Tenant2SecurityConfiguration (Notice the @Order(90), that's important

    @Order(90)
    @Configuration
    public class Tenant2SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.requestMatcher(new AntPathRequestMatcher("/tenant2/**"))
                    .csrf()
                    .disable()
                    .authorizeRequests()
                    .antMatchers("/tenant2/**").hasAuthority("BASIC_USER")
                    .and()
                    .httpBasic();
            http.cors().disable();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("user")
                    .password("{noop}password")
                    .roles("BASIC_USER");
        }
    }
    

    Finally the configuration:

    spring:
      security:
        oauth2:
          client:
            registration:
              keycloak:
                client-id: myclient
                client-secret: c6dce03e-ea13-4b76-8aab-c876f5c2c1d9
            provider:
              keycloak:
                issuer-uri: http://localhost:8180/auth/realms/myrealm
    

    With this in place, if we hit http://localhost:8080/tenant2/home, will be prompted with the basic auth popup:

    enter image description here

    Trying with http://localhost:8080/tenant1/home sends you to Keycloak's login form:

    enter image description here

    UPDATE:

    It's completely viable to configure a multitenant application with the configuration above.

    The key would be that each authentication provider works with a different set of users (tenants), e.g.:

    TENANT 1 (OAuth2 authentication):

    @Configuration
    public class Tenant1SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.
                ...
                .and()
                .oauth2Login()
                .and()
                ...
                
    

    This first subset of users is federated by the OAuth2 provider, Keycloak in this case.

    TENANT 2 (Basic / form /xxx authentication):

    @Order(90)
    @Configuration
    public class Tenant2SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            ...
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(XXX)
    

    For the second tenant, you can use a userDetailsService that points to a different repository of users (LDAP, database...).