Reputation: 4483
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:
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
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?
Upvotes: 1
Views: 1943
Reputation: 6103
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 registrationId
s.
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.
Upvotes: 2