• Lisa Steendam

  • Software Engineer

Spring Social offers a fairly complete implementation for most use cases. Yet, it doesn’t cover some issues, like what to do if you want to add two (or more) different ConnectionFactories to the same social provider.

This could, for example, be useful if you have a platform that is used by several customers, each of which want to keep their user data separate from one another. Here, we will show you a way to work around that, but first we will take a look at the possible solutions.

Note that in this blog post we will only use LinkedIn as an example, but you can do it with any other social API.

Problem: Two ConnectionFactories for the same social provider

If, for this issue, you would use the “normal” approach of adding a different social provider, that is, by creating a second LinkedInConnectionFactory next to the first LinkedInConnectionFactory, you would get the following:

@Bean
public ConnectionFactoryLocator connectionFactoryLocator() {
    ConnectionFactoryRegistry registry = new ConnectionFactoryRegistry();
    registry.addConnectionFactory( new LinkedInConnectionFactory( 
        "linkedIn.clientId1", "linkedIn.clientSecret1" ) );
    registry.addConnectionFactory(new LinkedInConnectionFactory( 
        "linkedIn.clientId2", "linkedIn.clientSecret2" ) );
    return registry;
}

Now, although the clientId and clientSecret are different, Spring Social still considers them both the same. Why? Because Spring Social uses the providerId and API-class to identify ConnectionFactories. And because those are still the same in the above snippet, this will result in IllegalArgumentException: "A ConnectionFactory for provider ”linkedin” has already been registered" being thrown by the ConnectionFactoryRegistry. In other words: Not a solution.

Give a different ProviderId to your additional ConnectionFactory

To tackle this we will create our own additional ConnectionFactory with a different providerId.

public class FooLinkedInConnectionFactory extends OAuth2ConnectionFactory<LinkedIn> {
   public FooLinkedInConnectionFactory( String consumerKey, String consumerSecret ) {
      super( "fooLinkedIn", new LinkedInServiceProvider( consumerKey, consumerSecret ), 
         new LinkedInAdapter() );
  }
}

The problem isn’t completely solved by registering our FooLinkedInConnectionFactory next to Spring Social’s own LinkedInConnectionFactory though. This time you get IllegalArgumentException: "A ConnectionFactory for API [LinkedIn] has already been registered", which is again thrown by the ConnectionFactoryRegistry.

Differentiate the ConnectionFactories for the API

You can see why these exceptions were thrown when you take a look at ConnectionFactoryRegistry and its method addConnectionFactory.

public class ConnectionFactoryRegistry implements ConnectionFactoryLocator {
… 
/**
* Add a {@link ConnectionFactory} to this registry.
*
* @param connectionFactory the connection factory
*/
public void addConnectionFactory(ConnectionFactory<?> connectionFactory) {
 if (connectionFactories.containsKey(connectionFactory.getProviderId())) {
    throw new IllegalArgumentException("A ConnectionFactory for provider '" + connectionFactory.getProviderId() + "' has already been registered");
 }
 Class<?> apiType = GenericTypeResolver.resolveTypeArgument(connectionFactory.getClass(), ConnectionFactory.class);
 if (apiTypeIndex.containsKey(apiType)) {
    throw new IllegalArgumentException("A ConnectionFactory for API [" + apiType.getName() + "] has already been registered");
 }
 connectionFactories.put(connectionFactory.getProviderId(), connectionFactory);
 apiTypeIndex.put(apiType, connectionFactory.getProviderId());
}
…
}

ConnectionFactoryRegistry keeps the ConnectionFactories in two maps: One based on the providerId, the other on the API-type. So while we did give our FooLinkedInConnectionFactory a different providerId, we still need to adapt the method above, so it accepts the same API more than once.

You can do this by adding a new boolean parameter to the method that indicates whether the ConnectionFactory needs to be added to the API-based map, or only to the providerId-based one.

public class CustomConnectionFactoryRegistry implements ConnectionFactoryLocator {
… 
/**
* Add ConnectionFactory
*
* @param connectionFactory given connectionFactory
* @param addAsDefaultForApiTypeIndex true if we should add it to apiTypes, false if it shouldn't be added (for LinkedIn we want 1 default)
*/
public void addConnectionFactory( ConnectionFactory<?> connectionFactory, boolean addAsDefaultForApiTypeIndex ) {
  if ( connectionFactories.containsKey( connectionFactory.getProviderId() ) ) {
      throw new IllegalArgumentException( "A ConnectionFactory for provider '" + connectionFactory.getProviderId() + "' has already been registered" );
  }
 
  connectionFactories.put( connectionFactory.getProviderId(), connectionFactory );
 
  if ( addAsDefaultForApiTypeIndex ) {
      Class<?> apiType = GenericTypeResolver.resolveTypeArgument( connectionFactory.getClass(), ConnectionFactory.class );
      if ( apiTypeIndex.containsKey( apiType ) ) {
          throw new IllegalArgumentException( "A ConnectionFactory for API [" + apiType.getName() + "] has already been registered" );
      }
      apiTypeIndex.put( apiType, connectionFactory.getProviderId() );
  }
}
…
}

Note that ConnectionFactoryLocator has two methods to get the correct ConnectionFactory for a provider: One method is based on the API, the other one on the providerId.

With our solution, it is preferred to use getConnectionFactory( String providerId ), because the API-based one will not be able to return every registered ConnectionFactory.

Usage of the adapted addConnectionFactory-Method

Just like with Spring’s ConnectionFactoryRegistry, our addConnectionFactory-method is used while instantiating the bean.

@Bean
public ConnectionFactoryLocator connectionFactoryLocator() {
  CustomConnectionFactoryRegistry registry = new CustomConnectionFactoryRegistry();
  registry.addConnectionFactory( new LinkedInConnectionFactory(
        "linkedIn.clientId1", "linkedIn.clientSecret1" ), true );
  registry.addConnectionFactory( new FooLinkedInConnectionFactory(
        "linkedIn.clientId2", "linkedIn.clientSecret2" ), false );
  return registry;
}

Wrapping it all up

The change is in and of itself very minimal and contained. You just need to create the additional ConnectionFactories and then make your own implementation of Spring Social’s ConnectionFactoryLocator. In it, you adapt the addConnectionFactory-method with a new boolean property, so that only one ConnectionFactory is added to the API-map for each social provider. With these small changes, it is now possible to keep the user data separate for different customers. For more Spring Social use cases, see How to Render the Social Login Flow in a Popup