Requirements
- PHP & ActionScript programming skills
- Adobe Flex Builder
- Adobe Flex SDK (4.5 recommended)
- Adobe Air SDK (2.6 recommended)
- AS3 Corelib
- Zend Framework
- MySQL database
Goal
We’re going to write two applications:
- Zend Framework based application which will be AMF endpoint and will be used as authorization service
- AIR/Actionscript client
Security
Extra security is provided by using ACL list and md5 password encryption (password is joined with serverside generated token).
Database configuration
In MySQL database we’ll store users session and general information such as login, password etc. So our database will look more or less like:
CREATE TABLE IF NOT EXISTS
sessions
(id
char(32) collate utf8_polish_ci NOT NULL default '',modified
int(11) default NULL,lifetime
int(11) default NULL,data
text collate utf8_polish_ci, PRIMARY KEY (id
) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_polish_ci; -- -- Table structure for tableusers
-- CREATE TABLE IF NOT EXISTSusers
(id
int(11) NOT NULL auto_increment,username
varchar(100) NOT NULL,password
varchar(32) NOT NULL,role
varchar(16) NOT NULL, PRIMARY KEY (id
) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; INSERT INTOusers
(id
,username
,password
,role
) VALUES (1, '[email protected]', 'admin', '21232f297a57a5a743894a0e4a801fc3', 'Administrator');
Zend Framework configuration – application.ini
[production]
phpSettings.display_startup_errors = 0
phpSettings.display_errors = 0
includePaths.library = APPLICATION_PATH "/../library"
bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
bootstrap.class = "Bootstrap"
resources.frontController.params.displayExceptions = 0
resources.frontcontroller.env = APPLICATION_ENV
resources.layout.layout = "layout"
resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts"
resources.view.encoding = "UTF-8"
resources.view.basePath = APPLICATION_PATH "/views/"
# Database settings (user session will be stored in database)
resources.db.adapter = "pdo_mysql"
resources.db.params.host = "your_host"
resources.db.params.username = "your_database_username"
resources.db.params.password = "your_password"
resources.db.params.dbname = "your_database_name"
resources.db.isDefaultTableAdapter = true
# Session properties
resources.session.use_only_cookies = true
resources.session.gc_maxlifetime = 864000
resources.session.remember_me_seconds = 864000
resources.session.name = "_s"
resources.session.use_trans_sid = false
resources.session.cookie_lifetime = 7200
resources.session.cookie_httponly = false
resources.session.saveHandler.class = "Zend_Session_SaveHandler_DbTable"
resources.session.saveHandler.options.name = "sessions"
resources.session.saveHandler.options.primary = "id"
resources.session.saveHandler.options.modifiedColumn = "modified"
resources.session.saveHandler.options.dataColumn = "data"
resources.session.saveHandler.options.lifetimeColumn = "lifetime"
appnamespace = "Application"
resources.frontcontroller.controllerDirectory = APPLICATION_PATH "/controllers"
resources.frontController.actionHelperPaths.acl = "Application_Controller_Helper_Acl"
acl.resources[] = "index_index"
acl.privileges.Guest[] = "index_index"
[development]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1
resources.frontController.params.displayExceptions = 1
resources.frontController.throwExceptions = 1
Bootstrap.php
The most important part of the boostrap file is _initCoreSession method which initializes a user session. The rest is/may be optional.
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap{
//Initialize routes, adress "http://yourdomain.com/404" will redirect action to the error controller
protected function _initRoutes(){
$this->bootstrap('FrontController');
$front = $this->getResource('FrontController');
$router = $front->getRouter();
$router->addRoute('error404', new Zend_Controller_Router_Route('404', array('controller' => 'error', 'action'=> 'error404')));
$router->addRoute('error403', new Zend_Controller_Router_Route('403', array('controller' => 'error', 'action'=> 'error403')));
}
//Include helpers directory
protected function _initHelperPath(){
Zend_Controller_Action_HelperBroker::addPath(APPLICATION_PATH . '/controllers/helpers', 'Application_Controller_Action_Helper_');
}
//Initialize session for each user (session is stored in database)
protected function _initCoreSession(){
$this->bootstrap('db');
$this->bootstrap('session');
try {
Zend_Session::start();
} catch(Zend_Session_Exception $e) {
Zend_Session::regenerateId(false);
Zend_Session::destroy(true);
}
Zend_Session::registerValidator(new Zend_Session_Validator_HttpUserAgent());
}
//Initialize the view
protected function _initView(){
$view = new Zend_View();
$view->doctype('XHTML1_STRICT');
$view->headTitle('AMF Auth');
$view->env = APPLICATION_ENV;
$viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('ViewRenderer');
$viewRenderer->setView($view);
return $view;
}
}
AmfController.php
The AmfController handles AIR client connections. If you want to check whether your amf endpoint works properly, try "curl http://your_host/amf" and you should get "Zend Amf Endpoint" inside of html the "p" tag.
Zend_Loader::loadClass("LoginService", APPLICATION_PATH.'/controllers/amf/services');
Zend_Loader::loadClass("LoginVO", APPLICATION_PATH.'/controllers/amf/vo');
Zend_Loader::loadClass("UserVO", APPLICATION_PATH.'/controllers/amf/vo');
class AmfController extends Zend_Controller_Action{
//Disable layout, it's useless
public function init(){
$this->_helper->layout()->disableLayout();
$this->_helper->viewRenderer->setNoRender();
}
//Initialize and run the AMF Server
public function indexAction() {
$server = new Zend_Amf_Server();
$server->setSession();
$server->setProduction(false);
$server->setAcl($this->_initAcl());
$server->setClass("LoginVO");
$server->setClass("UserVO");
$server->setClass("LoginService");
$server->setClassMap("LoginVO","LoginVO");
$server->setClassMap("UserVO","UserVO");
echo $server->handle();
}
private function _initAcl(){
$acl = new Zend_Acl();
$acl->addRole(new Zend_Acl_Role('Guest'));
$acl->addRole(new Zend_Acl_Role('User'),'Guest');
$acl->addRole(new Zend_Acl_Role('Moderator'), 'User');
$acl->addRole(new Zend_Acl_Role('Administrator'), 'Moderator');
$acl->addResource(new Zend_Acl_Resource('LoginService'));
$acl->allow('Guest','LoginService',array('getToken','login','logoutService','isLogged'));
$acl->allow('Administrator');
return $acl;
}
}
LoginService.php
class LoginService {
function login(LoginVO $data) {
if(Zend_Auth::getInstance()->hasIdentity()){
Zend_Auth::getInstance()->clearIdentity();
}
Zend_Loader::loadClass("DbTable", APPLICATION_PATH.'/controllers/amf');
$authAdapter = new DbTable(Zend_Db_Table::getDefaultAdapter());
$authAdapter->setTableName('users')
->setIdentityColumn('username')
->setCredentialColumn('password');
$username = htmlspecialchars($data->username);
//Is username a valid email address?
if(preg_match("/^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/", $username)){
$username = strstr($username, '@', true);
}
$authAdapter->setIdentity($username)
->setCredential(htmlspecialchars($data->signature));
$auth = Zend_Auth::getInstance();
$result = $auth->authenticate($authAdapter);
if($result->isValid()){
$userInfo = $authAdapter->getResultRowObject(null, 'password');
if($userInfo->role !== 'Administrator'){
$this->logoutService();
throw new Zend_Amf_Exception("LoginService.AUTHORISATION_FAIL.You are not permitted");
}
$authStorage = $auth->getStorage();
$authStorage->write($userInfo);
$userVO = new UserVO();
$userVO->id = $userInfo->id;
$userVO->email = $userInfo->email;
$userVO->role = $userInfo->role;
return $userVO;
}else{
throw new Zend_Amf_Exception("LoginService.AUTHORISATION_FAIL.You are not authorised");
}
}
function isLogged(){
return Zend_Auth::getInstance()->hasIdentity();
}
function getToken(){
$_SESSION['challenge'] = uniqid(rand()+time(),true);
return $_SESSION['challenge'];
}
function logoutService(){
$auth = Zend_Auth::getInstance();
if($auth->hasIdentity()){
$auth->clearIdentity();
return true;
}else{
return false;
}
}
}
DbTable.php
DbTable is a child of the Zend_Auth_Adapter_DbTable and is used to change default auth sql query. Instead of MD5(password) we have MD5(CONCAT(password,token)).
class DbTable extends Zend_Auth_Adapter_DbTable{
/**
* _authenticateCreateSelect() - This method creates a Zend_Db_Select object that
* is completely configured to be queried against the database.
*
* @return Zend_Db_Select
*/
protected function _authenticateCreateSelect()
{
// build credential expression
if (empty($this->_credentialTreatment) || (strpos($this->_credentialTreatment, '?') === false)) {
$this->_credentialTreatment = '?';
}
$credentialExpression = new Zend_Db_Expr(
'(CASE WHEN MD5(CONCAT(' .
$this->_zendDb->quoteInto(
$this->_zendDb->quoteIdentifier($this->_credentialColumn, true)
. " , '".$_SESSION['challenge']."')) = " . $this->_credentialTreatment, $this->_credential
)
. ' THEN 1 ELSE 0 END) AS '
. $this->_zendDb->quoteIdentifier(
$this->_zendDb->foldCase('zend_auth_credential_match')
)
);
// get select
$dbSelect = clone $this->getDbSelect();
$dbSelect->from($this->_tableName, array('*', $credentialExpression))
->where($this->_zendDb->quoteIdentifier($this->_identityColumn, true) . ' = ?', $this->_identity);
return $dbSelect;
}
}
LoginVO.php
LoginVO is an object which has to be declared either on server and client side.
class LoginVO{
public $_explicitType = 'LoginVO';
public $username;
public $signature;
}
Actionscript/Flex client
Client application contains a lot of files, so I won’t put them here. They’re on svn server or you can download them directly from the download section.
Main File:
``` <?xml version="1.0" encoding="utf-8"?> <s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" creationComplete="init()" width="800" height="600" currentState="BootForm" xmlns:controls="extra.spinner.*" xmlns:components="com.hillelcoren.components.*" xmlns:components1="org.flashcommander.components.*"> <fx:Script> <![CDATA[ import com.events.*; import com.services.LoginService; import model.*; import mx.collections.ArrayList; import mx.events.FlexEvent; import mx.events.StateChangeEvent; public var loginService:LoginService = new LoginService(); private function init():void { loginService.addEventListener(LoginServiceEvent.AUTHORISATION_SUCCESS,_enableMainForm); loginService.addEventListener(LoginServiceEvent.IS_LOGGED_TRUE, _enableMainForm); loginService.addEventListener(LoginServiceEvent.IS_LOGGED_FALSE, _enableLoginForm); loginService.addEventListener(LoginServiceEvent.LOGOUT, _enableLoginForm); loginService.addEventListener(LoginServiceEvent.AUTHORISATION_FAIL, function(e:LoginServiceEvent):void{ loginBtn.enabled = true; showAlert(e.message); }); loginService.addEventListener(UnknownEvent.UNKNOWN, function(e:UnknownEvent):void{ showAlert(e.message); }); loginService.addEventListener(LoginServiceEvent.CONNECTION_PROBLEM, function(e:LoginServiceEvent):void{ if(currentState === "BootForm"){ BootLabel.text = e.message; }else{ showAlert(e.message); } }); this.addEventListener(Event.CLOSING, onWindowClosing); } private function showAlert(message:String):void{ var panel:Panel = new Panel(); var button:Button = new Button(); var label:Label = new Label(); panel.verticalCenter = "0"; panel.horizontalCenter = "0"; panel.title = "Error"; label.text = message; label.y = 10; label.horizontalCenter = "0"; button.label = "OK"; button.y = 30; button.horizontalCenter = "0"; button.addEventListener(MouseEvent.CLICK, function(e:MouseEvent):void{ panel.visible = false; }); panel.addElement(label); panel.addElement(button); this.addElement(panel); } private function _enableMainForm(e:LoginServiceEvent):void{ if(currentState != "MainForm"){ currentState = "MainForm"; } } private function _enableLoginForm(e:LoginServiceEvent):void{ if(currentState != "LoginForm"){ currentState = "LoginForm"; } } private function onWindowClosing(event:Event):void{ if(loginService.isLogged){ event.preventDefault(); loginService.logout(); } } protected function loginForm_EnterState(e:FlexEvent):void{ loginBtn.enabled = true; passwordInput.text = ""; } protected function mainForm_EnterState(e:FlexEvent):void{ logoutBtn.enabled = true; } ]]> </fx:Script> <s:states> <s:State name="LoginForm" enterState="loginForm_EnterState(event)" /> <s:State name="MainForm" enterState="mainForm_EnterState(event)" /> <s:State name="BootForm" /> </s:states> <s:Panel id="loginPanel" title="Login Form" includeIn="LoginForm" verticalCenter="0" horizontalCenter="0"> <s:Form id="loginForm" width="400"> <s:FormItem label="Username"> <s:TextInput id="usernameInput" width="100%" text="[email protected]" /> </s:FormItem> <s:FormItem label="Password"> <s:TextInput id="passwordInput" width="100%" displayAsPassword="true" /> </s:FormItem> <s:FormItem> <s:Button id="loginBtn" label="Login" click="loginService.login(usernameInput.text,passwordInput.text); loginBtn.enabled = false"/> </s:FormItem> </s:Form> </s:Panel> <s:Group includeIn="MainForm" width="100%" height="100%"> <s:HGroup width="100%"> <s:Button id="logoutBtn" label="Logout" click="loginService.logout(); logoutBtn.enabled = false"/> </s:HGroup> </s:Group> <s:Panel title="BootForm" includeIn="BootForm" verticalCenter="0" horizontalCenter="0" height="95"> <s:Label id="BootLabel" text="Please wait..." y="45" horizontalCenter="0" /> <controls:Spinner numTicks="25" tickColor="green" y="5" size="35" id="spinner" horizontalCenter="0" /> </s:Panel> </s:WindowedApplication> ```
Demo
Sources: