4. Connecting to Service Providers

4.1 Introduction

In Chapter 2, Service Provider 'Connect' Framework, you learned how Spring Social's Service Provider 'Connect' Framework can be used to manage user connections that link your application's user accounts with accounts on external service providers. In this chapter, you'll learn how to control the connect flow in a web application environment.

Spring Social's spring-social-web module includes ConnectController, a Spring MVC controller that coordinates the connection flow between an application and service providers. ConnectController takes care of redirecting the user to the service provider for authorization and responding to the callback after authorization.

4.2 Configuring ConnectController

As ConnectController directs the connection flow, it depends on a couple of other objects to assist in the creation and persistence of connections. ConnectController works with one or more ConnectionFactorys to exchange authorization details with the provider and to create connections. Once a connection has been established, ConnectController hands it off to a ConnectionRepository to be persisted.

Spring Social comes with an implementation of ConnectionFactory for each of the supported service providers:

  • TwitterConnectionFactory

  • FacebookConnectionFactory

  • LinkedInConnectionFactory

  • TripItConnectionFactory

  • GitHubConnectionFactory

  • GowallaConnectionFactory

ConnectController relies on an implementation of ConnectionFactoryLocator (see Section 2.2.3, “Registering ConnectionFactory instances”) to find a connection factory for a specific provider. Spring Social's ConnectionFactoryRegistry is an implementation of ConnectionFactoryLocator that keeps a Map-based registry of connection factories. The following class constructs a ConnectionFactoryRegistry containing ConnectionFactorys for Twitter, Facebook, and TripIt using Spring's Java configuration style:

@Configuration
public class ConnectionFactoryConfig {
	
    @Bean
    public ConnectionFactoryLocator connectionFactoryLocator() {
        ConnectionFactoryRegistry registry = new ConnectionFactoryRegistry();
        registry.addConnectionFactory(new TwitterConnectionFactory(twitterConsumerKey, twitterConsumerSecret));
        registry.addConnectionFactory(new FacebookConnectionFactory(facebookClientId, facebookClientSecret));
        registry.addConnectionFactory(new TripItConnectionFactory(tripItConsumerKey, tripItConsumerSecret));
        return registry;
    }

    @Value("${twitter.consumerKey}")
    private String twitterConsumerKey;
	
    @Value("${twitter.consumerSecret}")
    private String twitterConsumerSecret;

    @Value("${facebook.clientId}")
    private String facebookClientId;
	
    @Value("${facebook.clientSecret}")
    private String facebookClientSecret;

    @Value("${tripit.consumerKey}")
    private String tripItConsumerKey;
	
    @Value("${tripit.consumerSecret}")
    private String tripItConsumerSecret;
	
}

Here, three connection factories--one each for Twitter, Facebook, and TripIt--are registered with ConnectionFactoryRegistry via the addConnectionFactory() method. If we wanted to add support for connecting to other providers, we would simply register their connection factories here. Because consumer keys and secrets may be different across environments (e.g., test, production, etc) it is recommended that these values be externalized. Therefore, they are wired in with @Value as property placeholder values to be resolved by Spring's property placeholder support.

ConnectController will use the ConnectionFactorys that it obtains through ConnectionFactoryLocator to perform the authorization exchange with each provider and ultimately to create a connection. Once a connection has been created, it must be persisted and associated with the user's account. For that, ConnectController depends on a ConnectionRepository.

As discussed in Section 2.3, “Persisting connections”, ConnectionRepository defines operations for persisting and restoring connections for a specific user. Therefore, when configuring a ConnectionRepository bean, it must be scoped such that it can be created on a per-user basis. The following Java-based configuration shows how to configure ConnectionRepository bean in request scope for the currently authenticated user:

@Configuration
public class ConnectionRepositoryConfig {

    @Inject
    private UsersConnectionRepository usersConnectionRepository;

    @Bean
    @Scope(value="request")
    public ConnectionRepository connectionRepository(@Value("#{request.userPrincipal}") Principal principal) {
        return usersConnectionRepository.createConnectionRepository(principal.getName());
    }
	
}
		

The connectionRepository() method is injected with a Principal (pulled from the request with a Spring Expression Language expression). The Principal is passed to the UsersConnectionRepository's createConnectionRepository() method to create a ConnectionRepository for the current user in the context of the current web request.

This means that we're also going to need to configure a UsersConnectionRepository bean. Here's one configured using Spring's Java configuration style:

@Configuration
public class UsersConnectionRepositoryConfig {

    @Bean
    public UsersConnectionRepository usersConnectionRepository(DataSource dataSource,
            ConnectionFactoryLocator connectionFactoryLocator, TextEncryptor textEncryptor) {
        return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, textEncryptor);
    }
	
}
		

JdbcUsersConnectionRepository is created with a references to a DataSource, a ConnectionFactoryLocator, and a text encryptor. It will use the DataSource to access the RDBMS when persisting and restoring connections. When restoring connections, it will use the ConnectionFactoryLocator to locate ConnectionFactory instances.

JdbcUsersConnectionRepository uses a TextEncryptor to encrypt the credentials (e.g., access tokens and secrets) obtained during authorization when writing them to the database. Spring Security 3.1 makes a few useful text encryptors available via static factory methods in its Encryptors class. For example, a no-op text encryptor is useful at development time and can be configured like this:

@Configuration
@Profile("dev")
public class DevEncryptionConfig {

    @Bean
    public TextEncryptor textEncryptor() {
        return Encryptors.noOpText();
    }

}
		

Notice that this configuration class is annotated with @Profile("dev"). Spring 3.1 introduced the profile concept where certain beans will only be created when certain profiles are active. Here, the @Profile annotation ensures that this TextEncryptor will only be created when "dev" is an active profile. For production-time purposes, a stronger text encryptor is recommended and can be created when the "production" profile is active:

@Configuration
@Profile("production")
public class ProductionEncryptionConfig {

    @Bean
    public TextEncryptor textEncryptor(@Value("${security.encryptPassword}") String password,
            @Value("${security.encryptSalt}") String salt) {
        return Encryptors.text(password, salt);
    }

}
		

4.2.1 Configuring connection support in XML

Up to this point, the connection support configuration has been done using Spring's Java-based configuration style. But you can configure it in either Java configuration or XML. Here's the XML equivalent of the ConnectionFactoryRegistry configuration:

<bean id="connectionFactoryLocator" class="org.springframework.social.connect.support.ConnectionFactoryRegistry">
    <property name="connectionFactories">
        <list>
            <bean class="org.springframework.social.twitter.connect.TwitterConnectionFactory">
                <constructor-arg value="${twitter.consumerKey}" />
                <constructor-arg value="${twitter.consumerSecret}" />				
            </bean>
            <bean class="org.springframework.social.facebook.connect.FacebookConnectionFactory">
                <constructor-arg value="${facebook.appId}" />
                <constructor-arg value="${facebook.appSecret}" />				
            </bean>
            <bean class="org.springframework.social.tripit.connect.TripItConnectionFactory">
                <constructor-arg value="${tripit.consumerKey}" />
                <constructor-arg value="${tripit.consumerSecret}" />				
            </bean>
        </list>
    </property>
</bean>
			

This is functionally equivalent to the Java-based configuration of ConnectionFactoryRegistry shown before. The only casual difference is that the connection factories are injected as a list into the connectionFactories property rather than with the addConnectionFactory() method.

Here's an XML equivalent of the JdbcUsersConnectionRepository and ConnectionRepository configurations shown before:

<bean id="usersConnectionRepository" class="org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository">
    <constructor-arg ref="dataSource" />
    <constructor-arg ref="connectionFactoryLocator" />
    <constructor-arg ref="textEncryptor" />
</bean>

<bean id="connectionRepository" factory-method="createConnectionRepository" factory-bean="usersConnectionRepository" scope="request">
    <constructor-arg value="#{request.userPrincipal.name}" />
</bean>
			

Likewise, here is the equivalent configuration of the TextEncryptor beans:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">

    <beans profile="dev">
        <bean id="textEncryptor" class="org.springframework.security.crypto.encrypt.Encryptors" factory-method="noOpText" />
    </beans>
	
    <beans profile="production">
        <bean id="textEncryptor" class="org.springframework.security.crypto.encrypt.Encryptors" factory-method="text">
            <constructor-arg value="${security.encryptPassword}" />
            <constructor-arg value="${security.encryptSalt}" />
        </bean>
    </beans>

</beans>
			

Just like the Java-based configuration, profiles are used to select which of the text encryptors will be created.

4.3 Creating connections with ConnectController

With ConnectController's dependencies configured, ConnectController will be able to coordinate the connection process for the service providers whose connection factories are registered with ConnectionFactoryRegistry. ConnectController is a Spring MVC controller and can be configured as a bean in your application's Spring MVC configuration as follows:

@Configuration
public class ConnectControllerConfig {

    @Bean
    public ConnectController connectController(@Value("${application.secureUrl}") String applicationUrl,
            ConnectionFactoryLocator connectionFactoryLocator, Provider<ConnectionRepository> connectionRepositoryProvider) {
        return new ConnectController(applicationUrl, connectionFactoryLocator, connectionRepositoryProvider);
    }
    
}
		

Or, if you prefer Spring's XML-based configuration, then you can configure ConnectController like this:

<bean class="org.springframework.social.connect.web.ConnectController">
    <constructor-arg value="${application.secureUrl}" />
    <!-- relies on by-type autowiring for the other constructor-args -->
</bean>
		

In either case, ConnectController is constructed with the base URL for the application. ConnectController will use this URL to construct callback URLs used in the authorization flow. Since the base URL of an application will be different between environments, it is recommended that you externalize it. Here the URL is specified as a placeholder variable.

ConnectController supports authorization flows for either OAuth 1 or OAuth 2, relying on OAuth1Operations or OAuth2Operations to handle the specifics for each protocol. ConnectController will obtain the appropriate OAuth operations interface from one of the provider connection factories registered with ConnectionFactoryRegistry. It will select a specific ConnectionFactory to use by matching the connection factory's ID with the URL path. The path pattern that ConnectController handles is "/connect/{providerId}". Therefore, if ConnectController is handling a request for "/connect/twitter", then the ConnectionFactory whose getProviderId() returns "twitter" will be used. (As configured in the previous section, TwitterConnectionFactory will be chosen.)

The flow that ConnectController follows is slightly different, depending on which authorization protocol is supported by the service provider. For OAuth 2-based providers, the flow is as follows:

  • GET /connect/{providerId} - Displays a web page showing connection status to the provider.

  • POST /connect/{providerId} - Initiates the connection flow with the provider.

  • GET /connect/{providerId}?code={code} - Receives the authorization callback from the provider, accepting an authorization code. Uses the code to request an access token and complete the connection.

  • DELETE /connect/{providerId} - Severs a connection with the provider.

For an OAuth 1 provider, the flow is very similar, with only a subtle difference in how the callback is handled:

  • GET /connect/{providerId} - Displays a web page showing connection status to the provider.

  • POST /connect/{providerId} - Initiates the connection flow with the provider.

  • GET /connect/{providerId}?oauth_token={request token}&oauth_verifier={verifier} - Receives the authorization callback from the provider, accepting a verification code. Exchanges this verification code along with the request token for an access token and completes the connection. The oauth_verifier parameter is optional and is only used for providers implementing OAuth 1.0a.

  • DELETE /connect/{providerId} - Severs a connection with the provider.

4.3.1 Displaying a connection page

Before the connection flow starts in earnest, a web application may choose to show a page that offers the user information on their connection status. This page would offer them the opportunity to create a connection between their account and their social profile. ConnectController can display such a page if the browser navigates to /connect/{provider}.

For example, to display a connection status page for Twitter, where the provider name is "twitter", your application should provide a link similar to this:

<a href="<c:url value="/connect/twitter" />">Connect to Twitter</a>
			

ConnectController will respond to this request by first checking to see if a connection already exists between the user's account and Twitter. If not, then it will with a view that should offer the user an opportunity to create the connection. Otherwise, it will respond with a view to inform the user that a connection already exists.

The view names that ConnectController responds with are based on the provider's name. In this case, since the provider name is "twitter", the view names are "connect/twitterConnect" and "connect/twitterConnected".

4.3.2 Initiating the connection flow

To kick off the connection flow, the application should POST to /connect/{providerId}. Continuing with the Twitter example, a JSP view resolved from "connect/twitterConnect" might include the following form:

<form action="<c:url value="/connect/twitter" />" method="POST">
    <p>You haven't created any connections with Twitter yet. Click the button to create
       a connection between your account and your Twitter profile. 
       (You'll be redirected to Twitter where you'll be asked to authorize the connection.)</p>
    <p><button type="submit"><img src="<c:url value="/resources/social/twitter/signin.png" />"/></button></p>
</form>
			

When ConnectController handles the request, it will redirect the browser to the provider's authorization page. In the case of an OAuth 1 provider, it will first fetch a request token from the provider and pass it along as a parameter to the authorization page. Request tokens aren't used in OAuth 2, however, so instead it passes the application's client ID and redirect URI as parameters to the authorization page.

For example, Twitter's authorization URL has the following pattern:

https://twitter.com/oauth/authorize?oauth_token={token}

If the application's request token were "vPyVSe"[1], then the browser would be redirected to https://twitter.com/oauth/authorize?oauth_token=vPyVSe and a page similar to the following would be displayed to the user (from Twitter)[2]:

In contrast, Facebook is an OAuth 2 provider, so its authorization URL takes a slightly different pattern:

https://graph.facebook.com/oauth/authorize?client_id={clientId}&redirect_uri={redirectUri}

Thus, if the application's Facebook client ID is "0b754" and it's redirect URI is "http://www.mycoolapp.com/connect/facebook", then the browser would be redirected to https://graph.facebook.com/oauth/authorize?client_id=0b754&redirect_uri=http://www.mycoolapp.com/connect/facebook and Facebook would display the following authorization page to the user:

If the user clicks the "Allow" button to authorize access, the provider will redirect the browser back to the authorization callback URL where ConnectController will be waiting to complete the connection.

The behavior varies from provider to provider when the user denies the authorization. For instance, Twitter will simply show a page telling the user that they denied the application access and does not redirect back to the application's callback URL. Facebook, on the other hand, will redirect back to the callback URL with error information as request parameters.

Authorization scope

In the previous example of authorizing an application to interact with a user's Facebook profile, you notice that the application is only requesting access to the user's basic profile information. But there's much more that an application can do on behalf of a user with Facebook than simply harvest their profile data. For example, how can an application gain authorization to post to a user's Facebook wall?

OAuth 2 authorization may optionally include a scope parameter that indicates the type of authorization being requested. On the provider, the "scope" parameter should be passed along to the authorization URL. In the case of Facebook, that means that the Facebook authorization URL pattern should be as follows:

https://graph.facebook.com/oauth/authorize?client_id={clientId}&redirect_uri={redirectUri}&scope={scope}

ConnectController accepts a "scope" parameter at authorization and passes its value along to the provider's authorization URL. For example, to request permission to post to a user's Facebook wall, the connect form might look like this:

<form action="<c:url value="/connect/twitter" />" method="POST">
    <input type="hidden" name="scope" value="publish_stream,offline_access" />
    <p>You haven't created any connections with Twitter yet. Click the button to create
       a connection between your account and your Twitter profile. 
       (You'll be redirected to Twitter where you'll be asked to authorize the connection.)</p>
    <p><button type="submit"><img src="<c:url value="/resources/social/twitter/signin.png" />"/></button></p>
</form>
				

The hidden "scope" field contains the scope values to be passed along in the scope> parameter to Facebook's authorization URL. In this case, "publish_stream" requests permission to post to a user's wall. In addition, "offline_access" requests permission to access Facebook on behalf of a user even when the user isn't using the application.

[Note]Note

OAuth 2 access tokens typically expire after some period of time. Per the OAuth 2 specification, an application may continue accessing a provider after a token expires by using a refresh token to either renew an expired access token or receive a new access token (all without troubling the user to re-authorize the application).

Facebook does not currently support refresh tokens. Moreover, Facebook access tokens expire after about 2 hours. So, to avoid having to ask your users to re-authorize ever 2 hours, the best way to keep a long-lived access token is to request "offline_access".

When asking for "publish_stream,offline_access" authorization, the user will be prompted with the following authorization page from Facebook:

Scope values are provider-specific, so check with the service provider's documentation for the available scopes. Facebook scopes are documented at http://developers.facebook.com/docs/authentication/permissions.

4.3.3 Responding to the authorization callback

After the user agrees to allow the application have access to their profile on the provider, the provider will redirect their browser back to the application's authorization URL with a code that can be exchanged for an access token. For OAuth 1.0a providers, the callback URL is expected to receive the code (known as a verifier in OAuth 1 terms) in an oauth_verifier parameter. For OAuth 2, the code will be in a code parameter.

ConnectController will handle the callback request and trade in the verifier/code for an access token. Once the access token has been received, the OAuth dance is complete and the application may use the access token to interact with the provider on behalf of the user. The last thing that ConnectController does is to hand off the access token to the ServiceProvider implementation to be stored for future use.

4.3.4 Disconnecting

To delete a connection via ConnectController, submit a DELETE request to "/connect/{provider}".

In order to support this through a form in a web browser, you'll need to have Spring's HiddenHttpMethodFilter configured in your application's web.xml. Then you can provide a disconnect button via a form like this:

<form action="<c:url value="/connect/twitter" />" method="post">
   <div class="formInfo">
      <p>Spring Social Showcase is connected to your Twitter account.
         Click the button if you wish to disconnect.</p>
   </div>
   <button type="submit">Disconnect</button>	
   <input type="hidden" name="_method" value="delete" />
</form>
			

When this form is submitted, ConnectController will disconnect the user's account from the provider. It does this by calling the disconnect() method on each of the Connections returned by the provider's getConnections() method.

4.4 Connection interceptors

In the course of creating a connection with a service provider, you may want to inject additional functionality into the connection flow. For instance, perhaps you'd like to automatically post a tweet to a user's Twitter timeline immediately upon creating the connection.

ConnectController may be configured with one or more connection interceptors that it will call at points in the connection flow. These interceptors are defined by the ConnectInterceptor interface:

public interface ConnectInterceptor<A> {
	
    void preConnect(ConnectionFactory<A> connectionFactory, WebRequest request);

    void postConnect(Connection<A> connection, WebRequest request);
	
}
		

The preConnect() method will be called by ConnectController just before redirecting the browser to the provider's authorization page. postConnect() will be called immediately after a connection has been persisted linking the user's local account with the provider profile.

For example, suppose that after connecting a user account with their Twitter profile , you want to immediately post a tweet about that connection to the user's Twitter timeline. To accomplish that, you might write the following connection interceptor:

public class TweetAfterConnectInterceptor implements ConnectInterceptor<TwitterApi> {

    public void preConnect(ConnectionFactory<TwitterApi> provider, WebRequest request) {
        // nothing to do
    }

    public void postConnect(Connection<TwitterApi> connection, WebRequest request) {
        connection.updateStatus("I've connected with the Spring Social Showcase!");
    }
}
		

This interceptor can then be injected into ConnectController when it is created:

@Bean
public ConnectController connectController(@Value("${application.secureUrl}") applicationUrl,
        ConnectionFactoryLocator connectionFactoryLocator, Provider<ConnectionRepository> connectionRepositoryProvider) {
    ConnectController controller = new ConnectController(applicationUrl, connectionFactoryLocator, connectionRepositoryProvider);
    controller.addInterceptor(new TweetAfterConnectInterceptor());
    return controller;
}
		

Or, as configured in XML:

<bean class="org.springframework.social.connect.web.ConnectController">
    <constructor-arg value="${application.secureUrl}" />
    <property name="interceptors">
        <list>
            <bean class="org.springframework.social.showcase.twitter.TweetAfterConnectInterceptor" />
        </list>
    </property>
</bean>
		

Note that the interceptors property is a list and can take as many interceptors as you'd like to wire into it. When it comes time for ConnectController to call into the interceptors, it will only invoke the interceptor methods for those interceptors whose service operations type matches the service provider's operations type. In the example given here, only connections made through a service provider whose operation type is TwitterApi will trigger the interceptor's methods.



[1] This is just an example. Actual request tokens are typically much longer.

[2] If the user has not yet signed into Twitter, the authorization page will also include a username and password field for authentication into Twitter.