5. Signing in with Service Provider Accounts

5.1 Introduction

In order to ease sign in for their users, many applications allow sign in with a service provider such as Twitter or Facebook. With this authentication technique, the user signs into (or may already be signed into) his or her provider account. The application then tries to match that provider account to a local user account. If a match is found, the user is automatically signed into the application.

Spring Social supports such service provider-based authentication with ProviderSignInController from the spring-social-web module. ProviderSignInController works very much like ConnectController in that it goes through the OAuth flow (either OAuth 1 or OAuth 2, depending on the provider). Instead of creating a connection at the end of process, however, ProviderSignInController attempts to find a previously established connection and uses the connected account to authenticate the user with the application. If no previous connection matches, the flow will be sent to the application's sign up page so that the user may register with the application.

5.2 Enabling provider sign in

To add provider sign in capability to your Spring application, configure ProviderSignInController as a bean in your Spring MVC application:

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

The ProviderSignInController bean requires a single explicit <constructor-arg> value to specify the application's base secure URL. ProviderSignInController will use this URL to construct the callback URL used in the authentication flow. As with ConnectController, it is recommended that the application URL be externalized so that it can vary between deployment environments.

When authenticating via an OAuth 2 provider, ProviderSignInController supports the following flow:

  • POST /signin/{providerId} - Initiates the sign in flow by redirecting to the provider's authentication endpoint.

  • GET /signin/{providerId}?code={verifier} - Receives the authentication callback from the provider, accepting a code. Exchanges this code for an access token. It uses this access token to lookup a connected account and then authenticates to the application through the sign in service.

    • If the received access token doesn't match any existing connection, ProviderSignInController will redirect to a sign up URL. The sign up URL is "/signup" (relative to the application root).

For OAuth 1 providers, the flow is only slightly different:

  • POST /signin/{providerId} - Initiates the sign in flow. This involves fetching a request token from the provider and then redirecting to Provider's authentication endpoint.

  • GET /signin/{providerId}?oauth_token={request token}&oauth_verifier={verifier} - Receives the authentication callback from the provider, accepting a verification code. Exchanges this verification code along with the request token for an access token. It uses this access token to lookup a connected account and then authenticates to the application through the sign in service.

    • If the received access token doesn't match any existing connection, ProviderSignInController will redirect to a sign up URL. The sign up URL is "/signup" (relative to the application root).

5.2.1 ProviderSignInController's dependencies

As shown above, ProviderSignInController can be configured as a Spring bean given only a single constructor argument. Nevertheless, ProviderSignInController depends on a handful of other beans to do its job.

  • A ConnectionFactoryLocator to lookup the ConnectionFactory used to create the Connection to the provider.

  • A UsersConnectionRepository to find the user that has the connection to the provider user attempting to sign-in.

  • A ConnectionRepository to persist a new connection when a new user signs up with the application after a failed sign-in attempt.

  • A SignInService to sign a user into the application when a matching connection is found.

Because ProviderSignInController's constructor is annotated with @Inject, those dependencies will be given to ProviderSignInController via autowiring. You'll still need to make sure they're available as beans in the Spring application context so that they can be autowired.

You should have already configured most of these dependencies when setting up connection support (in the previous chapter). The SignInService is exclusively used for provider sign in and so a SignInService bean will need to be added to the configuration. But first, you'll need to write an implementation of the SignInService interface.

The SignInService interface is defined as follows:

public interface SignInService {
    void signIn(String localUserId);
}
			

The signIn() method takes a single argument which is the local application user's user ID normalized as a String. No other credentials are necessary here because by the time this method is called the user will have signed into the provider and their connection with that provider has been used to prove the user's identity. Implementations of this interface should use this user ID to authenticate the user to the application.

Different applications will implement security differently, so each application must implement SignInService in a way that fits its unique security scheme. For example, suppose that an application's security is based Spring Security and simply uses a user's account ID as their principal. In that case, a simple implementation of SignInService might look like this:

@Service
public class SpringSecuritySignInService implements SignInService {
    public void signIn(String localUserId) {
        SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(localUserId, null, null));
    }
}

			

5.2.2 Adding a provider sign in button

With ProviderSignInController and a SignInService configured, the backend support for provider sign in is in place. The last thing to do is to add a sign in button to your application that will kick off the authentication flow with ProviderSignInController.

For example, the following HTML snippet adds a "Signin with Twitter" button to a page:

<form id="tw_signin" action="<c:url value="/signin/twitter"/>" method="POST">
    <button type="submit"><img src="<c:url value="/resources/social/twitter/sign-in-with-twitter-d.png"/>" />
    </button>
</form>
			

Notice that the path used in the form's action attribute maps to the first step in ProviderSignInController's flow. In this case, the provider is identified as "twitter".

Clicking this button will trigger a POST request to "/signin/twitter", kicking off the Twitter sign in flow. If the user has not yet signed into Twitter, the user will be presented with the following page from Twitter:

After signing in, the flow will redirect back to the application to complete the sign in process.

5.3 Signing up after a failed sign in

If ProviderSignInController can't find a local user associated with a provider user attempting to sign-in, it will put the sign-in on hold and redirect the user to an application sign up page. By default, the sign up URL is "/signup", relative to the application root. You can override that default by setting the signupUrl property on the controller. For example, the following configuration of ProviderSignInController sets the sign up URL to "/register":

<bean class="org.springframework.social.connect.signin.web.ProviderSignInController">
    <constructor-arg value="${application.url}" />
    <property name="signupUrl" value="/register" />
</bean>
		

Before redirecting to the sign up page, ProviderSignInController collects some information about the authentication attempt. This information can be used to prepopulate the sign up form and then, after successful registration, to establish a connection between the new account and the provider account.

To prepopulate the sign up form, you can fetch the user profile data from a connection retrieved from ProviderSignInUtils.getConnection(). For example, consider this Spring MVC controller method that setups up the sign up form with a SignupForm to bind to the sign up form:

@RequestMapping(value="/signup", method=RequestMethod.GET)
public SignupForm signupForm(WebRequest request) {
    Connection<?> connection = ProviderSignInUtils.getConnection(request);
    if (connection != null) {
        return SignupForm.fromProviderUser(connection.fetchUserProfile());
    } else {
        return new SignupForm();
    }
}
		

If ProviderSignInUtils.getConnection() returns a connection, that means there was a failed provider sign in attempt that can be completed if the user registers to the application. In that case, a SignupForm object is created from the user profile data obtained from the connection's fetchUserProfile() method. Within fromProviderUser(), the SignupForm properties may be set like this:

public static SignupForm fromProviderUser(UserProfile providerUser) {
    SignupForm form = new SignupForm();
    form.setFirstName(providerUser.getFirstName());
    form.setLastName(providerUser.getLastName());
    form.setUsername(providerUser.getUsername());
    form.setEmail(providerUser.getEmail());
    return form;
}
		

Here, the SignupForm is created with the user's first name, last name, username, and email from the UserProfile. In addition, UserProfile also has a getName() method which will return the user's full name as given by the provider.

The availability of UserProfile's properties will depend on the provider. Twitter, for example, does not provide a user's email address, so the getEmail() method will always return null after a sign in attempt with Twitter.

After the user has successfully signed up in your application a connection can be created between the new local user account and their provider account. To complete the connection call ProviderSignInUtils.handlePostSignUp(). For example, the following method handles the sign up form submission, creates an account and then calls ProviderSignInUtils.handlePostSignUp() to complete the connection:

@RequestMapping(value="/signup", method=RequestMethod.POST)
public String signup(@Valid SignupForm form, BindingResult formBinding, WebRequest request) {
    if (formBinding.hasErrors()) {
        return null;
    }
    boolean accountCreated = createAccount(form, formBinding);
    if (accountCreated) {
        ProviderSignInUtils.handlePostSignUp(request);
        return "redirect:/";
    }
    return null;
}