Facebook SSO For Flash & Mobile AIR Apps

facebooksso

This implementation is taken from TrainLord as it runs on web (flash), android and iOS (AIR) platforms. Facebook’s authentication flows are based on the OAuth 2.0 protocol. Its recommended you have general undesrtanding of OAuth protocol prior reading this article.

If you run services on your own backend (like TrainLord does), the main point of whole thing is to securely pair a facebook id and user session. So later, whenever user does a request to your backend it automatically recoginzes the facebook id. In my case I used id and name to create an unique user record in my database so I can save some more user related data (score, etc.).

Facebook SSO For Flash (web)

First, start with Facebook Developer App setup. The important part here is to define “App on Facebook” and “Canvas URL” to an url where you expect the login popup to be redirected, once user is logged in:

http://trainlord.com/facebook.php?

I decided to implement multiple features in my facebook.php:

  • redirect to facebook login dialog
  • validate response from facebook login and pair facebook user with current session
  • get user from session if possible
  • handle errors
  • automatically closes itself (popup)
<?php
if(isset($_GET["error"]))
{
	$error = isset($_GET["error_description"]) ? $_GET["error_description"] : $_GET["error"];
	?>
	<!doctype html>
	<html>
	<header>
		<script type="text/javascript">
			window.opener.RTW.connect.Facebook.error("<?php echo $error; ?>");
			self.close();
		</script>
	</header>
	<body><h1 class="error"><?php echo $error; ?></h1></body>
	</html>
	<?php
	exit;
}

require_once "commons.php";
require_once "Authentication.php";
require_once 'facebook/facebook.php';
require_once 'Connect.php';

Zend_Session::start();

$facebook = new Facebook(array(
	'appId'  => FACEBOOK_APPID,
	'secret' => FACEBOOK_SECRET,
));

$userId = $facebook->getUser();

if($userId)
{
	try
	{
		$user = $facebook->api('/me');
	}
	catch(FacebookApiException $e)
	{
		error_log($e);
		$userId = null;
	}
}

if(!$userId)
{
	Authentication::setUserId(null);
	$loginUrl = $facebook->getLoginUrl(array('display' => 'popup'));
	header("location:" . $loginUrl);
	exit;
}

Authentication::setUserId(Connect::facebook($user));

?>
<!doctype html>
<html>
<header>
	<script type="text/javascript">
		window.opener.RTW.connect.Facebook.success();
		self.close();
	</script>		
</header>
<body><h1>Success</h1></body>
</html>

Dependencies:

  • commons.php – just some definitions like FACEBOOK_APPID,FACEBOOK_SECRET
  • Authentication.php – a wrapper over php session
  • facebook/facebook.php – facebook php sdk
  • Connect.php – Connect::facebook() creates (or reuses) and returns a specific user id based on facebook user id

Client side implementation Facebook.as handles the following:

  • defines some javascript methods and callbacks
  • opens facebook.php in popup
  • handles errors
package sk.yoz.rtw.connect
{
    import flash.external.ExternalInterface;
    import org.osflash.signals.Signal;

    public class Facebook
    {
        private static const SCRIPT:XML = <script>
            <![CDATA[
                function()
                {
                    window.RTW = {
                        getFlash: function(){
                            return document.getElementById("${objectID}");
                        },
                        connect: {
                            Facebook: {
                                connect: function(url){
                                    window.open(url, "facebook", "width=670,height=370");
                                },
                                success: function(){
                                    window.RTW.getFlash().connectFacebookSuccess();
                                },
                                error: function(message){
                                    window.RTW.getFlash().connectFacebookError(message);
                                }
                            }
                        }
                    }
                }
            ]]>
            </script>;
        
        public var signalSuccess:Signal = new Signal;
        public var signalError:Signal = new Signal(String);
        
        public function connect():void
        {
            ExternalInterface.call(SCRIPT.replace("${objectID}", ExternalInterface.objectID));
            ExternalInterface.addCallback("connectFacebookSuccess", signalSuccess.dispatch);
            ExternalInterface.addCallback("connectFacebookError", signalError.dispatch);
            ExternalInterface.call("RTW.connect.Facebook.connect", "http://trainlord.com/facebook.php");
        }
    }
}

Notice that Facebook.as defines javascript functions on current window and facebook.php (opened in popup) reuses those using window.opener scope. Facebook.as has now pretty straightforward use:

var facebook:Facebook = new Facebook();
facebook.signalSuccess.add(function():void
{
    // logged in sucessfully
});
facebook.signalError.add(function(message:String):void
{
    // login failed with some error
});
facebook.connect();

Facebook SSO For AIR (iOS, Android)

Even you could use StageWebView (and original facebook.php), there are some ANEs available for Facebook SSO, that does some native heavy lifting. I decided to use the one provided by frehplanet. This works for iOS and Android using the same sources. I still needed some backend side validation but a bit simplier this time in facebook2.php:

<?php
require_once "Authentication.php";
require_once 'Connect.php';

Zend_Session::start();

$response = file_get_contents("https://graph.facebook.com/me?access_token=" . $_GET['accessToken']);
$data = json_decode($response);

if(!$data->id)
{
	Authentication::setUserId(null);
	die('Unexpected authorization error.');
}

Authentication::setUserId(Connect::facebook(array(
	'id' => $data->id,
	'name' => $data->name,
	'username' => $data->username)));
	
die('ok');

The supplied accessToken provides method secure enough for validating the user. It responses with error message (if improperly used) or with ok. Client side implementation for Facebook.as with freshplanet dependency:

package sk.yoz.rtw.connect
{
    import com.freshplanet.ane.AirFacebook.Facebook;
    
    import flash.events.Event;
    import flash.net.URLLoader;
    import flash.net.URLRequest;
    import flash.net.URLRequestMethod;
    import flash.net.URLVariables;
    
    import org.osflash.signals.Signal;

    import sk.yoz.rtw.Setup;
    
    public class Facebook
    {
        public var signalSuccess:Signal = new Signal;
        public var signalError:Signal = new Signal(String);

        private var facebook:com.freshplanet.ane.AirFacebook.Facebook;
        private var inited:Boolean;

        private function init():void
        {
            inited = true;
            facebook = com.freshplanet.ane.AirFacebook.Facebook.getInstance();
            facebook.init(Setup.FACEBOOK_APP_ID);
        }
        
        public function connect():void
        {
            if(!inited)
                init();
            
            if(!com.freshplanet.ane.AirFacebook.Facebook.isSupported)
            {
                signalError.dispatch("Connect only supported in AIR.");
                return;
            }
            
            if(!facebook.isSessionOpen)
                return facebook.openSessionWithReadPermissions([], requestMe);
            
            requestMe();
        }
        
        private function requestMe(...rest):void
        {
            facebook.requestWithGraphPath("/me", null, URLRequestMethod.GET, onRequestMeResponse);
        }
        
        private function onRequestMeResponse(response:Object):void
        {
            var variables:URLVariables = new URLVariables;
            variables.accessToken = response.accessToken;
            
            var request:URLRequest = new URLRequest;
            request.url = "http://trainlord.com/facebook2.php";
            request.data = variables;
            request.method = URLRequestMethod.GET;
            
            var loader:URLLoader = new URLLoader;
            loader.addEventListener(Event.COMPLETE, onLoaderComplete);
            loader.load(request);
        }
        
        private function onLoaderComplete(event:Event):void
        {
            var loader:URLLoader = event.target as URLLoader;
            var response:String = loader.data;
            if(response == "ok")
                signalSuccess.dispatch();
            else
                signalError.dispatch(response);
        }
    }
}

We can handle this class the same way we did with the one for web:

var facebook:Facebook = new Facebook();
facebook.signalSuccess.add(function():void
{
    // logged in sucessfully
});
facebook.signalError.add(function(message:String):void
{
    // login failed with some error
});
facebook.connect();

In next acrticles I will cover Twitter and Google SSO for web and AIR apps.

Where to go from here:

Leave a comment

Please be polite and on topic. Your e-mail will never be published.