Last week, for some reason, I had multiple requests for an example of ColdFusion and OAuth integration. I ended up creating quick demos for Facebook, LinkedIn, and Google. This week I'll be blogging each in turn in the hopes that these entries can help others. Today, I'm going to share the Facebook code.
Before I begin, I want to warn folks. I wrote this code very quickly. It is not optimized. Also, the person I was helping was on ColdFusion 8. So the code isn't exactly what I'd call up to date. Of course, it will run just fine with ColdFusion 10. I typically assume that folks take my code samples here as just that - code samples - but I wanted to be more clear that this code would probably not be exactly best practices.
To begin, ensure you have access to Facebook's Developer portal (developer.facebook.com) and create a new application.
You can call it whatever you want, but the name should reflect your site in some way. Users will see this when your app launches so make it familiar. You can ignore the other two options.
On the next page, make note of your App ID and App Secret:
Click on "Website with Facebook Login" and enter a value for your site. You can, and probably should, use a local domain. In other words, you can enter something under localhost. Obviously this will change to a production URL once you're done, but for testing, localhost is fine. For my testing I used: http://localhost/testingzone/cf8fb and clicked Save Changes.
That's it for the Facebook side. Now let's talk about the OAuth process in general. I'm not going to go very deep into this as OAuth has been discussed elsewhere and my focus here is to demonstrate a ColdFusion example, but at a high level, the process looks like this:
- Your site tells the user that you're going to send them to Facebook to authenticate/connect.
- You create a "special" link that includes some required crap in the URL. Along with the required crap, you will have some optional crap. So for example, many OAuth providers ask you to spell out exactly what you want to use from the user. I.e., how much private data you require. Your link will include that, and Facebook will then warn the user. I.e., "This site wants to take your lunch money, read your email, and have relations with your significant other."
- The user clicks and ends up at Facebook.com with a app-specific screen there. See the previous bullet point on how that screen may change.
- The user clicks Yes or No (or approve or whatever).
- Facebook sends you back to your site. In the URL will be a flag that you can check that will tell you if the user allowed your app. If they did, you will also have a special code.
- You take that code, make a request (using CFHTTP) to Facebook, to get a secret access token.
- This access token then allows you to get stuff. What stuff depends on what you asked for (see bullet point two).
That's it - roughly - and what's cool is that you will see this exact same (for the most part) process over my next blog entries as well.
With that in mind, let's look at the code. Again, I want to warn you this is a bit rough. First, the Application.cfc:
<cfcomponent output="false"> <cfset this.name = "cfb8fbG"> <cfset this.sessionManagement = true> <cffunction name="onApplicationStart" output="false"> <cfset application.fbappid = "foo"> <cfset application.fbsecret = "goo"> <cfset application.fbredirecturl = "http://localhost/testingzone/cf8fb/redir.cfm"> </cffunction> <cffunction name="onRequestStart" output="false"> <cfif isDefined("url.init")> <cfset onApplicationStart()> </cfif> </cffunction> </cfcomponent>
Obviously the app ID and app secret are removed above. The redirect URL will be used in a bit. Note that I've enabled session management as well. Now let's look at index.cfm.
<cfif not isDefined("session.fbcode")> <cfif isDefined("url.startfb")> <cfset session.state = createUUID()> <cflocation url="https://www.facebook.com/dialog/oauth?client_id=#application.fbappid#&redirect_uri=#application.fbredirecturl#&state=#session.state#&scope=friends_hometown" addtoken="false"> </cfif> <a href="?startfb=1">Login with FB</a> <cfelse> This is a FB user. <cfset session.fbAPI = createObject("component","facebook").init(session.fbaccesstoken)> <cfdump var="#session.fbAPI.getMe()#" label="me" expand="false"> <cfdump var="#session.fbAPI.getFriends()#" label="friends"> </cfif>
There's two parts to this template. The first portion is run when you first get to the site and haven't connected to Facebook yet. I'm using a simple Session variable to track that. I provide a quick prompt and when clicked, I send the user to Facebook. Upon reflection, how I did this was kind of stupid. I could have just had the link in the original HTML link.
Ok, so what happens when you click? Here is a screen shot.
Now let's look at redir.cfm, the main handler for the result from Facebook.
<cfif isDefined("url.code") and url.state is session.state> <cfset session.fbcode = url.code> <cfhttp url="https://graph.facebook.com/oauth/access_token?client_id=#application.fbappid#&redirect_uri=#urlEncodedFormat(application.fbredirecturl)#&client_secret=#application.fbsecret#&code=#session.fbcode#"> <cfif findNoCase("access_token=", cfhttp.filecontent)> <cfset parts = listToArray(cfhttp.filecontent, "&")> <cfset at = parts> <cfset session.fbaccesstoken = listGetAt(at, 2, "=")> <cflocation url="index.cfm" addtoken="false"> <cfelse> <!--- This is an error case. ---> <cfdump var="#cfhttp#"> </cfif> <cfelseif isDefined("url.error_reason")> <!--- Handle error here. Variables are: url.error_reason and error_description ---> </cfif>
We begin by assuming that Facebook has return a code variable to us in the query string. We also validate the state variable I mentioned earlier. At this point, we need to get the Access Token from Facebook. This is done with a simple CFHTTP call. Note we pass back in the redirect URI. We aren't going back to redir.cfm again, this is just part of the security system.
If everything worked out ok, the result will be a string that looks like this:
That's where the string parsing code above comes into play. The access_token is what will give us access to the user's data.
If you return to the code sample above for index.cfm, you will notice that I've got a Facebook component tied to the user session. I can pass in the access token there and make use of it for future calls. I wrote this component very quickly. It doesn't have nice error handling or even pagination, but you can see an example of getting my profile as well as my friends.
<cfcomponent output="false"> <cffunction name="init" access="public" returnType="facebook" output="false"> <cfargument name="accesstoken" type="string" required="true"> <cfset variables.accesstoken = arguments.accesstoken> <cfreturn this> </cffunction> <cffunction name="getFriends" access="public" returnType="array" output="false"> <cfset var httpResult = ""> <cfset var initialResult = ""> <cfhttp url="https://graph.facebook.com/me/friends?fields=name,hometown&access_token=#variables.accesstoken#" result="httpResult"> <cfset initialResult = deserializeJSON(httpResult.filecontent)> <!--- For now, skipping pagination. ---> <cfreturn initialResult.data> </cffunction> <cffunction name="getMe" access="public" returnType="struct" output="false"> <cfset var httpResult = ""> <cfhttp url="https://graph.facebook.com/me?access_token=#variables.accesstoken#" result="httpResult"> <cfreturn deserializeJSON(httpResult.filecontent)> </cffunction> </cfcomponent>
I didn't spend much time on their API, but it seemed pretty darn easy to use.
You may be wondering - during testing - how do I get that permission screen to show up? You want to go to your Facebook Privacy settings, and then Manage Apps. This can be confusing because there is another link for this that brings you to your apps as a developer. You want to ensure you come here via your privacy settings, which will then help you manage your user app settings. Here's mine:
See my test app there on top? If you click the delete icon (x), you get this prompt:
This sounds like it may delete the app completely, but to be clear, this is user specific only. It is safe to confirm and test your application again.
Anyway, I hope this helps. I want to be clear that there's more involved here than what I've shown, but I wanted to share my sample app in the hopes others could use it.