Monday, January 21, 2013

Writing REST services in Java: Part 4 Facebook Authentication

Previous Post : Part Three: Email Verification
Get the Code: https://github.com/iainporter/rest-java

This post will cover OAuth Authentication with Facebook. Most REST applications provide a user with a choice of signing up with their email address and password or to delegate to a third-party security provider such as Facebook, Twitter, Google, etc. The solution in the rest-java application uses Javascript libraries on the client side for the oauth dance and Spring Social on the server for communicating with the Facebook social graph API.

 Client Code

Whether the client is a web page or a mobile device the desired end result is the same: to get an access token from the Facebook API.

The client code in scr/main/webapp/index.html:


window.fbAsyncInit = function() {
     FB.init({
       appId      : '133718006790561', // App ID
       status     : true, // check login status
       cookie     : true, // enable cookies to allow the server to access the session
       xfbml      : true  // parse XFBML
      });

      $('.fb').on('click', function () {

      FB.login(function(response) {
         console.log(response)
         if (response.authResponse) {
            javaRest.user.loginSocial(response.authResponse.accessToken, 
               function (error) {
               if (error)
                  console.log(error)
               else
                  window.location = 'dashboard.html'
               })
               console.log('Welcome!  Fetching your information.... ');
               FB.api('/me', function(response) {
                   console.log('Good to see you, ' + response.name + '.');
               });
          } else {
             console.log('User cancelled login or did not fully authorize.');
          }
       });

    })

 };
        
// Load the SDK Asynchronously
(function(d){
    var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0];
    if (d.getElementById(id)) {return;}
    js = d.createElement('script'); js.id = id; js.async = true;
    js.src = "//connect.facebook.net/en_US/all.js";
    ref.parentNode.insertBefore(js, ref);
}(document));
When registering the app with Facebook we insert the appId in line 03.
Facebook makes a callback with an authorization popup window.
The user accepts and we receive back an access token which we then use to call the API (line 14).

Server API Endpoint


Once the client has an access token they then call the server API which has an endpoint of:

user/login/<providerId>

In this case the providerId is facebook

The json payload is simply {"accessToken":"<the User's Facebook access token>"}

On the server side we want to achieve three things:

1. Access the user's Facebook profile and persist some relevant details
2. Match the profile up with an existing user account or else create a new one
3. Return an Authenticated User Token for API access so that we can treat Facebook logins the same as an email login

User Resource


    @PermitAll
    @Path("login/{providerId}")
    @POST
    public Response socialLogin(@PathParam("providerId") String providerId, OAuth2Request request) {
        OAuth2ConnectionFactory<?> connectionFactory = (OAuth2ConnectionFactory<?>) connectionFactoryLocator.getConnectionFactory(providerId);
        Connection<?> connection = connectionFactory.createConnection(new AccessGrant(request.getAccessToken()));
        AuthenticatedUserToken token = userService.socialLogin(connection);
        return getLoginResponse(token);
    }
We get a ConnectionFactory using the ConnectionFactoryLocator that was configured in SocialConfig.java
The factory class then creates a connection using the access token and the service method is called to login the user.

SocialConfig.java

@Configuration
public class SocialConfig {

    @Autowired
    ApplicationConfig config;

    @Autowired
    SocialUserRepository socialUserRepository;

    @Autowired
    UserRepository userRepository;

    @Autowired
    TextEncryptor textEncryptor;

    @Bean
    public ConnectionFactoryLocator connectionFactoryLocator() {
        ConnectionFactoryRegistry registry = new ConnectionFactoryRegistry();
        registry.addConnectionFactory(new FacebookConnectionFactory(
            config.getFacebookClientId(),
            config.getFacebookClientSecret()));
        return registry;
    }

    @Bean
    public UsersConnectionRepository usersConnectionRepository() {
        JpaUsersConnectionRepository usersConnectionRepository = new 
                JpaUsersConnectionRepository(socialUserRepository, userRepository,
                connectionFactoryLocator(), textEncryptor);

        return usersConnectionRepository;
    }
}

Social User Domain Model


A User has a many-to-one relationship with SocialUser instances. In the SocialUser model we need to capture several important pieces of information such as:

  • providerId - the security implementation provider (facebook)
  • providerUserId - the identifier within the Facebook universe
  • accessToken - the key needed to access the user's profile in Facebook (encrypted in the DB)
We can also capture other useful information such as profileImage, profileUrl, displayName, etc.

Note that we cannot guarantee to get an email address.

User Service


The implementation method for social login is:

@Transactional
public AuthenticatedUserToken socialLogin(Connection<?> connection) {
     List<String> userUuids = 
          jpaUsersConnectionRepository.findUserIdsWithConnection(connection);
     if(userUuids.size() == 0) {
         throw new AuthenticationException();
     }
     User user = userRepository.findByUuid(userUuids.get(0));
     if (user == null) {
         throw new AuthenticationException();
     }
     updateUserFromProfile(connection, user);
     return new AuthenticatedUserToken(user.getUuid().toString(), user.addSessionToken().getToken());
}


The JpaUsersConnectionRepository is used to connect to Facebook and retrieve the User's profile. It then matches it up to an existing user or else creates a new one if this is the first login attempt.
Once we have an API User we then create or update a SocialUser profile and associate it with the User.

JpaUsersConnectionRepository


The relevant method:

/**
* Find User with the Connection profile (providerId and providerUserId)
* If this is the first connection attempt there will be nor User so create one and
* persist the Connection information
* In reality there will only be one User associated with the Connection
*
* @param connection
* @return List of User Ids (see User.getUuid())
*/
public List<String> findUserIdsWithConnection(Connection<?> connection) {
    List<String> userIds = new ArrayList<String>();
    ConnectionKey key = connection.getKey();
    List<SocialUser> users = 
              socialUserRepository.findByProviderIdAndProviderUserId
             (key.getProviderId(), key.getProviderUserId());
    if (!users.isEmpty()) {
        for (SocialUser user : users) {
            userIds.add(user.getUser().getUuid().toString());
        }
        return userIds;
    }
//First time connected so create a User account or find one that is already 
//created with the email address
    User user = findUserFromSocialProfile(connection);
    String userId;
    if(user == null) {
       userId = userService.createUser(Role.authenticated).getId();
    } else {
       userId = user.getUuid().toString();
    }
    //persist the Connection
    createConnectionRepository(userId).addConnection(connection);
    userIds.add(userId);
    return userIds;
}

private User findUserFromSocialProfile(Connection connection) {
    User user = null;
    UserProfile profile = connection.fetchUserProfile();
    if(profile != null && StringUtils.hasText(profile.getEmail())) {
        user = userRepository.findByEmailAddress(profile.getEmail());
    }
    return user;
}

Testing the API



Step 1. Create an application on Facebook and if you want to test it locally set the site url to http://localhost:8080/java-rest
















Step 2. Insert your App Id and App Secret in src/main/resources/app.properties
Step 3. Insert your appId in to the relevant place in the javascript in src/main/webapp/index.html and src/main/webapp/signup.html
Step 4. Start the application by running :
gradle tomcatRun


Step 5. In a browser window request http://localhost:8080/java-rest/index.html

You should be able to login with your Facebook credentials

This can easily be extended to include other OAuth providers such as Twitter or Google
That wraps up this post.
In the next post I will cover the Lost Password API.

3 comments:

  1. similarly, I had blogged about openid authentication with google. Some readers may find it useful.

    ReplyDelete
  2. Can you provide client steps (using javascript) to pull the accessToken code for twitter?

    ReplyDelete