7

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.

Joy
  • 4,197
  • 14
  • 61
  • 131

1 Answers1

9

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...).

codependent
  • 23,193
  • 31
  • 166
  • 308
  • Thanks for the detailed answer. I am actually working on making my application multi-tenant where each tenant might have its own authentication provider say tenant A uses Oauth2, tent B uses form-login, etc. Here my question is: how should I achieve this with your answer? – Joy Dec 08 '21 at 02:55
  • I've updated the answer with more info – codependent Dec 08 '21 at 10:43
  • Thanks for your answer. Actually, I am not able to understand the following: say one user A logs into the application, here how will the tenant id of associated user will be determined and then how appropriate authentication provider will be invoked based on tenant id? – Joy Dec 08 '21 at 11:26
  • 1
    One way to do that is use different URLs for each tenant (I've updated the answer with an example). – codependent Dec 08 '21 at 11:46
  • Thanks. Could you please show the approach as well , where there is one single entry point of the application and post logging into the application, based on authentication token of the user, tenant is determined and associated authentication provider is called ? – Joy Dec 08 '21 at 13:25
  • Hi, the original question was: `"I would like to add support for multiple authentication providers: LDAP, Form-Login, etc."`. I think I already answered that and this is kind of deviating to something else. Anyway, what I posted already shows a way to login different tenants using two providers based on different URLs (`/tenant2` and `/tenant1`). – codependent Dec 08 '21 at 14:17
  • Yeah, you are correct. Apologies, I would post another question for the same. Thanks :) – Joy Dec 08 '21 at 14:25