2
votes

I am building a Spring Social application that supports multiple tenants. The application identifies the tenants based on the url (e.g., tenant1.example.com and tenant2.example.com). I want to allow users of the application to connect using Facebook or Twitter. The issue I am having is that each tenant is a unique business and will have its own unique set of Facebook & twitter credentials. I don't think I can have more than one Facebook ConnectionFactory instance or more than one Twitter ConnectionFactory instance in my ConnectionFactoryRegistry. For example, this will not work:

ConnectionFactoryRegistry registry = new ConnectionFactoryRegistry();
registry.addConnectionFactory(new FacebookConnectionFactory("tenant1ClientId", "tenant1ClientSecret"));
registry.addConnectionFactory(new FacebookConnectionFactory("tenant2ClientId", "tenant2ClientSecret"));

The above will not work because, according to the Spring Social documentation, the locator returns a single instance.

public interface ConnectionFactoryLocator {
   ConnectionFactory<?> getConnectionFactory(String providerId);
   <A> ConnectionFactory<A> getConnectionFactory(Class<A> apiType);

   Set<String> registeredProviderIds();
}   

I thought about creating a ConnectInterceptor and injecting the relevant tenant credentials into the provided ConnectionFactory instance during the preConnect() step, but I suspect that will not work in a multi-threaded web application since the connectionFactory instance is shared.

So, my next thought was to simply create a unique ConnectionFactoryRegistry instance for each tenant. That solves the above issue.

But then the problem moves to the ConnectController (Spring's OAuth dance controller). Since the application can only have one controller, and the controller only holds one registry, how would I instruct the controller to use the right registry based on the tenant?

What is the right way to build a multitenant Spring Social application which will support unique ConnectionFactory instances for each tenant? Ideally, I would NOT want to define the ConnectionFactory instances in XML, since they are dynamic and stored in a DB.

2
I have faced similar problem in the past. Spring Social + multilingual site (example.de, example.fr, ...) => multiple facebook apps (because with Facebook app you can return a user only to the same domain). I ended up with my custom version of ConnectController (in reality it was ProviderSignInController) and one helper class (I am not sure about name, ConnectionSupport or something like this) to be able choose at runtime right FacebookConnectionFactory. Hope this helps.Maksym Demidas
Maksym, I think your approach of creating a custom ProviderSignInController is the right solution. All of the other framework components work well, and the controller logic is pretty simple. I have successfully connected to Facebook and Twitter using this approach.Raff
Glad to know that it works. I post my comment as a answer.Maksym Demidas
Hi @Raff, can you share the solution? As i have the same problem of using multiple tennants.Adith

2 Answers

1
votes

I have faced similar problem in the past. Spring Social + multilingual site (example.de, example.fr, ...) => multiple facebook apps (because with Facebook app you can return a user only to the same domain). I ended up with my custom version of ConnectController (in reality it was ProviderSignInController) and one helper class (I am not sure about name, ConnectionSupport or something like this) to be able choose at runtime right FacebookConnectionFactory.

1
votes

I found that creating a CustomConnectionFactoryRegistry will do the trick as well and tackles the problem more downstream. Twitter example below. The solution relies on a data repository to retrieve the oauth credentials for the application tenant and providerId which are cached.

public class CustomConnectionFactoryRegistry implements ConnectionFactoryLocator {

    @Autowired
    private ConnectServiceRepository connectServiceRepository; // get the credentials for the tenant

    private final Map<Class<?>, String> apiTypeIndex = new HashMap<Class<?>, String>();

    public CustomConnectionFactoryRegistry(){
        apiTypeIndex.put(Twitter.class, "twitter");
    }

    @Override
    public ConnectionFactory<?> getConnectionFactory(String providerId) {
        OAuthApp app = connectServiceFactory.getProviderApp(providerId);
        if (app == null) {
            throw new IllegalArgumentException("No connection factory for service provider '" + providerId + "' is registered");
        }
        OAuth1ConnectionFactory<?> twitterConnectionFactory = new TwitterConnectionFactory(app.getApiKey(),app.getApiSecret());

        return twitterConnectionFactory;        
    }

    @Override
    @SuppressWarnings("unchecked")
    public <A> ConnectionFactory<A> getConnectionFactory(Class<A> apiType) {
        String providerId = apiTypeIndex.get(apiType);
        if (providerId == null) {
            throw new IllegalArgumentException("No connection factory for API [" + apiType.getName() + "] is registered");
        }
        return (ConnectionFactory<A>) getConnectionFactory(providerId);
    }

}