Search code examples
springspring-socialspring-social-facebook

Spring facebook login: signup redirect


SCENARIO 1 When logging in with facebook to my webapp

If the browser is already logged in to Facebook, and

if the user's social authentication details are already registered in my webapp,

then when the user clicks "Sign in with Facebook" on my webapp,

he/she is authorized by Facebook and the Authorization object returned by

Authentication auth = SecurityContextHolder.getContext().getAuthentication();

is the named user.

All is good.


SCENARIO 2 If the browser is NOT logged into Facebook but

the user's social authentication details are already registered in my webapp,

then when the user clicks "Sign in with Facebook" on my webapp,

the user signs in with Facebook by entering username and password BUT

the the Authorization object returned by

Authentication auth = SecurityContextHolder.getContext().getAuthentication();

is authorized as anonymous and has the name anonymousUser.

Moreover, the call from Facebook after authorization is to /signup

However, I am expecting the same registered user as in scenario 1

My security configuration is below. Can anyone give me a hint as to what the problem may be?

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                    http://www.springframework.org/schema/beans/spring-beans.xsd
                    http://www.springframework.org/schema/security
                    http://www.springframework.org/schema/security/spring-security.xsd">

	<security:http   use-expressions="true" entry-point-ref="appAuthenticationEntryPoint">

		<security:intercept-url pattern="/login"  access="permitAll()" />
		<security:intercept-url pattern="/flow-entry.html" access="permitAll()"/> 
		<security:intercept-url pattern="/flow-jobpostdata.html" access="permitAll()"/>
		<security:intercept-url pattern="/flow-jobpostdata_anydegree.html" access="permitAll()"/>
		  <security:intercept-url pattern="/j_spring_security_check" access="permitAll()"/>	
     <!-- Adds social authentication filter to the Spring Security filter chain. -->
        <security:custom-filter before="PRE_AUTH_FILTER" ref="socialAuthenticationFilter"/>
        <security:custom-filter position="FORM_LOGIN_FILTER" ref="SecurityAuthFilter"/>
	</security:http>

<!--  authentication manager and its provider( social provider deals with social login & local user provider deals with form login ) -->
    <security:authentication-manager alias="authenticationManager">
    
        <security:authentication-provider ref="socialAuthenticationProvider"/>
        <security:authentication-provider user-service-ref="localUserDetailService"/>
    </security:authentication-manager>
    

   

	<bean id="customAuthenticationProvider" class="com.ikoda.service.loginservices.CustomAuthenticationProvider">
			<property name="auService" ref="auService" />
	</bean> 

    <bean id="socialAuthenticationProvider" class="org.springframework.social.security.SocialAuthenticationProvider">
        <constructor-arg ref="inMemoryUsersConnectionRepository"/>
        <constructor-arg ref="socialUserDetailService"/>
    </bean>

    <!-- form login beans -->
  
    

     
     
     <bean id="successHandler" class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
    </bean>
     
     <bean id="appAuthenticationEntryPoint"
          class="com.ikoda.service.loginservices.AppAuthenticationEntryPoint">
        <constructor-arg name="loginFormUrl" value="/login"/>
    </bean>
    
    <bean id="rememberMeServices"
          class="org.springframework.security.web.authentication.NullRememberMeServices"/>

    <bean id="failureHandler"
          class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
        <constructor-arg name="defaultFailureUrl" value="/login?error=true"/>
    </bean>
    
    


    
    <bean class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"
          id="SecurityAuthFilter">
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="authenticationSuccessHandler" ref="successHandler"/>
        <property name="authenticationFailureHandler" ref="failureHandler"/>
        <property name="filterProcessesUrl" value="/j_spring_security_check"/>
        <property name="rememberMeServices" ref="rememberMeServices"/>
    </bean>

    <!-- social login filter which is a pre authentication filter and works for /auth service url -->
    <bean id="socialAuthenticationFilter" class="org.springframework.social.security.SocialAuthenticationFilter">
        <constructor-arg name="authManager" ref="authenticationManager"/>
        <constructor-arg name="userIdSource" ref="userIdSource"/>
        <constructor-arg name="usersConnectionRepository" ref="inMemoryUsersConnectionRepository"/>
        <constructor-arg name="authServiceLocator" ref="appSocialAuthenticationServiceRegistry"/>
        <property name="authenticationSuccessHandler" ref="successHandler"/>
    </bean>


    <!-- inmemory connection repository which holds connection repository per local user -->
    <bean id="inMemoryUsersConnectionRepository"
          class="org.springframework.social.connect.mem.InMemoryUsersConnectionRepository">
        <constructor-arg name="connectionFactoryLocator" ref="appSocialAuthenticationServiceRegistry"/>
        <property name="connectionSignUp" ref="connectionSignUp"/>
    </bean>

    <!-- service registry will holds connection factory of each social provider-->
    <bean id="appSocialAuthenticationServiceRegistry"
          class="com.ikoda.service.loginservices.AppSocialAuthenticationServiceRegistry">
        <constructor-arg>
            <list>
                <ref bean="facebookAuthenticationService"/>
               <ref bean="googleAuthenticationService"/> 
            </list>
        </constructor-arg>
    </bean> 

    <bean id="facebookAuthenticationService"
          class="org.springframework.social.facebook.security.FacebookAuthenticationService">
        <constructor-arg name="apiKey" value="11111"/>
        <constructor-arg name="appSecret" value="11111"/>
    </bean>

    <bean id="googleAuthenticationService"
          class="org.springframework.social.google.security.GoogleAuthenticationService">
        <constructor-arg name="apiKey" value="111-lpmhcmuj1577bd6god0696g4u2g16c1i.apps.googleusercontent.com"/>
        <constructor-arg name="appSecret" value="111-111-"/>
    </bean>
    

    <bean id="userIdSource" class="org.springframework.social.security.AuthenticationNameUserIdSource"/>

    <!-- If no local user is associated to a social connection then connection sign up will create a new local user and map it to social user -->
    <bean id="connectionSignUp" class="com.ikoda.service.loginservices.AppConnectionSignUp"/>

</beans>


Solution

  • The redirect to signUp is the intended behaviour of Spring Social. The key is how to manage the behaviour.

    One way to solve the problem is by authenticating the social login in the controller signup method. To do this requires accessing the social connection. You can do this by autowiring ProviderSignInUtils in your controller.

    Note AppConnectionSignUP is some implementation of org.springframework.social.connect.ConnectionSignUp

    Connection<?> connection = providerSignInUtils.getConnectionFromSession(webRequest);
            if (null != connection)
            {
    
                String userIdString = appConnectionSignUp.execute(connection);
            }
    

    Another way to solve the problem is to implement your own ConnectionSignUp

        <bean id="connectionSignUp" class="com.ikoda.service.loginservices.AppConnectionSignUp"/>

    and reference that in your UsersConnectionRepository

         <bean id="jdbcUsersConnectionRepository" 
                  class="org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository">
                <constructor-arg ref="dataSource" />
                <constructor-arg ref="appSocialAuthenticationServiceRegistry" />
                <constructor-arg ref="textEncryptor" />
                <property name="connectionSignUp" ref="connectionSignUp"/>
            </bean>

    If you take this approach, then social login will bypass the Controller /signUp and directly invoke the execute method of connectionSignUP bean.

    Meanwhile, in the question above the inMemoryUsersConnectionRepository was ill-advised. I switched to the jdbcUsersConnectionRepository.