Facebook Connect with JSecurity on Grails

April 18, 2010

Say you have a Grails application using JSecurity (now called Apache Shiro) for authentication. How do you provide an alternate mechanism to authenticate users using Facebook Connect?

Good thing! Grails has the Facebook Connect plugin so you can authenticate users without registering them to your system. But what you will want is to integrate the Facebook Connect Plugin with the JSecurity Plugin. That way, even though a user is actually authenticated using Facebook Connect, your JSecurity knows about the user and can assign roles and permissions to the user.

I assume you have both the Apache Shiro Plugin installed and authentication working properly. Now go ahead and install the and Facebook Connect Plugin by following the documentation.

In addition to the regular JSecurity Login form, you will have the Facebook Login button as follows:

login.gsp

<script type="text/javascript">
    <!--
        function delayer() {
            window.location = "${createLink(controller:'auth', action: 'signin')}"
        }
    //-->
</script>
<div id="login_section">
    <h4>Login using Facebook Connect</h4> 
    <div class="login_form">
        <fb:login-button autologoutlink="false" onlogin="setTimeout('delayer()',100)">
        </fb:login-button>
        <br/>
	<fb:name uid="loggedinuser" useyou="false"></fb:name>
	<fb:profile-pic uid="loggedinuser" size="normal" />			
	<g:facebookConnectJavascript  />
    </div>
</div> <!--Login Form Ends-->  

The above code snippet will display the Facebook login button. When the user clicks on the button, Facebook Connect’s login dialog is displayed. Once he user authenticates himself to Facebook, the delayer() javascript method is called which then redirects to the auth/signin grails action.

Now lets insert the following snippet of code into the signIn action.

AuthController.groovy

def facebookConnectService
def signin = {
    if(!params?.username && !params?.password) {
        if(facebookConnectService.isLoggedIn(request)) {
            try {
                facebookConnectService.getFacebookClient().users_getLoggedInUser()
                params.rememberMe = false
                params.username = "facebook"
                params.password = "randompassword"
            } catch (Exception e) {
                flash.error ="We are sorry. Please try again in a while."
                redirect(controller: 'home', action: 'index') 
                return
            }
        }
    }
    // Rest of the Code continues
}

where, facebookConnectService is the service class that is provided by the the Grails Facebook Connect plugin. We are just checking if the user is logged in to Facebook, and then setting values for username and password as per JSecurity’s needs.

The actual authentication of the user is done in grails-app/realms/ShiroDbRealm.groovy.

Now lets add a few lines to our ShiroDbRealm.groovy to handle our Facebook user:

ShiroDbRealm.groovy

def facebookConnectService

def authenticate(authToken) {
        log.info "Attempting to authenticate ${authToken.username} in DB realm..."
        def username = authToken.username

        // Null username is invalid
        if (username == null) {
            throw new AccountException('Null usernames are not allowed by this realm.')
        }

        def user = null
        def account = null
        if (username == "facebook") {
            def facebookUser = null
            try {
                facebookUser = getFacebookUser()
            } catch (Exception e) {
                e.printStackTrace()
            }
       	    try {
       	        user = User.findByUsername(facebookUser.username)
       	    } catch (Exception e) {
       	        e.printStackTrace()
       	    }

           if (!user) {
                facebookUser.passwordHash = new Sha1Hash("randompassword").toHex()
                if (facebookUser.validate()) {
	                user = facebookUser.save(flush: true)
                } else {
                        facebookUser.errors.allErrors.each {
                            log.error it
                        }
                }

                account = new SimpleAccount(user.username, user.passwordHash, "ShiroDbRealm")
                return account;

            } else {
                account = new SimpleAccount(user.username, user.passwordHash, "ShiroDbRealm")
                return account;
            }
      }
      // Rest of the Code continues
}

def getFacebookUser () {
        String userId = facebookConnectService.getFacebookClient().users_getLoggedInUser()
        java.lang.Iterable<java.lang.Long> userIds = new ArrayList<java.lang.Long>()
        userIds.add(Long.parseLong(userId))
		
        Set<ProfileField> c = new HashSet<ProfileField>()
        c.add(ProfileField.FIRST_NAME)
        c.add(ProfileField.LAST_NAME)
        c.add(ProfileField.NAME)

        def myresults = facebookConnectService.getFacebookClient().users_getInfo(userIds, c)
        def useObj = myresults.getJSONObject(0)
        User user = new User()
    	user.username = userId
    	user.firstName = useObj.getString("first_name")
    	user.lastName = useObj.getString("last_name")
    	user.name = useObj.getString("name")  	
    	user.displayName = user.name
    	
        Date date = new java.util.Date()
    	user.dateCreated = date
    	user.lastUpdated = date
    	user.lastVisited = date
        return user
}

Basically, if the user uses Facebook Connect, we are setting his username to ‘”facebook” and password to “randompassword” from the signin action. In the authenticate method, if the username is “facebook”, we are getting all the info of the user from facebook. If the user with that facebook id is already in our database, it is an existing user and is authenticated. If the user doesnt exist in our database, the new user is added to the databse with a username and password. The username and password is only for integrating Facebook Connect with JSecurity, and the user has no idea about it. Now even though the user is actually authenticated by Facebook, it still is a JSecurity user in our database, and will be treated just like any other user.

Please note that I have added a few properties like firstName, lastName, displayName etc to the User.groovy which is created by JSecurity. Feel free to add new properties to the User object if you want to capture more user info from Facebook for that user.

Now you are good to go. Deploy and test the app.

Soon, you will notice that this will work in development, but there is a bug in the Current revision of Grails Facebook Connect Plugin in production environment, due to which it it cannot find the FacebookConnectConfig.groovy in prod. Go and download the source for Facebook Connect Grails plugin and modify the afterPropertiesSet method under services/FacebookConnectService.groovy in the grails-facebook-connect-0.1 project.

FacebookConnectService.groovy

void afterPropertiesSet() {
    // check if there is a compiled class for the Facebook connect config groovy script
    // this will be the case when an application is bundled in a WAR
    def config = null
    try {
        config = Class.forName("FacebookConnectConfig").newInstance()
    } catch(Exception  e) {
        e.printStackTrace()
    }
    if (config != null) {
        // compiled config class found, we must be running as a WAR 
        facebookConnectConfig = new ConfigSlurper().parse(config.getClass())
    } else {
        // no compiled class exists for the config, we must be running the Grails built-in web server
        GroovyClassLoader loader = new GroovyClassLoader(getClass().getClassLoader())
        Class clazz = loader.parseClass(new File("grails-app/conf/FacebookConnectConfig.groovy"))
        facebookConnectConfig = new ConfigSlurper().parse(clazz)
    }
}

Now use the patched plugin in your app and that should work!

Shoud you use Facebook Connect?:
Based on my few months of experience, users are pretty hesitant to login to a site using Facebook Connect. In my case, less than 10% of the users used Facebook Connect on my site, and it certainly wasn’t worth the effort I put into making it work and mantaining it. Its kind of clunky at times too. I don’t think I am never gonna use it again just for the sake of authentication. So, make sure it really adds some value to your application and users, before you decide to integrate it in your app coz you thought its cool.

7 Comments on Facebook Connect with JSecurity on Grails

Respond | Trackback

  1. David Carnley says:

    Thanks for this article. I am designing FB integration with our secure site and your article was helpful because I am considering taking a similar approach. Although I am using .NET and javascript.

    Also, I think you have stumbled onto one of the most important ideas in being a software engineer – “make sure it really adds some value to your application and users, before you decide to integrate it in your app coz you thought its cool” This is one of the most important things I have learned about this profession :)

  2. Egle says:

    Thanks for the post. Maybe we can use this in our app ;)

  3. Mike Croteau says:

    Thanks so much for this post. I was wracking my brain on how to handle this type of scenario where you want to assign roles and permissions to your app’s users while allowing the use of Facebook Connect as a registration/login point.

    I have no experience with JSecurity. I use Spring Security (Acegi) mostly. Spring Security (Acegi) plugin has support for Facebook Connect on login, but no real examples on how to save the user for security purposes who logs in via Facebook Connect. So, I will be attempting to pull off something similar to what you have done using JSecurity.

    I will post an update. Thanks again for this great post!

    Best
    Mike

  4. Jason Crist says:

    I’ve tried so hard to get this working. It seems like it should be simple. I understand everything that should be going on. I even got it working for a few minutes! But now my Grails app can’t seem to get to the FB cookie and I don’t know why! the JESSIONID is all I seem to be able to get to.

    Can anybody help me out?!

  5. Mike Croteau says:

    Hey Jason, were you able to get it working?

  6. in ShiroDbRealm.groovy, in the getFacebookUser method, what type of User class are you using?

    My IDE shows me that it’s org.apache.catalina.realm.User, but when I try to instantiate it with new, I end up with a NullPointerException

    Using grails 1.3.7

    Thanks!

  7. Francesco says:

    Hi, is possible to retrieve user email from facebookClient?
    Thank you in advance.
    Francesco

Respond

Comments

Comments