#ZendAMF: Simple authorization

0. List of contents

1. Requirements

2. Goal

We’re going to write two applications:
1. Zend Framework based application which will be AMF endpoint and will be used as authorization service
2. AIR/Actionscript client

3. Security

Extra security is provided by using ACL list and md5 password encryption (password is joined with serverside generated token).

4. 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 table `users`
--

CREATE TABLE IF NOT EXISTS `users` (
  `id` int(11) NOT NULL auto_increment,
  `email` varchar(100) NOT NULL,
  `username` varchar(100) NOT NULL,
  `password` varchar(32) NOT NULL,
  `role` varchar(16) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;

INSERT INTO `users` (`id`, `email`, `username`, `password`, `role`) VALUES
(1, 'admin@admin.com', 'admin', '21232f297a57a5a743894a0e4a801fc3', 'Administrator');

5. 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

6. Bootstrap.php

The most important part of the boostrap file is _initCoreSession method which initializes a user session. The rest is/may be optional.

<?php
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;
    }
}

7. 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.

<?php

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;
	}
}

8. LoginService.php

<?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;
        }
    }
}
?>

9. 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)).

<?php
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;
    }
}
?>

10. LoginVO.php

LoginVO is an object which has to be declared either on server and client side.

<?php
class LoginVO{
    public $_explicitType = 'LoginVO';
    public $username;
    public $signature;
}
?>

11. 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="admin@admin.com" />
			</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>

12. Demo

13. Download

PHP files
Flex Project

Leave a Comment.