The spring-social-core
module includes a Service Provider 'Connect' Framework for managing connections to Software-as-a-Service (SaaS) providers such as Facebook and Twitter.
This framework allows your application to establish connections between local user accounts and accounts those users have with external service providers.
Once a connection is established, it can be be used to obtain a strongly-typed Java binding to the ServiceProvider's API, giving your application the ability to invoke the API on behalf of a user.
To illustrate, consider Facebook as an example ServiceProvider. Suppose your application, AcmeApp, allows users to share content with their Facebook friends. To support this, a connection needs to be established between a user's AcmeApp account and her Facebook account. Once established, a Facebook instance can be obtained and used to post content to the user's wall. Spring Social's 'Connect' framework provides a clean API for managing service provider connections such as this.
The Connection<A>
interface models a connection to an external service provider such as Facebook:
public interface Connection<A> { ConnectionKey getKey(); String getDisplayName(); String getProfileUrl(); String getImageUrl(); void sync(); boolean test(); boolean hasExpired(); void refresh(); UserProfile fetchUserProfile(); void updateStatus(String message); A getApi(); ConnectionData createData(); }
Each connection is uniquely identified by a composite key consisting of a providerId (e.g. 'facebook') and connected providerUserId (e.g. '1255689239', for Keith Donald's Facebook ID). This key tells you what provider user the connection is connected to.
A connection has a number of meta-properties that can be used to render it on a screen, including a displayName, profileUrl, and imageUrl. As an example, the following HTML template snippet could be used to generate a link to the connected user's profile on the provider's site:
<img src="${connection.imageUrl}" /> <a href="${connection.profileUrl}">${connection.displayName}</a>
The value of these properties may depend on the state of the provider user's profile. In this case, sync() can be called to synchronize these values if the user's profile is updated.
A connection can be tested to determine if its authorization credentials are valid. If invalid, the connection may have expired or been revoked by the provider. If the connection has expired, a connection may be refreshed to renew its authorization credentials.
A connection provides several operations that allow the client application to invoke the ServiceProvider's API in a uniform way. This includes the ability to fetch a model of the user's profile and update the user's status in the provider's system.
A connection's parameterized type <A> represents the Java binding to the ServiceProvider's native API.
An instance of this API binding can be obtained by calling getApi()
.
As an example, a Facebook connection instance would be parameterized as Connection<Facebook>.
getApi()
would return a Facebook instance that provides a Java binding to Facebook's graph API for a specific Facebook user.
Finally, the internal state of a connection can be captured for transfer between layers of your application by calling createData()
.
This could be used to persist the connection in a database, or serialize it over the network.
To put this model into action, suppose we have a reference to a Connection<Twitter> instance. Suppose the connected user is the Twitter user with screen name 'kdonald'.
Connection#getKey() would return ('twitter', '14718006') where '14718006' is @kdonald's Twitter-assigned user id that never changes.
Connection#getDisplayName() would return '@kdonald'.
Connection#getProfileUrl() would return 'http://twitter.com/kdonald'.
Connection#getImageUrl() would return 'http://a0.twimg.com/profile_images/105951287/IMG_5863_2_normal.jpg'.
Connection#sync() would synchronize the state of the connection with @kdonald's profile.
Connection#test() would return true indicating the authorization credentials associated with the Twitter connection are valid. This assumes Twitter has not revoked the AcmeApp client application, and @kdonald has not reset his authorization credentials (Twitter connections do not expire).
Connection#hasExpired() would return false.
Connection#refresh() would not do anything since connections to Twitter do not expire.
Connection#fetchUserProfile() would make a remote API call to Twitter to get @kdonald's profile data and normalize it into a UserProfile model.
Connection#updateStatus(String) would post a status update to @kdonald's timeline.
Connection#getApi() would return a Twitter giving the client application access to the full capabilities of Twitter's native API.
Connection#createData() would return ConnectionData that could be serialized and used to restore the connection at a later time.
So far we have discussed how existing connections are modeled, but we have not yet discussed how new connections are established. The manner in which connections between local users and provider users are established varies based on the authorization protocol used by the ServiceProvider. Some service providers use OAuth, others use Basic Auth, others may use something else. Spring Social currently provides native support for OAuth-based service providers, including support for OAuth 1 and OAuth 2. This covers the leading social networks, such as Facebook and Twitter, all of which use OAuth to secure their APIs. Support for other authorization protocols can be added by extending the framework.
Each authorization protocol is treated as an implementation detail where protocol-specifics are kept out of the core Connection API. A ConnectionFactory abstraction encapsulates the construction of connections that use a specific authorization protocol. In the following sections, we will discuss the major ConnectionFactory classes provided by the framework. Each section will also describe the protocol-specific flow required to establish a new connection.
OAuth 2 is rapidly becoming a preferred authorization protocol, and is used by major service providers such as Facebook, Github, Foursquare, and 37signals. In Spring Social, a OAuth2ConnectionFactory is used to establish connections with a OAuth2-based service provider:
public class OAuth2ConnectionFactory<A> extends ConnectionFactory<A> { public OAuth2Operations getOAuthOperations(); public Connection<A> createConnection(AccessGrant accessGrant); public Connection<A> createConnection(ConnectionData data); }
getOAuthOperations()
returns an API to use to conduct the authorization flow, or "OAuth Dance", with a service provider.
The result of this flow is an AccessGrant
that can be used to establish a connection with a local user account by calling createConnection
.
The OAuth2Operations interface is shown below:
public interface OAuth2Operations { String buildAuthorizeUrl(GrantType grantType, OAuth2Parameters parameters); String buildAuthenticateUrl(GrantType grantType, OAuth2Parameters parameters); AccessGrant exchangeForAccess(String authorizationCode, String redirectUri, MultiValueMap<String, String> additionalParameters); AccessGrant refreshAccess(String refreshToken, String scope, MultiValueMap<String, String> additionalParameters); }
Callers are first expected to call buildAuthorizeUrl(GrantType, OAuth2Parameters) to construct the URL to redirect the user to for connection authorization. Upon user authorization, the authorizationCode returned by the provider should be exchanged for an AccessGrant. The AccessGrant should then used to create a connection. This flow is illustrated below:
As you can see, there is a back-and-forth conversation that takes place between the application and the service provider to grant the application access to the provider account. This exchange, commonly known as the "OAuth Dance", follows these steps:
The flow starts by the application redirecting the user to the provider's authorization URL. Here the provider displays a web page asking the user if he or she wishes to grant the application access to read and update their data.
The user agrees to grant the application access.
The service provider redirects the user back to the application (via the redirect URI), passing an authorization code as a parameter.
The application exchanges the authorization code for an access grant.
The service provider issues the access grant to the application. The grant includes an access token and a refresh token. One receipt of these tokens, the "OAuth dance" is complete.
The application uses the AccessGrant to establish a connection between the local user account and the external provider account. With the connection established, the application can now obtain a reference to the Service API and invoke the provider on behalf of the user.
The example code below shows use of a FacebookConnectionFactory to create a connection to Facebook using the OAuth2 server-side flow illustrated above. Here, FacebookConnectionFactory is a subclass of OAuth2ConnectionFactory:
FacebookConnectionFactory connectionFactory = new FacebookConnectionFactory("clientId", "clientSecret"); OAuth2Operations oauthOperations = connectionFactory.getOAuthOperations(); OAuth2Parameters params = new OAuth2Parameters(); params.setRedirectUri("https://my-callback-url"); String authorizeUrl = oauthOperations.buildAuthorizeUrl(GrantType.AUTHORIZATION_CODE, params); response.sendRedirect(authorizeUrl); // upon receiving the callback from the provider: AccessGrant accessGrant = oauthOperations.exchangeForAccess(authorizationCode, "https://my-callback-url", null); Connection<Facebook> connection = connectionFactory.createConnection(accessGrant);
The following example illustrates the client-side "implicit" authorization flow also supported by OAuth2. The difference between this flow and the server-side "authorization code" flow above is the provider callback directly contains the access grant (no additional exchange is necessary). This flow is appropriate for clients incapable of keeping the access grant credentials confidential, such as a mobile device or JavaScript-based user agent.
FacebookConnectionFactory connectionFactory = new FacebookConnectionFactory("clientId", "clientSecret"); OAuth2Operations oauthOperations = connectionFactory.getOAuthOperations(); OAuth2Parameters params = new OAuth2Parameters(); params.setRedirectUri("https://my-callback-url"); String authorizeUrl = oauthOperations.buildAuthorizeUrl(GrantType.IMPLICIT_GRANT, params); response.sendRedirect(authorizeUrl); // upon receiving the callback from the provider: AccessGrant accessGrant = new AccessGrant(accessToken); Connection<Facebook> connection = connectionFactory.createConnection(accessGrant);
public class OAuth1ConnectionFactory<A> extends ConnectionFactory<A> { public OAuth1Operations getOAuthOperations(); public Connection<A> createConnection(OAuthToken accessToken); public Connection<A> createConnection(ConnectionData data); }
Like a OAuth2-based provider, getOAuthOperations()
returns an API to use to conduct the authorization flow, or "OAuth Dance".
The result of the OAuth 1 flow is an OAuthToken
that can be used to establish a connection with a local user account by calling createConnection
.
The OAuth1Operations interface is shown below:
public interface OAuth1Operations { OAuthToken fetchRequestToken(String callbackUrl, MultiValueMap<String, String> additionalParameters); String buildAuthorizeUrl(String requestToken, OAuth1Parameters parameters); String buildAuthenticateUrl(String requestToken, OAuth1Parameters parameters); OAuthToken exchangeForAccessToken(AuthorizedRequestToken requestToken, MultiValueMap<String, String> additionalParameters); }
Callers are first expected to call fetchNewRequestToken(String) to obtain a temporary token from the ServiceProvider to use during the authorization session. Next, callers should call buildAuthorizeUrl(String, OAuth1Parameters) to construct the URL to redirect the user to for connection authorization. Upon user authorization, the authorized request token returned by the provider should be exchanged for an access token. The access token should then used to create a connection. This flow is illustrated below:
The flow starts with the application asking for a request token. The purpose of the request token is to obtain user approval and it can only be used to obtain an access token. In OAuth 1.0a, the consumer callback URL is passed to the provider when asking for a request token.
The service provider issues a request token to the consumer.
The application redirects the user to the provider's authorization page, passing the request token as a parameter. In OAuth 1.0, the callback URL is also passed as a parameter in this step.
The service provider prompts the user to authorize the consumer application and the user agrees.
The service provider redirects the user's browser back to the application (via the callback URL). In OAuth 1.0a, this redirect includes a verifier code as a parameter. At this point, the request token is authorized.
The application exchanges the authorized request token (including the verifier in OAuth 1.0a) for an access token.
The service provider issues an access token to the consumer. The "dance" is now complete.
The application uses the access token to establish a connection between the local user account and the external provider account. With the connection established, the application can now obtain a reference to the Service API and invoke the provider on behalf of the user.
The example code below shows use of a TwitterConnectionFactory to create a connection to Facebook using the OAuth1 server-side flow illustrated above. Here, TwitterConnectionFactory is a subclass of OAuth1ConnectionFactory:
TwitterConnectionFactory connectionFactory = new TwitterConnectionFactory("consumerKey", "consumerSecret"); OAuth1Operations oauthOperations = connectionFactory.getOAuthOperations(); OAuthToken requestToken = oauthOperations.fetchRequestToken("https://my-callback-url", null); String authorizeUrl = oauthOperations.buildAuthorizeUrl(requestToken, OAuth1Parameters.NONE); response.sendRedirect(authorizeUrl); // upon receiving the callback from the provider: OAuthToken accessToken = oauthOperations.exchangeForAccessToken( new AuthorizedRequestToken(requestToken, oauthVerifier), null); Connection<Twitter> connection = connectionFactory.createConnection(accessToken);
As you will see in subsequent sections of this reference guide, Spring Social provides infrastructure for establishing connections to one or more providers in a dynamic, self-service manner. For example, one client application may allow users to connect to Facebook, Twitter, and LinkedIn. Another might integrate Github and Pivotal Tracker. To make the set of connectable providers easy to manage and locate, Spring Social provides a registry for centralizing connection factory instances:
ConnectionFactoryRegistry registry = new ConnectionFactoryRegistry(); registry.addConnectionFactory(new FacebookConnectionFactory("clientId", "clientSecret")); registry.addConnectionFactory(new TwitterConnectionFactory("consumerKey", "consumerSecret")); registry.addConnectionFactory(new LinkedInConnectionFactory("consumerKey", "consumerSecret"));
This registry implements a locator interface that other objects can use to lookup connection factories dynamically:
public interface ConnectionFactoryLocator { ConnectionFactory<?> getConnectionFactory(String providerId); <A> ConnectionFactory<A> getConnectionFactory(Class<A> apiType); Set<String> registeredProviderIds(); }
Example usage of a ConnectionFactoryLocator is shown below:
// generic lookup by providerId ConnectionFactory<?> connectionFactory = locator.getConnectionFactory("facebook"); // typed lookup by service api type ConnectionFactory<Facebook> connectionFactory = locator.getConnectionFactory(Facebook.class);
After a connection has been established, you may wish to persist it for later use. This makes things convenient for the user since a connection can simply be restored from its persistent form and does not need to be established again. Spring Social provides a ConnectionRepository interface for managing the persistence of a user's connections:
public interface ConnectionRepository { MultiValueMap<String, Connection<?>> findAllConnections(); List<Connection<?>> findConnections(String providerId); <A> List<Connection<A>> findConnections(Class<A> apiType); MultiValueMap<String, Connection<?>> findConnectionsToUsers( MultiValueMap<String, String> providerUserIds); Connection<?> getConnection(ConnectionKey connectionKey); <A> Connection<A> getConnection(Class<A> apiType, String providerUserId); <A> Connection<A> getPrimaryConnection(Class<A> apiType); <A> Connection<A> findPrimaryConnection(Class<A> apiType); void addConnection(Connection<?> connection); void updateConnection(Connection<?> connection); void removeConnections(String providerId); void removeConnection(ConnectionKey connectionKey); }
As you can see, this interface provides a number of operations for adding, updating, removing, and finding Connections. Consult the JavaDoc API of this interface for a full description of these operations. Note that all operations on this repository are scoped relative to the "current user" that has authenticated with your local application. For standalone, desktop, or mobile environments that only have one user this distinction isn't important. In a multi-user web application environment, this implies ConnectionRepository instances will be request-scoped.
For multi-user environments, Spring Social provides a UsersConnectionRepository that provides access to the global store of connections across all users:
public interface UsersConnectionRepository { List<String> findUserIdsWithConnection(Connection<?> connection); Set<String> findUserIdsConnectedTo(String providerId, Set<String> providerUserIds); ConnectionRepository createConnectionRepository(String userId); }
As you can see, this repository acts as a factory for ConnectionRepository instances scoped to a single user, as well as exposes a number of multi-user operations. These operations include the ability to lookup the local userIds associated with connections to support provider user sign-in and "registered friends" scenarios. Consult the JavaDoc API of this interface for a full description.
Spring Social provides a JdbcUsersConnectionRepository implementation capable of persisting connections to a RDBMS. The database schema designed to back this repository is defined as follows:
create table UserConnection (userId varchar(255) not null, providerId varchar(255) not null, providerUserId varchar(255), rank int not null, displayName varchar(255), profileUrl varchar(512), imageUrl varchar(512), accessToken varchar(255) not null, secret varchar(255), refreshToken varchar(255), expireTime bigint, primary key (userId, providerId, providerUserId)); create unique index UserConnectionRank on UserConnection(userId, providerId, rank);
For convenience is bootstrapping the schema from a running application, this schema definition is available in the spring-social-core
module as a resource at the path /org/springframework/social/connect/jdbc/JdbcUsersConnectionRepository.sql.
Note that although this schema was designed with compatibility in mind, it may not be compatible with all databases.
You may need to adapt this schema definition to accommodate any peculiarities of your chosen database.
The implementation also provides support for encrypting authorization credentials so they are not stored in plain-text.
The example code below demonstrates construction and usage of a JdbcUsersConnectionRepository:
// JDBC DataSource pointing to the DB where connection data is stored DataSource dataSource = ...; // locator for factories needed to construct Connections when restoring from persistent form ConnectionFactoryLocator connectionFactoryLocator = ...; // encryptor of connection authorization credentials TextEncryptor encryptor = ...; UsersConnectionRepository usersConnectionRepository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, encryptor); // create a connection repository for the single-user 'kdonald' ConnectionRepository repository = usersConnectionRepository.createConnectionRepository("kdonald"); // find kdonald's primary Facebook connection Connection<Facebook> connection = repository.findPrimaryConnection(Facebook.class);