Harmonic Development

RSS
Dec 1

Adding Spring Social to a Spring MVC and Spring Security Web App, Part 2

In Part I of this series, we covered adding the jars/dependencies needed to start using Spring Social in a Spring MVC and Spring Security Web application. We also covered the classes involved in persisting users’ social connection information.

Now we’ll configure our Spring Social related beans. Then we’ll start modifying the MVC configuration and classes to start making Spring Social functionality available to users.

Configuring Spring Social Beans

There are several beans you will need to configure, either using Spring’s traditional XML configuration, or its more recently developed Java configuration. You can find some good example Java and XML configuration in the reference documentation. I’m just going to go over the beans you’ll need to configure:

  • ConnectionFactoryLocator - this is the central bean of Spring Social; you must register all provider specific connection factories with this bean
  • FacebookConnectionFactory, TwitterConnectionFactory, etc. - provider specific ConnectionFactory implementations; these take the OAuth consumer key and secret for your Facebook, Twitter, etc. applications; you should provide those key/secret values from a properties file using a PropertyPlaceholderConfigurer
  • UsersConnectionRepository - either the included JdbcUsersConnectionRepository or your own implementation
  • ConnectionRepository - the request scoped bean for logged in users created by UsersConnectionRepository.createConnectionRepository() (see the reference documentation for how to use Spring Expression Language to access the currently logged in user’s username)

You’ll want to configure these beans in a new XML file or @Configuration class, and add that file or class to your application context. (For example, you could add it to a “myapp-social.xml” file and add that file to the list of config files in your “contextConfigLocation” context-param value in your web.xml.)

Class to Support Signing In With Facebook/Twitter

Now we start on the meat of the integration between Spring Social and your Spring MVC and Spring Security app.

First we will work on making it possible for users to sign in using Facebook/Twitter/etc. Spring Social Web provides a controller, ProviderSignInController, and an interface, SignInAdapter, that allow you to tie the controller in to your application’s sign in process. The SignInAdapter implementation is what will talk to Spring Security to sign a user in to your application (complete with optional “remember-me” support).

When a user tries to sign in using Facebook/Twitter/etc., ProviderSignInController will check if there is an existing local user associated with the retrieved provider id and provider user id. If an associated local user is found, the controller will call the SignInAdapter implementation with the local user id, the Connection<?> instance, and a NativeWebRequest instance.

Here is my example SignInAdapter. Yours may be slightly different.

public class SignInAdapterImpl implements SignInAdapter {
  private UserService userService; // injected
  private TokenBasedRememberMeServices tokenBasedRememberMeServices; // injected

  public String signIn(String userId, Connection<?> connection, NativeWebRequest request) {
    User user = userService.findByLogin(userId);
    Authentication authentication = SecurityUtil.signInUser(user);
    // set remember-me cookie
    tokenBasedRememberMeServices.onLoginSuccess(
        (HttpServletRequest) request.getNativeRequest(),
        (HttpServletResponse) request.getNativeResponse(),
        authentication);
    return null;
  }

There are three basic steps being taken in this code:

  • look up your local user by the given local user id
  • log the user in to the Spring Security context
  • set a Spring Security remember-me cookie (this part is optional)

Here is the code from my SecurityUtil.signInUser() method. The basic idea is to create an Authentication instance and set it in the SecurityContext.

  public static Authentication signInUser(User user) {
    List<GrantedAuthority> authorities = UserDetailsServiceImpl.createAuthorities(user);
    SpringSecurityUser springSecurityUser = new SpringSecurityUser(user, authorities);
    Authentication authentication = new UsernamePasswordAuthenticationToken(springSecurityUser, user.getPassword(), authorities);
    SecurityContextHolder.getContext().setAuthentication(authentication);
    return authentication;
  }

If a local user associated with the provider id and provider user id cannot be found, users will be redirected to a configurable sign up URL. You can also specify a different URL using the return value of SignInAdapter.signIn() (I’m returning null, indicating that the controller should use its signInUrl property.)

Automatic Sign Up / Registration

If you would prefer new users who try to sign in with Facebook/Twitter to be registered automatically, rather than sent to a sign up URL, you will need to make sure that the ProviderSignInController always finds a local user account associated with the provider id and provider user id from a sign in request.

ProviderSignInController calls UserConnectionRepository.findUserIdsWithConnection() and passes in the Connection when checking for associated local users. Inside this method you will need to create a local user account if none is found for the connection.

If you are using the JdbcUsersConnectionRepository class provided by Spring Social, you need to implement the ConnectionSignUp interface. This interface is used in JdbcUsersConnectionRepository.findUserIdsWithConnection() when no user ids are found for a given Connection.

If you have your own UsersConnectionRepository implementation, you do not necessarily need to implement ConnectionSignUp. You just need to modify the code in your “findUserIdsWithConnection()” implementation. You could use ConnectionSignUp the same way that the JdbcUsersConnectionRepository does if you want to, though.

Either way, you will need to write some code that translates the Connection, and probably also the user’s profile info from the provider, into a local user object and persist the new local user. In the case of implementing ConnectionSignUp you would then return the local unique username of the new user.

The user profile can be retrieved from the provider by calling “fetchUserProfile()” on the Connection. Be aware that different providers return different profile data. Twitter, for example, does not return an email address. You may want to perform whatever validation you normally perform during a form-based sign-up to make sure that all the data required by your app are present. If you are implementing ConnectionSignUp and you cannot automatically register the user for whatever reason, you should return null, indicating that no new user was created.

If you do implement ConnectionSignUp you will likely want to declare that bean in your Spring Social Java or XML configuration.

Sign Up Controller Modification

If you need to handle the case when a user tries to sign in via Facebook/Twitter and the sign in fails (because no local user is found, and you either have not enabled automatic registration, or automatic registration failed), you will need to modify your sign up controller. You will need to add code like this to your GET request handler method:

// check for failed sign in/up from facebook/twitter
Connection<?> connection = ProviderSignInUtils.getConnection(request);
if (connection != null) {
  // user tried signing in/up with but they could not be signed in or signed up automatically

  // present the user with a meaningful error
  // errors.reject(“failedSocialSignIn”, “error message goes here”);

  UserProfile userProfile = connection.fetchUserProfile();
  // copy connection and profile data to your sign up form bean
  // validate the data and put errors in the model to display to the user
}

You will probably need to add WebRequest and BindingResult arguments to your GET request method handler.

You will also need to modify your POST request method handler, to complete the social sign in/up process:

// check for failed sign in/up from facebook/twitter
Connection<?> connection = ProviderSignInUtils.getConnection(request);
if (connection != null) {
  // finish social signup/login if there is one
  ProviderSignInUtils.handlePostSignUp(user.getUsername(), request);
}
// you can then either sign the user in immediately (e.g. using your SignInAdapter implementation)
// or send them to the success view of the registration process per normal

You will probably need to add a NativeWebRequest argument to your POST request method handler.

Next Time: Changing Web and Security Config, and the View

Whew! That was intense. The good news is you are almost done. Next time we will cover changing your Spring MVC config, your web.xml, and your Spring Security config. Then lastly we will cover changes to your views. And then you’ll be done. Hooray! Hang in there.

On to Part 3!