This recipe uses the Spring social project in order to use the OAuth2 protocol from a client perspective.
We won't create an OAuth2 Authentication Server (AS) here. We will establish connections to third-party Authentication servers (Yahoo!) to authenticate on our application. Our application will be acting as a Service Provider (SP).
We will use Spring social whose first role is to manage social connections transparently and to provide a facade to invoke the provider APIs (Yahoo! Finance) using Java objects.
<!– Spring Social Core –> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-core</artifactId> <version>1.1.0.RELEASE</version> </dependency> <!– Spring Social Web (login/signup controllers) –> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-web</artifactId> <version>1.1.0.RELEASE</version> </dependency>
<!– Spring Social Twitter –> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-twitter</artifactId> <version>1.1.0.RELEASE</version> </dependency> <!– Spring Social Facebook –> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-facebook</artifactId> <version>1.1.0.RELEASE</version> </dependency>
http
bean:<security:http create-session="stateless" entry-point-ref="authenticationEntryPoint" authentication-manager-ref="authenticationManager"> <security:custom-filter ref="basicAuthenticationFilter" after="BASIC_AUTH_FILTER" /> <security:csrf disabled="true"/> <security:intercept-url pattern="/signup" access="permitAll"/> ... <security:intercept-url pattern="/**" access="permitAll"/> </security:http>
With the following SocialUserConnectionRepositoryImpl
, we have created our own implementation of org.sfw.social.connect.ConnectionRepository
, which is a Spring Social
core
interface with methods to manage the social-users connections. The code is as follows:
@Transactional(propagation = Propagation.REQUIRED) @SuppressWarnings("unchecked") public class SocialUserConnectionRepositoryImpl implements ConnectionRepository { @Autowired private SocialUserRepository socialUserRepository; private final String userId; private final ConnectionFactoryLocator connectionFactoryLocator; private final TextEncryptor textEncryptor; public SocialUserConnectionRepositoryImpl(String userId, SocialUserRepository socialUserRepository, ConnectionFactoryLocator connectionFactoryLocator, TextEncryptor textEncryptor){ this.socialUserRepository = socialUserRepository; this.userId = userId; this.connectionFactoryLocator = connectionFactoryLocator; this.textEncryptor = textEncryptor; } ... public void addConnection(Connection<?> connection) { try { ConnectionData data = connection.createData(); int rank = socialUserRepository.getRank(userId, data.getProviderId()) ; socialUserRepository.create(userId, data.getProviderId(), data.getProviderUserId(), rank, data.getDisplayName(), data.getProfileUrl(), data.getImageUrl(), encrypt(data.getAccessToken()), encrypt(data.getSecret()), encrypt(data.getRefreshToken()), data.getExpireTime() ); } catch (DuplicateKeyException e) { throw new DuplicateConnectionException(connection.getKey()); } } ... public void removeConnections(String providerId) { socialUserRepository.delete(userId,providerId); } ... }
In reality, this custom implementation extends and adapts the work from https://github.com/mschipperheyn/spring-social-jpa published under a GNU GPL license.
SocialUserConnectionRepositoryImpl
makes use of a custom Spring Data JPA SocialUserRepository
interface whose definition is as follows:public interface SocialUserRepository { List<SocialUser> findUsersConnectedTo(String providerId); ... List<String> findUserIdsByProviderIdAndProviderUserIds( String providerId, Set<String> providerUserIds); ... List<SocialUser> getPrimary(String userId, String providerId); ... SocialUser findFirstByUserIdAndProviderId(String userId, String providerId); }
SocialUser
entity (social connections) that we have created. This Entity is the direct model of the UserConnection
SQL table that JdbcUsersConnectionRepository
would expect to find if we would use this implementation rather than ours. The SocialUser
definition is the following code:@Entity @Table(name="userconnection", uniqueConstraints = {@UniqueConstraint(columnNames = { ""userId", "providerId", "providerUserId" }), @UniqueConstraint(columnNames = { "userId", "providerId", "rank" })}) public class SocialUser { @Id @GeneratedValue private Integer id; @Column(name = "userId") private String userId; @Column(nullable = false) private String providerId; private String providerUserId; @Column(nullable = false) private int rank; private String displayName; private String profileUrl; private String imageUrl; @Lob @Column(nullable = false) private String accessToken; private String secret; private String refreshToken; private Long expireTime; private Date createDate = new Date(); //+ getters / setters ... }
SocialUserConnectionRepositoryImpl
is instantiated in a higher-level service layer: SocialUserServiceImpl
, which is an implementation of the Spring UsersConnectionRepository
interface. This implementation is created as follows:@Transactional(readOnly = true) public class SocialUserServiceImpl implements SocialUserService { @Autowired private SocialUserRepository socialUserRepository; @Autowired private ConnectionFactoryLocator connectionFactoryLocator; @Autowired private UserRepository userRepository; private TextEncryptor textEncryptor = Encryptors.noOpText(); public List<String> findUserIdsWithConnection(Connection<?> connection) { ConnectionKey key = connection.getKey(); return socialUserRepository. findUserIdsByProviderIdAndProviderUserId(key.getProviderId(), key.getProviderUserId()); } public Set<String> findUserIdsConnectedTo(String providerId, Set<String> providerUserIds) { return Sets.newHashSet(socialUserRepository.findUserIdsByProviderIdAndProviderUserIds(providerId, providerUserIds)); } public ConnectionRepository createConnectionRepository(String userId) { if (userId == null) { throw new IllegalArgumentException"("userId cannot be null""); } return new SocialUserConnectionRepositoryImpl( userId, socialUserRepository, connectionFactoryLocator, textEncryptor); } ... }
SocialUserServiceImpl
is registered in the cloudstreetmarket-api
Spring configuration file (dispatcher-context.xml
) as a factory-bean that has the capability to produce SocialUserConnectionRepositoryImpl
under a request-scope (for a specific social-user profile). The code is as follows:<bean id="usersConnectionRepository" class="edu.zc.csm.core.services.SocialUserServiceImpl"/> <bean id="connectionRepository" factory-method="createConnectionRepository" factory-bean="usersConnectionRepository" scope"="request"> <constructor-arg value="#{request.userPrincipal.name}"/> <aop:scoped-proxy proxy-target-class"="false"" /> </bean>
dispatcher-context.xml
file:<bean id="signInAdapter" class="edu.zc.csm.api.signin.SignInAdapterImpl"/> <bean id="connectionFactoryLocator" class="org.sfw.social.connect.support. ConnectionFactoryRegistry"> <property name="connectionFactories"> <list> <bean class"="org.sfw.social.yahoo.connect.YahooOAuth2ConnectionFactory""> <constructor-arg value="${yahoo.client.token}"/> <constructor-arg value="${yahoo.client.secret}" /> <constructor-arg value="${yahoo.signin.url}" /> </bean> </list> </property> </bean> <bean class="org.sfw.social.connect.web.ProviderSignInController"> <constructor-arg ref="connectionFactoryLocator"/> <constructor-arg ref="usersConnectionRepository"/> <constructor-arg ref="signInAdapter"/> <property name="signUpUrl" value="/signup"/> <property name="postSignInUrl" value="${frontend.home.page.url}"/> </bean>
SignInAdapterImpl
signs in a user in our application after the OAuth2 authentication. It performs what we want it to perform at this step from the application business point of view. The code is as follows:@Transactional(propagation = Propagation.REQUIRED) @PropertySource("classpath:application.properties") public class SignInAdapterImpl implements SignInAdapter{ @Autowired private UserRepository userRepository; @Autowired private CommunityService communityService; @Autowired private SocialUserRepository socialUserRepository; @Value("${oauth.success.view}") private String successView; public String signIn(String userId, Connection<?> connection, NativeWebRequest request) { User user = userRepository.findOne(userId); String view = null; if(user == null){ //temporary user for Spring Security //won't be persisted user = new User(userId, communityService.generatePassword(), null, true, true, true, true, communityService.createAuthorities(newRole[]{Role.ROLE_BASIC, Role.ROLE_OAUTH2})); } else{ //We have a successful previous oAuth //authentication //The user is already registered //Only the guid is sent back List<SocialUser> socialUsers = socialUserRepository. findByProviderUserIdOrUserId(userId, userId); if(CollectionUtils.isNotEmpty(socialUsers)){ //For now we only deal with Yahoo! view = successView.concat( "?spi=" + socialUsers.get(0) .getProviderUserId()); } } communityService.signInUser(user); return view; } }
connectionFactoryLocator
can also refer to more than one connection factories. In our case, we have only one: YahooOAuth2ConnectionFactory
. These classes are the entry points of social providers APIs (written for Java). We can normally find them on the web (from official sources or not) for the OAuth protocol we target (OAuth1, OAuth1.0a, and OAuth2).dispatcher-context.xml
configures a ProviderSignInController
, which is completely abstracted in Spring Social Core
. However, to register a OAuth2 user in our application (the first time the user visits the site), we have created a custom SignUpController
:@Controller @RequestMapping"("/signup"") @PropertySource"("classpath:application.properties"") public class SignUpController extends CloudstreetApiWCI{ @Autowired private CommunityService communityService; @Autowired private SignInAdapter signInAdapter; @Autowired private ConnectionRepository connectionRepository; @Value("${oauth.signup.success.view}") private String successView; @RequestMapping(method = RequestMethod.GET) public String getForm(NativeWebRequest request, @ModelAttribute User user) { String view = successView; // check if this is a new user signing in via //Spring Social Connection<?> connection = ProviderSignInUtils.getConnection(request); if (connection != null) { // populate new User from social connection //user profile UserProfile userProfile = connection.fetchUserProfile(); user.setUsername(userProfile.getUsername()); // finish social signup/login ProviderSignInUtils. handlePostSignUp(user.getUsername(), request); // sign the user in and send them to the user //home page signInAdapter.signIn(user.getUsername(), connection, request); view += ?spi=+ user.getUsername(); } return view; } }
cloudstreetmarket.com
server and specifically to the /api/signin/yahoo
handler with an authorization code as URL parameter.Cloudstreet Market
database there isn't any User
registered for the SocialUser
. This triggers the following popup and it should come back to the user until the account actually gets created:username: <marcus> email: <[email protected]> password: <123456> preferred currency: <USD>
Also, click on the user icon in order to upload a profile picture (if you wish). While doing so, make sure the property pictures.user.path
in cloudstreetmarket-api/src/main/resources/application.properties
is pointing to a created path on the filesystem.
We perform in this recipe a social integration within our application. An OAuth2 authentication involves a service provider (cloudstreetmarket.com) and an identity provider (Yahoo!).
This can only happen if a user owns (or is ready to own) an account on both parties. It is a very popular authentication protocol nowadays. As most of Internet users have at least one account in one of the main Social SaaS providers (Facebook, Twitter, LinkedIn, Yahoo!, and so on), this technique dramatically drops the registration time and login time spent on web service providers.
When the sign in with Yahoo! button is clicked by the user, a HTTP POST request is made to one of our API handler /api/signin/yahoo
. This handler corresponds to ProviderSignInController
, which is abstracted by Spring Social
.
ProviderSignInController
. This handler completes the connection recalling Yahoo! in order to exchange the authorization code against a refresh token and an access token. This operation is done transparently in the Spring Social
background.Spring Security
and redirected to the home page of the portal with the Yahoo! user-ID as request parameter (parameter named spi
).SignupController
where his connection is created and persisted. He is then authenticated in Spring Security and redirected to the portal's home page with the Yahoo! user ID as request parameter (named spi
).sessionStorage
(we have done all this).spi
identifier will be passed as a request header, until the user actually logs out or closes his browser.The Yahoo! APIs provide two ways of authenticating with OAuth2. This induces two different flows: the Explicit OAuth2 flow, suited for a server-side (web) application and the Implicit OAuth2 flow that particularly benefits to frontend web clients. We will focus on the implemented explicit flow here.
Here's a summary picture of the communication protocol between our application and Yahoo!. This is more or less a standard OAuth2 conversation:
The parameters marked with the *
symbol are optional in the communication. This flow is also detailed on the OAuth2 Yahoo! guide:
The difference between these two tokens must be understood. An access-token is used to identify the user (Yahoo! user) when performing operations on the Yahoo! API. As an example, below is a GET request that can be performed to retrieve the Yahoo! profile of a user identified by the Yahoo! ID abcdef123:
GET https://social.yahooapis.com/v1/user/abcdef123/profile Authorization: Bearer aXJUKynsTUXLVY
To provide identification to this call, the access-token must be passed in as the value of the Authorization
request header with the Bearer
keyword. In general, access-tokens have a very limited life (for Yahoo!, it is an hour).
A refresh-token is used to request new access-tokens. Refresh-tokens have much longer lives (for Yahoo!, they actually never expire, but they can be revoked).
The role of Spring social is to establish connections with Software-as-a-Service (SaaS) providers such as Facebook, Twitter, or Yahoo! Spring social is also responsible for invoking APIs on the application (Cloudstreet Market) server side on behalf of the users.
These two duties are both served in the spring-social-core dependency using the Connect Framework and the OAuth client support, respectively.
In short, Spring social is:
Connect Framework
handling the core authorization and connection flow with service providersConnect Controller
that handles the OAuth exchange between a service provider, consumer, and user in a web application environmentSign-in Controller
that allows users to authenticate in our application, signing in with their Saas provider accountThe Spring social core provides classes able to persist social connections in database using JDBC (especially with JdbcUsersConnectionRepository
). The module even embeds a SQL script for the schema definition:
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);
When an application (like ours) uses JPA, an Entity can be created to represent this table in the persistence context. We have created the SocialUser
Entity for this purpose in the sixth step of the recipe.
In this table Entity, you can see the following fields:
userId
: This field matches the @Id
(username) of the User
when the user is registered. If the user is not yet registered, userId
is the GUID (Yahoo! user ID, also called spi
on the web side)providerId
: This field is the lowercase name of the provider: Yahoo, Facebook or Twitter.providerUserId
: This field is the GUID, the unique identifier in the provider's system (Yahoo! user ID or spi.).accessToken, secret, refreshToken, and expireTime
: These are the OAuth2 tokens (credentials) for the connection and their related information.Two interfaces come with the framework:
ConnectionRepository
: This manages the persistence of one user connection. Implementations are request-scoped for the identified user.UsersConnectionRepository
: This provides access to the global store of connections across all users.If you remember, we created our own UsersConnectionRepository
implementation (SocialUserServiceImpl
). Registered in the dispatcher-servlet.xml
file, this implementation acts as a factory to produce request-scope connectionRepository
implementations (SocialUserConnectionRepositoryImpl
):
<bean id="connectionRepository" factory-method="createConnectionRepository" factory-bean="usersConnectionRepository" scop="request"> <constructor-arg value="#{request.userPrincipal.name}" /> <aop:scoped-proxy proxy-target-class="false" /> </bean> <bean id="usersConnectionRepository" class="edu.zc.csm.core.services.SocialUserServiceImpl"/>
Those two custom implementations both use the Spring Data JPA SocialUserRepository
that we have created for finding, updating, persisting, and removing connections.
In the SocialUserServiceImpl
implementation of the UsersConnectionRepository
interface, a ConnectionFactoryLocator
property is autowired
and a TextEncryptor
property is initialized with a default NoOpTextEncryptor
instance.
The default TextEncryptor
instance can be replaced with a proper encryption for the SocialUser data maintained in the database. Take a look at the spring-security-crypto module:
http://docs.spring.io/spring-security/site/docs/3.1.x/reference/crypto.html
The provider-specific configuration (Facebook, Twitter, Yahoo!) starts with definitions of the connectionFactoryLocator
bean.
The
connectionFactoryLocator
bean that we have defined in the dispatcher-servlet.xml
plays a central role in Spring Social. Its registration is as follows:
<bean id="connectionFactoryLocator" class="org.sfw.social.connect.support.ConnectionFactoryRegistry"> <property name="connectionFactories"> <list> <bean class"="org.sfw.social.yahoo.connect. YahooOAuth2ConnectionFactory""> <constructor-arg value="${yahoo.client.token}" /> <constructor-arg value="${yahoo.client.secret}" /> <constructor-arg value="${yahoo.signin.url}" /> </bean> </list> </property> </bean>
With this bean, Spring social implements a ServiceLocator
pattern that allows us to easily plug-in/plug-out new social connectors. Most importantly, it allows the system to resolve at runtime a provider-specific connector (a connectionFactory
).
The specified Type for our connectionFactoryLocator
is ConnectionFactoryRegistry
, which is a provided implementation of the ConnectionFactoryLocator
interface:
public interface ConnectionFactoryLocator { ConnectionFactory<?> getConnectionFactory(String providerId); <A> ConnectionFactory<A> getConnectionFactory(Class<A> apiType); Set<String> registeredProviderIds(); }
We have an example of the connectionFactory
lookup in the ProviderSignInController.signin
method:
ConnectionFactory<?> connectionFactory = connectionFactoryLocator.getConnectionFactory(providerId);
Here, the providerId
argument is a simple String (yahoo in our case).
The
ConnectionFactory
such as YahooOAuth2ConnectionFactory
are registered in ConnectionFactoryRegistry
with the OAuth2 consumer key and consumer secret, which identify (with authorization) our application on the provider's side.
We have developed the YahooOAuth2ConnectionFactory
class, but you should be able to find your ProviderSpecificConnectionFactory
either from official Spring Social
subprojects (spring-social-facebook
, spring-social-twitter
, and so on) or from open sources projects.
In order to perform the OAuth2 authentication steps, Spring social provides an abstracted Spring MVC Controller: ProviderSignInController
.
This controller performs the OAuth flows and establishes connections with the provider. It tries to find a previously established connection and uses the connected account to authenticate the user in the application.
If no previous connection matches, the flow is sent to the created SignUpController
matching the specific request mapping/signup
. The user is not automatically registered as a CloudStreetMarket User
at this point. We force the user to create his account manually via a Must-Register
response header when an API call appears OAuth2 authenticated without a bound local user. This Must-Register
response header triggers the create an account now popup on the client side (see in home_community_activity.js
, the loadMore
function).
It is during this registration that the connection (the SocialUser
Entity) is synchronized with the created User
Entity (see the CommunityController.createUser
method).
The
ProviderSignInController
works closely with a SignInAdapter
implementation (that we had to build as well) which actually authenticates the user into CloudStreetMarket with Spring Security. The authentication is triggered with the call to communityService.signInUser(user)
.
Here are the details of the method that creates the Authentication
object and stores it into the SecurityContext
:
@Override public Authentication signInUser(User user) { Authentication authentication = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); return authentication; }
We register and initialize a Spring bean for ProviderSigninController
with the following configuration:
<bean class"="org.sfw.social.connect.web.ProviderSignInController""> <constructor-arg ref="connectionFactoryLocator"/> <constructor-arg ref="usersConnectionRepository"/> <constructor-arg ref="signInAdapter"/> <property name="signUpUrl"" value"="/signup"/> <property name="postSignInUrl" value="${frontend.home.page.url}"/> </bean>
As you can see, we have specified the signUpUrl
request mapping to redirect to our custom SignupController
when no previous connection is found in database.
Alternatively, the specified postSignInUrl
allows the user to be redirected to the home page of the portal when the ProviderSignInController
resolves an existing connection to reuse.
Let's have a look at other features of Spring social.
In this recipe, we focused on presenting the OAuth2-client authentication process. In the next chapter, we will see how to use Spring social to perform requests to Yahoo! APIs on behalf of the users. We will see how can be used existing libraries in this purpose and how they work. In our case, we had to develop API connectors to Yahoo! financial APIs.
Spring social web provides another abstracted controller which allows social users to directly interact with their social connections to connect, disconnect, and obtain their connection status. The ConnectController
can also be used to build an interactive monitoring screen for managing connections to all the providers a site could possibly handle. Check out the Spring social reference for more details:
http://docs.spring.io/spring-social/docs/current/reference/htmlsingle/#connecting
This is a filter to be added to Spring Security so that a social authentication can be performed from the Spring Security filter-chain (and not externally as we did).
You will find a list of implemented connectors to Saas-providers from the main page of the project: http://projects.spring.io/spring-social