Our application is strongly integrated with Twitter, so it seems logical that we would allow authentication through Twitter.
Before going further, make sure that you have enabled Twitter sign in on your app on Twitter (https://apps.twitter.com):
Spring social enables authentication through an OAuth provider such as Twitter through a signin/signup scenario. It will intercept a POST
request on /signin/twitter
. If the user is not known to the UsersConnectionRepository
interface, the signup
endpoint will be called. It will allow us to take the necessary measures to register the user on our system and maybe ask them for additional details.
Let's get to work. The first thing we need to do is to add the signin/**
and /signup
URLs as publicly available resources. Let's modify our WebSecurityConfiguration
class, changing the permitAll
line:
.antMatchers("/webjars/**", "/login", "/signin/**", "/signup").permitAll()
To enable the signin/signup scenario, we also need a SignInAdapter
interface, a simple listener that will be called when an already known user signs in again.
We can create an AuthenticatingSignInAdapter
class right next to our LoginController
:
package masterSpringMvc.authentication; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.social.connect.Connection; import org.springframework.social.connect.UserProfile; import org.springframework.social.connect.web.SignInAdapter; import org.springframework.stereotype.Component; import org.springframework.web.context.request.NativeWebRequest; @Component public class AuthenticatingSignInAdapter implements SignInAdapter { public static void authenticate(Connection<?> connection) { UserProfile userProfile = connection.fetchUserProfile(); String username = userProfile.getUsername(); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, null); SecurityContextHolder.getContext().setAuthentication(authentication); System.out.println(String.format("User %s %s connected.", userProfile.getFirstName(), userProfile.getLastName())); } @Override public String signIn(String userId, Connection<?> connection, NativeWebRequest request) { authenticate(connection); return null; } }
As you can see, this handler is called at the perfect time to allow user authentication with Spring Security. We'll come back to that in just a moment. For now, we need to define our SignupController
class in the same package, the one in charge of first-time visiting users:
package masterSpringMvc.authentication; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.Connection; import org.springframework.social.connect.ConnectionFactoryLocator; import org.springframework.social.connect.UsersConnectionRepository; import org.springframework.social.connect.web.ProviderSignInUtils; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.request.WebRequest; @Controller public class SignupController { private final ProviderSignInUtils signInUtils; @Autowired public SignupController(ConnectionFactoryLocator connectionFactoryLocator, UsersConnectionRepository connectionRepository) { signInUtils = new ProviderSignInUtils(connectionFactoryLocator, connectionRepository); } @RequestMapping(value = "/signup") public String signup(WebRequest request) { Connection<?> connection = signInUtils.getConnectionFromSession(request); if (connection != null) { AuthenticatingSignInAdapter.authenticate(connection); signInUtils.doPostSignUp(connection.getDisplayName(), request); } return "redirect:/profile"; } }
First, this controller retrieves the current connection from the session. Then, it authenticates the user through the same method as before. Lastly, it will trigger the doPostSignUp
event, which will allow Spring Social to store information relative to our user in the UsersConnectionRepository
interface that we mentioned earlier.
The last thing we need to do is add a triumphant "login with twitter" button to our login page, right below the previous form:
<form th:action="@{/signin/twitter}" method="POST" class="center"> <div class="row"> <button class="btn indigo" name="twitterSignin" type="submit">Connect with Twitter <i class="mdi-social-group-add left"></i> </button> </div> </form>
When the user clicks on the CONNECT WITH TWITTER button, they will be redirected to a Twitter sign in page:
There isn't much code, but it is a bit tricky to understand all the parts. The first step to getting what's going on is to have a look at the SocialWebAutoConfiguration
class of Spring Boot.
The SocialAutoConfigurationAdapter
class declared in this class contains the following bean:
@Bean @ConditionalOnBean(SignInAdapter.class) @ConditionalOnMissingBean(ProviderSignInController.class) public ProviderSignInController signInController( ConnectionFactoryLocator factoryLocator, UsersConnectionRepository usersRepository, SignInAdapter signInAdapter) { ProviderSignInController controller = new ProviderSignInController( factoryLocator, usersRepository, signInAdapter); if (!CollectionUtils.isEmpty(this.signInInterceptors)) { controller.setSignInInterceptors(this.signInInterceptors); } return controller; }
The ProviderSignInController
class will automatically be set up if one ProviderSignInController
class is detected in our configuration. This controller is the cornerstone of the sign-in process. Have a look at what it does (I will only summarize the important parts):
POST /signin/{providerId}
from our connect buttonGET /signin/{providerId}
from the identification providerUsersConnectionRepository
interface, it will use a SessionStrategy
interface to store the pending login request and will then redirect to the signupUrl
pageSignInAdapter
interface is called and the user is redirected to the postSignupUrl
pageThe two important components of this identification are the UsersConnectionRepository
interface in charge of storing and retrieving users from some kind of storage and the SessionStrategy
interface that will temporarily store the user connection so it can be retrieved from the SignupController
class.
By default, Spring Boot creates an InMemoryUsersConnectionRepository
interface for each authentication provider, which means that our user connection data will be stored in memory. If we restart the server, the user will become unknown and will go through the sign-up process again.
The ProviderSignInController
class defaults to HttpSessionSessionStrategy
, which will store the connection in the HTTP session. The ProviderSignInUtils
class that we use in our SignupController
class also uses this strategy by default. If we were distributing our application on multiple servers, this would be problematic because the session would likely not be available on every server.
It is easy enough to override these defaults by providing a custom SessionStrategy
interface to both the ProviderSignInController
and ProviderSignInUtils
classes to store data somewhere other than in the HTTP session.
Likewise, we can use another kind of storage for our user connection data by providing another implementation of the UsersConnectionRepository
interface.
Spring Social provides a JdbcUsersConnectionRepository
interface that will automatically save authenticated users in a UserConnection
table in your database. This won't be covered in this book extensively, but you should be able to configure it easily by adding the following bean to your configuration:
@Bean @Primary public UsersConnectionRepository getUsersConnectionRepository( DataSource dataSource, ConnectionFactoryLocator connectionFactoryLocator) { return new JdbcUsersConnectionRepository( dataSource, connectionFactoryLocator, Encryptors.noOpText()); }
Check out this article http://geowarin.github.io/spring/2015/08/02/social-login-with-spring.html on my blog for more details.