I previously posted a quick guide on how to integrate Grails with Facebook connect using the facebook-graph plugin. This is my in-depth followup. But before we start, a few points:
1. The facebook-graph plugin, while convenient, doesn’t really buy you much. If anything, since the Facebook development platform changes so frequently, using the plugin actually hampers your ability to adapt. For instance, Facebook is switching over to OAuth2.0 and has been in the process of doing so for the past few months. The facebook-graph plugin is NOT using OAuth2.0, so if you want to get compliant, you better start thinking about rolling your own.
2. My initial example was very basic and incomplete. While we got the Facebook profile, we didn’t really tie it into our existing security framework.
3. For this in-depth how-to, I’m using stable, but pre-release software. Specifically, I’ll be using Grails 2.0.0.M1. Most of the stuff should work with previous versions, but no guarantees.
4. I’m assuming you have a good understanding of Grails, Spring, Spring Security, Facebook, etc.
Let’s start.
Integrating Facebook – Using Grails Taglibs and Templates
For ease of use, I’m creating a tag library that will allow us to quickly inject the FB JavaScript resources in our gsps.
My FacebookTagLib.groovy looks like:
class FacebookTagLib {
static namespace = "fcbk"
def resources = {attrs, body ->
if(!grailsApplication.config.facebook.applicationId) {
out << "<div>facebook.applicationId not defined in the Config.groovy!</div>"
} else {
out << render(template:"/tags/facebookResources")
}
}
}
Instead of trying to cram all our FB code in the taglib, I created a template under my views/tags directory.
My _facebookResources.gsp looks like:
<div id="fb-root"></div>
<script src="http://connect.facebook.net/en_US/all.js"></script>
<script>
window.fbAsyncInit = function() {
FB.init({appId: '${grailsApplication.config.facebook.applicationId}', status:true, cookie:true, xfbml:true, oauth:true, channel:'http://www.alexduan.com:8080/channel'});
function updateButton(response) {
var button = document.getElementById('fb-auth');
if (response.authResponse) {
//user is already logged in and connected
var userInfo = document.getElementById('user-info');
FB.api('/me', function(response) {
userInfo.innerHTML = '<img src="https://graph.facebook.com/'
+ response.id + '/picture">' + response.name;
button.innerHTML = 'Logout';
});
button.onclick = function() {
FB.logout(function(response) {
var userInfo = document.getElementById('user-info');
userInfo.innerHTML="";
});
};
} else {
//user is not connected to your app or logged out
button.innerHTML = 'Login';
button.onclick = function() {
FB.login(function(response) {
if (response.authResponse) {
FB.api('/me', function(response) {
var userInfo = document.getElementById('user-info');
userInfo.innerHTML = '<img src="https://graph.facebook.com/'
+ response.id + '/picture" style="margin-right:5px"/>'
+ response.name;
});
} else {
//user cancelled login or did not grant authorization
}
}, {scope:'email'});
}
}
}
FB.getLoginStatus(updateButton);
FB.Event.subscribe('auth.statusChange', updateButton);
FB.Event.subscribe('auth.login', function(response) {
$('<input>').attr({type:'hidden',
id:'accessToken',
name:'${com.alexduan.services.security.FacebookTokenAuthenticationFilter.FACEBOOK_SECURITY_FORM_ACCESS_TOKEN_KEY}',
value:response.authResponse.accessToken}).appendTo('#hiddenFacebookLoginForm');
$('#hiddenFacebookLoginForm').submit();
});
};
(function() {
var e=document.createElement('script');
e.src = document.location.protocol + '//connect.facebook.net/en_US/all.js';
e.async = true;
document.getElementById('fb-root').appendChild(e);
}());
</script>
<form action="${grailsApplication.config.facebook.security.filter.url}" method='POST' id="hiddenFacebookLoginForm"></form>
Few things…
1. The above enables oauth.
2. The button example is from the Facebook documentation, and looks fugly.
3. For an added layer of security (don’t feel too secure, it’s just a basic line of defense), I’m submitting my FB access token through a hidden form using POST. I dynamically generate the input and submit it via JQuery.
I created a VERY basic FacebookService.groovy that has one method to get the user’s profile:
class FacebookService {
final static def GRAPH_URL = "https://graph.facebook.com/"
def getProfile(String accessToken) {
if(accessToken) {
def urlString = GRAPH_URL + "me/?access_token=" + accessToken
log.debug urlString
URL url = new URL(urlString)
JSON.parse(url.getText())
}
}
}
Spring Security
The biggest problem with my first try was that I was essentially logging in outside of my security framework’s lifecycle. For this go around, I wanted to make sure I did things the Spring Security way.
In my FB template, I’m submitting my hidden form to:
facebook.security.filter.url="/j_facebook_security_check"
Essentially, I create an Authentication filter which handles the request to my given URL. It then routes that request to a designated AuthenticationManager. The AuthenticationManager will try to process the Authentication token we pass to it with its registered AuthenticationProviders – until it receives a non-null Authentication token that is authenticated in return.
My FacebookTokenAuthenticationFilter:
class FacebookTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
def grailsApplication
FacebookService facebookService
SpringSecurityService springSecurityService
public static final String FACEBOOK_SECURITY_FORM_ACCESS_TOKEN_KEY = "j_facebooktoken"
private boolean postOnly = true
public FacebookTokenAuthenticationFilter() {
super("/j_facebook_security_check")
}
@Override
public void afterPropertiesSet() {
super.setFilterProcessesUrl(grailsApplication.config.facebook.security.filter.url)
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod())
}
String accessToken = request.getParameter(FACEBOOK_SECURITY_FORM_ACCESS_TOKEN_KEY)
def profile = facebookService.getProfile(accessToken)
if(profile) {
//this is where your logic goes...
return this.getAuthenticationManager().authenticate(new FacebookAuthenticationToken(profile.id, accessToken))
}
return null;
}
1. I’m using the afterPropertiesSet() method because DI of grailsApplication happens after the initial call to the super’s constructor.
2. We’re making sure that we’re only accepting POST requests.
3. Make sure if you do any DB work, to use the withTransaction closure, otherwise you will get an error complaining about HB transactions.
I register this in my Resources.groovy:
facebookTokenAuthenticationFilter(FacebookTokenAuthenticationFilter) {
facebookService = ref('facebookService')
springSecurityService = ref('springSecurityService')
authenticationManager = ref('authenticationManager')
sessionAuthenticationStrategy = ref('sessionAuthenticationStrategy')
authenticationSuccessHandler = ref('authenticationSuccessHandler')
authenticationFailureHandler = ref('authenticationFailureHandler')
rememberMeServices = ref('rememberMeServices')
authenticationDetailsSource = ref('authenticationDetailsSource')
grailsApplication = ref('grailsApplication')
}
My Authentication token:
class FacebookAuthenticationToken extends AbstractAuthenticationToken {
Object principal
String accessToken;
public FacebookAuthenticationToken(Object principal, String accessToken) {
super(null)
this.principal = principal
this.accessToken = accessToken
}
public FacebookAuthenticationToken(Object principal, String accessToken, Collection<? extends GrantedAuthority> authorities) {
super(authorities)
this.principal = principal
this.accessToken = accessToken
super.setAuthenticated(true) //since we throw illegal exceptions...call super
}
@Override
public Object getCredentials() {
//always return null
return null
}
@Override
public Object getPrincipal() {
return principal
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if(isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor containing GrantedAuthority[]s instead")
}
super.setAuthenticated(false)
}
}
Before we create our Authentication Provider, we’re going to create a customer User Details Service as well as a User Details object (so we can store our FacebookUID)…
My user details class:
class MyUserDetails extends GrailsUser {
String facebookUID
public MyUserDetails(String username, String password,
boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<GrantedAuthority> authorities, Object id) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired,
accountNonLocked, authorities, id)
}
}
The user details service:
class MyUserDetailsService implements UserDetailsService {
static final List NO_ROLES = [new GrantedAuthorityImpl(SpringSecurityUtils.NO_ROLE)]
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException {
// TODO Auto-generated method stub
Person.withTransaction {
Person user = Person.findByUsername(username)
if(!user) throw new UsernameNotFoundException('Unable to find Person by username', username)
return makeUserDetails(user)
}
}
public UserDetails loadUserByFacebookUID(String facebookUID) {
Person.withTransaction {
Person user = Person.findByFacebookUID(facebookUID)
if(!user) throw new UsernameNotFoundException('Unable to find Person by facebookUID', facebookUID)
return makeUserDetails(user)
}
}
private MyUserDetails makeUserDetails(Person user) {
def authorities = user.authorities.collect { new GrantedAuthorityImpl(it.authority) }
def userDetails = new MyUserDetails(user.username ?: user.facebookUID, user.password, user.enabled,
!user.accountExpired, !user.passwordExpired,
!user.accountLocked, authorities ?: NO_ROLES, user.id)
userDetails.facebookUID = user.facebookUID
return userDetails
}
}
And my Authentication Provider:
class FacebookAuthenticationProvider implements AuthenticationProvider {
def userDetailsService
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//quickly return if for some reason the authentication manager uses this provider for non-facebook
if(!supports(authentication.getClass())) return null
MyUserDetails userDetails = userDetailsService.loadUserByFacebookUID(authentication.principal)
//return null if no user details are found
if(userDetails == null) return null
log.debug 'User details found: ' + userDetails
//create facebook token, which includes UID
FacebookAuthenticationToken token = new FacebookAuthenticationToken(userDetails, ((FacebookAuthenticationToken) authentication).accessToken, userDetails.getAuthorities())
token.setDetails(userDetails)
return token
}
@Override
public boolean supports(Class authentication) {
return (FacebookAuthenticationToken.class.isAssignableFrom(authentication));
}
}
And to tie it all together in my resources.groovy:
userDetailsService(MyUserDetailsService)
facebookAuthenticationProvider(FacebookAuthenticationProvider) {
userDetailsService = ref('userDetailsService')
}
And some of my security settings in Config.groovy (includes encryption, persistent login, etc.):
grails.plugins.springsecurity.useSessionFixationPrevention = true
grails.plugins.springsecurity.rememberMe.persistent = true
grails.plugins.springsecurity.rememberMe.persistentToken.domainClassName = 'com.alexduan.domain.user.PersistentLogin'
grails.plugins.springsecurity.password.algorithm = 'bcrypt'
grails.plugins.springsecurity.password.bcrypt.logrounds = 10
grails.plugins.springsecurity.providerNames = ['daoAuthenticationProvider', 'facebookAuthenticationProvider', 'anonymousAuthenticationProvider', 'rememberMeAuthenticationProvider']
The main things to note are that we added the provider to our providerNames property, which tells our AuthenticationManager which providers to cycle through for each request. I chose to use bcrypt, which is more secure, but takes longer to generate. I set it to 15 and it basically took 2 minutes for it to generate an encrypted password. Just sayin’…
Bonus: Event Logging
And for giggles, I’ll just throw in a quick SecurityListener:
class SecurityListener implements ApplicationListener {
public void onApplicationEvent(ApplicationEvent event) {
if(event instanceof AbstractAuthenticationEvent) {
if(event instanceof InteractiveAuthenticationSuccessEvent) {
Authentication token = ((InteractiveAuthenticationSuccessEvent) event).getAuthentication()
def principal = token.getPrincipal()
LoginHistory.withTransaction {
def loginHistory = new LoginHistory(personId: principal.id).save(flush:true)
}
}
//TODO: Other Stuff
}
}
}
Be sure to register that as a resource…
Takeaways
I think the biggest takeaway here is that Spring Security is a very flexible framework. By allowing you to register your own filters and providers, we can basically write custom login modules for every social network known to man. Of course, I guess that’s why there is a Spring Social project…but that’s neither here nor there.
Some of the smaller takeaways include:
1. There is a lot of Facebook API documentation – too bad a lot of it is out of date.
2. Facebook really is the worst developer platform.
3. I wonder how many people who use Grails actually have a background in Spring…
Hope this provided some insight into Grails, Spring Security, and Facebook…