Search code examples
spring-securityoauth-2.0spring-security-oauth2

Single resource server with multiple authorisation servers, one for each tenant


I am working on a Spring Boot application, which is basically a resource server. As of now, my application has one tenant, which gets authenticated with an authorization server, external to my application.

In order to achieve the same, as of now, I have made the following changes in my application:

config changes are as following:

spring.security.oauth2.client.registration.tenant1.client-id=abcd
spring.security.oauth2.client.registration.tenant1.client-authentication-method=basic
spring.security.oauth2.client.registration.tenant1.authorization-grant-type=authorization_code
myapp.oauth2.path=https://external.authorization.server/services/oauth2/
spring.security.oauth2.client.provider.tenant1.token-uri=${myapp.oauth2.path}token
spring.security.oauth2.client.provider.tenant1.authorization-uri=${myapp.oauth2.path}authorize
spring.security.oauth2.client.provider.tenant1.user-info-uri=${myapp.oauth2.path}userinfo
spring.security.oauth2.client.provider.tenant1.user-name-attribute=name

As of now, I am fetching client secrets from Vault, so I had to define the OAuth2 configuration as follows:

@EnableConfigurationProperties(OAuth2ClientProperties.class)
@Conditional(ClientsConfiguredCondition.class)
@Configuration
public class OAuth2Configuration {

  static final String OAUTH2_CLIENT_SECRET_KEY = "oauth2_client_secret";
  private static final Logger log = LoggerFactory.getLogger(OAuth2Configuration.class);
  private static final String OAUTH2_REGISTRATION_MISSING =
      "oAuth2 registration properties are missing";
  private final ApplicationSecretProvider applicationSecretProvider;
  private final Map<String, ClientAuthenticationMethod> clientAuthenticationMethodMap =
      new HashMap<>();
  private final String authenticationMethod;

  public OAuth2Configuration(
      @Value("${spring.security.oauth2.client.registration.tenant1.client-authentication-method}")
      final String authenticationMethod,
      final ApplicationSecretProvider applicationSecretProvider) {
    this.authenticationMethod = authenticationMethod;
    this.applicationSecretProvider = applicationSecretProvider;
    this.clientAuthenticationMethodMap
        .put(ClientAuthenticationMethod.POST.getValue(), ClientAuthenticationMethod.POST);
    this.clientAuthenticationMethodMap
        .put(ClientAuthenticationMethod.BASIC.getValue(), ClientAuthenticationMethod.BASIC);
    this.clientAuthenticationMethodMap
        .put(ClientAuthenticationMethod.NONE.getValue(), ClientAuthenticationMethod.NONE);
  }

  @Bean
  public InMemoryClientRegistrationRepository getClientRegistrationRepository(
      OAuth2ClientProperties properties) {

    List<ClientRegistration> registrations = new ArrayList<>(
        OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
    //We will have only one client registered for oAuth
    if (CollectionUtils.isEmpty(registrations)) {
      log.error(OAUTH2_REGISTRATION_MISSING);
      throw new IllegalStateException(OAUTH2_REGISTRATION_MISSING);
    }
    ClientRegistration registration = registrations.get(0);
    ClientRegistration.Builder builder = ClientRegistration.withClientRegistration(registration);

    ClientAuthenticationMethod clientAuthenticationMethod =
        getClientAuthenticationMethod(authenticationMethod);
    
    ClientRegistration completeRegistration = builder
        .clientSecret(applicationSecretProvider.getSecretForKey(OAUTH2_CLIENT_SECRET_KEY))
        .clientAuthenticationMethod(clientAuthenticationMethod)
        .build();
    return new InMemoryClientRegistrationRepository(completeRegistration);
  }

  protected ClientAuthenticationMethod getClientAuthenticationMethod(String grantType) {
    ClientAuthenticationMethod retValue = clientAuthenticationMethodMap.get(grantType);
    if (retValue == null) {
      return ClientAuthenticationMethod.NONE;
    }
    return retValue;
  }
}

Then I extended DefaultOAuth2UserService in order to save user details in my application as follows:

@Component
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

  private UserRepository userRepository;
  private AuthorityRepository authRepository;    
  
  @Autowired
  public void setUserRepository(UserRepository userRepository) {
    this.userRepository = userRepository;
  }    
  
  @Autowired
  public void setAuthorityRepository(AuthorityRepository
                                                  authorityRepository) {
    this.authorityRepository = authorityRepository;
  }

  @Override
  public OAuth2User loadUser(OAuth2UserRequest userRequest)  {
    DefaultOAuth2User oAuth2User = (DefaultOAuth2User) super.loadUser(userRequest);
    Collection<GrantedAuthority> authorities = new HashSet<>(oAuth2User.getAuthorities());
    Map<String, Object> attributes = oAuth2User.getAttributes();
    ...
    return new DefaultOAuth2User(authorities, oAuth2User.getAttributes(), userNameAttributeName);
  }
}

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;
  }

  public void configure(HttpSecurity http) throws Exception {

    http
        .csrf()
        .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()
            //.and()
        .oauth2Login()
            .redirectionEndpoint()
                .baseUri("/oauth2**")
                .and()
            .failureUrl("/api/redirectToHome")
            .userInfoEndpoint().userService(customoAuth2UserService);
    http.cors().disable();
  }
}

Now, I would like to onboard multiple tenants using OAuth2 as well. Say I want to onboard another tenant tenant2. In order to achieve this, I think, I need to do the following changes in the existing code base as follows:

  1. adding config entries in the properties file as above:

     spring.security.oauth2.client.registration.tenant2.client-id=efgh
     spring.security.oauth2.client.registration.tenant2.client-authentication-method=basic
     spring.security.oauth2.client.registration.tenant2.authorization-grant-type=authorization_code
     spring.security.oauth2.client.provider.tenant2.token-uri=${myapp.oauth2.path}token
     spring.security.oauth2.client.provider.tenant2.authorization-uri=${myapp.oauth2.path}authorize
     spring.security.oauth2.client.provider.tenant2.user-info-uri=${myapp.oauth2.path}userinfo
     spring.security.oauth2.client.provider.tenant2.user-name-attribute=name
    
  2. I need to do changes in the security configuration class:

    SecurityConfiguration and OAuth2 configuration class OAuth2Configuration as well. But I am not able to understand what should I add there in order to make my applications work seamlessly for multiple tenants.

In this context, I found this related post: Dynamically register OIDC client with Spring Security OAuth in a multi-tenant stup, but could not get any concrete idea regarding what changes should I do in the existing code base to make my application work in multi-tenancy set up.

Could anyone please help here?


Solution

  • I think there's a bit of confusion that it might help to clear up.

    First, it seems that you are not actually building a resource server, as a resource server would require an access token for authentication. Using .oauth2Login() is for either OAuth 2.0 or OpenID Connect 1.0 login, which is a regular application in most respects except how you log in. You still have a browser session after login is successful, which you would not have in a resource server.

    Second, configuring a static number of client registrations isn't really quite the same as building a multi-tenant application. Perhaps you're building up to that later, by demonstrating two clients. When configuring two clients using static configuration properties, nothing is really different from a single configuration, other than that there are two possible registrationIds.

    Start by building a simple hello world application, such as the OAuth 2.0 Login Sample. If you add a second client registration to your properties, you'll notice that the auto-generated login page (/login) simply shows two links, one for each client. See docs for more on this.

    The default URI for initiating the authorization_code flow is /oauth2/authorization/{registrationId}, which means navigating to /oauth2/authorization/abcd launches the first client's login flow. Navigating to /oauth2/authorization/efgh launches the second client's login flow. There's not really anything else needed to support multiple login clients other than understanding how to initiate login.

    If you wish to support a fully multi-tenant login configuration, you would then provide a custom ClientRegistrationRepository, which you have done. The only difference is that you should no longer seek to configure clients through the Spring Boot properties, as that seems to be the point that is confusing in your example. If you want to use properties for some of the configuration, create your own configuration properties for your custom repository implementation. Typically at that point, all of this configuration would come from a database.

    I would start with that progression (hello world, two statically configured clients, custom ClientRegistrationRepository) then proceed to add other custom components. It will help illustrate the differences at each point.