Zend AMF Authentication & Authorization

dkozar evolved a working method to Authenticate and Authorize a Flex based app datas service call using Zend AMF, he writes;

I’ve been struggling with it, and figured it all out – so, perhaps it could help others.

The authentication is called on the server only if credentials supplied from the client (via the remote procedure call headers). This snippet illustrates the setup of custom auth (these are the last 6 lines of gateway.php script):

// Handle request
$auth = new My_Amf_Auth(); // authentication
$server->setAuth($auth);
$acl = new Zend_Acl(); // authorization
$server->setAcl($acl);
echo $server->handle();

Now, your custom auth should extend Zend_Amf_Auth_Abstract. Since I want to authenticate users from a database, I bring the Zend_Auth_Adapter_DbTable to play. But since I cannot extend both Zend_Amf_Auth_Abstract and Zend_Auth_Adapter_DbTable, I use a composition:

< ?php require_once ('Zend/Amf/Auth/Abstract.php');/** * AMF auth class by Danko Kozar, dankokozar.com * @author dkozar * */ class My_Amf_Auth extends Zend_Amf_Auth_Abstract { function __construct() { } public function authenticate() { $adapter = My_Db_Adapter::getInstance(); $adapter->setIdentity($this->_username);
$adapter->setCredential($this->_password);

// the adapter call
// you can wrap it into try.. catch and process DB connection errors
$result = Zend_Auth::getInstance()->authenticate($adapter);

return $result;
}
}

Here’s the adapter class:

< ?php /** * DB table adapter auth class for AMF by Danko Kozar, dankokozar.com * @author dkozar * Singleton */ class My_Db_Adapter extends Zend_Auth_Adapter_DbTable { protected static $_instance = null; /** * private! * @param My_Db_Adapter $adapter */ public function __construct(Zend_Db_Adapter_Abstract $adapter = null) { if (!$adapter) $adapter = new Zend_Db_Adapter_Mysqli( array( 'dbname' => 'test',
'username' => 'root',
'password' => '')
);

parent::__construct($adapter);

$this
->setTableName('users')
->setIdentityColumn('username')
->setCredentialColumn('password')
;

// just for testing
// $this
// ->setIdentity('username')
// ->setCredential('password')
// ;
}

/**
* @return My_Db_Adapter
*/
public static function getInstance()
{
if (null === self::$_instance) {
self::$_instance = new self();
}
return self::$_instance;
}

public function authenticate() {

$_authResult = parent::authenticate();

// NOTE: The point is that $result->_identity is an OBJECT (of type stdClass), NOT string
// with Zend_Auth_Adapter_DbTable it is internally accomplished by calling its getResultRowObject() method
// It constructs the stdClass with properties named after table attributes

// $user = new stdClass();
// $user->role = "administrator";
// $user->username = $_authResult->getIdentity();

$identity = $this->getResultRowObject();

$result = new Zend_Auth_Result($_authResult->getCode(), $identity);

return $result;
}
}

MyService.php class. Here it is:


< ?php /** * PHP service class with authorization * by Danko Kozar, dankokozar.com * @author dkozar * */ class MyService { /** * from zend docs: * If the ACL object is set, and the class being called defines initAcl() method, * this method will be called with the ACL object as an argument. * This method can create additional ACL rules and return TRUE, * or return FALSE if no access control is required for this class. * * @param Zend_Acl $acl * @return boolean */ public function initAcl($acl) { $acl->addRole(new Zend_Acl_Role("administrator"));
$acl->addRole(new Zend_Acl_Role("user"));

//acl "allow" method takes 3 parameters (role, resource - class name, privileges - it's function name in this class)

// administrator
$acl->allow('administrator', 'MyService', 'helloWorld');
$acl->allow('administrator', 'MyService', 'getData');

// user
$acl->allow('user', 'MyService', 'helloWorld');
$acl->deny('user', 'MyService', 'getData');

//returning true to signal that we want to check privileges before accessing methods of this class
//in my tests if we don't return anything it will treat it like we will return false so better return true or false
//your intentions will be clear
return true;
}

/**
* Hello world method
*/
public function helloWorld(){
return "Hello world from MyService service";
}

/**
*
* Returns data
* @return [int]
*/
function getData()
{
$arr = array(1, 2, 3);
return $arr;
}
}
?>

Note that the authorization is being built dynamically inside the initAcl method.

On the Flex side I have an auto-generated class (MyService) which extends another auto-generated class (_Super_MyService).

The point is that the outer one is auto-generated only once (initially), and you can modify it, without worrying to be overwritten on service regeneration.

There’s a protected property _serviceControl (which is of type RemoteObject) which could be tweaked if needed.

I’m tweaking it by of setting the endpoint (with string read from a client side config in preInitializeService() method). Plus, I’m adding 2 more methods, which expose setCredentials and setRemoteCredentials methods of _serviceControl, so I can acces it from my code.


package services.myservice
{
public class MyService extends _Super_MyService
{
/**
* Override super.init() to provide any initialization customization if needed.
*/
protected override function preInitializeService():void
{
super.preInitializeService();

// Initialization customization goes here
_serviceControl.endpoint = "http://localhost/myapp/gateway.php";
}

public function setCredentials(username:String, password:String, charset:String=null):void
{
_serviceControl.setCredentials(username, password, charset);
}

public function setRemoteCredentials(username:String, password:String, charset:String=null):void
{
_serviceControl.setRemoteCredentials(username, password, charset);
}
}
}


So, before calling MyService methods, I’m setting the credentials with setCredentials() method and this runs the authentication on the PHP side:


private var service:MyService;
....
service = new MyService(); // ServiceLocator.getInstance().getHTTPService("presetLoader");
service.setCredentials("user1", "pass1");
var token:AsyncToken = service.getData();

The authentication via Zend_Amf_Server is, by the way, OPTIONAL! Meaning, with no credentials supplied, Zend_Amf_Server will NOT RUN IT. Thus you should rely on Zend_Acl (e.g. roles) to so your permissions and security!

Finally, here’s the MySQL DB table I’ve been using for authentication:

--
-- Table structure for table `users`
--
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(32) DEFAULT NULL,
`role` varchar(45) DEFAULT NULL,
`firstname` varchar(50) DEFAULT NULL,
`lastname` varchar(50) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `id_UNIQUE` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;

--
-- Dumping data for table `users`
--
INSERT INTO `users` (`id`, `username`, `password`, `role`, `firstname`, `lastname`, `email`) VALUES
(1, 'user1', 'pass1', 'administrator', 'Danko', 'Kozar', NULL);

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Cheers!
Danko

Adobe Forums

Namespacing ACL resources and Galahad_Acl

Chris Morrell writes; In most of my applications I like to handle authorization (querying the ACL) in one (or more) of three ways:

  • Authorize access to a model’s method
  • Authorize access to a controller action
  • Authorize access to an arbitrary “permission”

In general I find it’s best to keep authorization within the domain (querying the ACL within my models when they’re accessed) as this provides the most consistent behavior. For example, if I eventually add a REST API to my application I don’t have to duplicate all my authorization logic in the new REST controllers. When the application calls something like Default_Model_Post::save() it either saves or throws an ACL exception, no matter where it was called from. This is great in that it saves me from having to duplicate code and keeps my system more secure.

On the other hand, there are times when it’s just a lot easier to handle authorization in the controller. For example, if guests should never access my “Admin” module, it doesn’t make sense to ever let them access /admin/ URLs. Also, if you’re using Zend_Navigation, having ACL resources that match controller actions lets you utilize its ACL integration.

If you’re ever going to mix these two techniques, you’ll eventually bump into the case where a model and a controller share the same name. What if you need to set permissions on a “user” controller and different permissions on a “user” model? This is where namespacing comes into play. As suggested by the Zend Framework manual, I always name my controller action resources in the format mvc:module.controller.action. I name my model resources similarly, in the format model:module.modelName.methodName. In both theses cases, “mvc” and “model” are the namespace, and everything following the colon is the actual resource name. Now I can refer to my “admin” module as mvc:admin and the models within my admin module as model:admin.

This is where things get interesting.

Read on; “Namespacing ACL resources & Galahad_Acl”.

Chris continues; Right now I treat my models as resources with a special exception for the user model which is both a resource and a role. Then I actually make the models responsible for managing their own ACL permissions, both setting them up and querying them. To facilitate that, I have a base model class that does a few things. First it has a way to inject an ACL instance into the model as well as a way to pass an ACL instance as the default ACL for all models (which I do in my bootstrap). Second it automatically adds itself to that ACL (with the resource id model:moduleName.modelName). Finally, I have an _initAcl() method which is called when my model is instantiated which adds the appropriate rules to the ACL if they don’t already exist. Whenever my model is doing something that is access-controlled I check the ACL right then.

Here’s a simple code example:


class Default_Model_Post extends Galahad_Model_Entity
{
protected function _initAcl($acl)
{
// Deny permissions to anything on this model unless explicitly allowed
$acl->deny(null, $this);

// Allow guests to fetch the content of posts
$acl->allow('guest', $this, 'fetch');

// Allow admins to save changes to posts
$acl->allow('admin', $this, 'save');
}

public function save()
{
if (!$this->getAcl()->isAllowed($this->getRole(), $this, 'save')) {
throw new Galahad_Acl_Exception('Current user is not allowed to save posts.');
}

$dataMapper = $this->getDataMapper();
return $dataMapper->save($this);
}
}

There’s a little bit more happening in there (for example, I also have helper methods like getRole(), which either gets the role ID stored in the model or grabs it from Zend_Auth if available), but you should get the picture.

That way my access control is happening when the access itself is happening. No matter how my model is used, the ACL is always queried right when it matters. I also like setting up the ACL this way because all rules are loaded into the the ACL only when they could potentially apply (you never need the rules for a Post model if the current request never even loads the Post class).

This is something I’ve been thinking about a lot lately, and I’m just settling into this method. I just blogged about it a little over a week ago: “Namespacing ACL resources & Galahad_Acl” — if anyone has any comments I’d love to hear them

Logging in Users using Doctrine and Zend_Auth

Jon Lebensold publishes the second part of his series on using Doctrine in combination with Zend_Auth & Zend_Auth_Adaptor; Here’s the second part of my Doctrine / Zend_Auth example. In 15 minutes, we create a logout, login and protected area that’s reliant on the ZC_Auth_Adapter adapter we created in last week’s video. Notice how there’s no code in the IndexController exposing the authentication implementation,

Grab a copy of the project or browse the repository.

Check it out here; Zendcasts.

Writing a Zend_Auth_Adapter with Doctrine

Jon Lebensold publishes another installment of his popular screen cast series, he writes; I’ve been using Doctrine a lot in my own work, and recently found myself itching to have tighter integration between Zend and Doctrine when it comes to user logins. Luckily, Zend provides a very simple interface with regards to Zend_Auth. This way, it’s easy to decouple your persistence layer (in my case Doctrine) from the authentication layer. I’ve borrowed from Palo Verede’s wonderful article on Doctrine and Zend_Auth and I invite you to check his blog out.

Grab a copy of the project or browse the repository.

Look at the video and comment here;  Zendcasts.

Zend_Acl & Zend_Navigation

Setting up a simple working example of Acl & Navigation in Zend Framework 1.9.x as demonstrated by jscherer26.
Enjoy

models/Acl.php

< ?phpclass Model_Acl extends Zend_Acl { public function __construct() {// define Roles $this->addRole(new Zend_Acl_Role('guest')); // not authenicated
$this->addRole(new Zend_Acl_Role('member'), 'guest'); // authenticated as member inherit guest privilages
$this->addRole(new Zend_Acl_Role('admin'), 'member'); // authenticated as admin inherit member privilages

// define Resources
$this->add(new Zend_Acl_Resource('error'));
$this->add(new Zend_Acl_Resource('index'));
$this->add(new Zend_Acl_Resource('authentication'));
$this->add(new Zend_Acl_Resource('activity'));

// assign privileges
$this->allow('guest', array('index','error'));
$this->allow('guest', 'authentication', array('index','signin'));

$this->allow('member', 'authentication', array('index','signout'));
$this->deny( 'member', 'authentication', 'signin');
$this->allow('member', 'activity', array('index','list')); // member has list privilages for resource activity

$this->allow('admin', 'activity'); // admin has all privileges for resource activity
}
}

plugins/Authenticated.php

< ?php class Plugin_Authenticated extends Zend_Controller_Plugin_Abstract { private $_acl = null; private $_auth = null;public function __construct(Zend_Acl $acl, Zend_Auth $auth) { $this->_acl = $acl;
$this->_auth = $auth;
}

public function preDispatch(Zend_Controller_Request_Abstract $request)
{
$resource = $request->getControllerName();
$action = $request->getActionName();

$role .= $this->_auth->getStorage()->read()->role;
if(!$this->_acl->isAllowed($role, $resource, $action)) {
$request->setControllerName('authentication')
->setActionName('notauthorized');
}
}
}

Bootstrap.php

< ?phpclass Bootstrap extends Zend_Application_Bootstrap_Bootstrap { private $_acl = null; private $_auth = null;protected function _initAutoload() { $modelLoader = new Zend_Application_Module_Autoloader(array( 'namespace' => '',
'basePath' => APPLICATION_PATH));

$this->_acl = new Model_Acl;
$this->_auth = Zend_Auth::getInstance();
if(!$this->_auth->hasIdentity()) {$this->_auth->getStorage()->read()->role = 'guest';}

$fc = Zend_Controller_Front::getInstance();
$fc->registerPlugin(new Plugin_Authenticated($this->_acl, $this->_auth));

return $modelLoader;
}

function _initViewHelpers()
{
$this->bootstrap('layout');
$layout = $this->getResource('layout');
$view = $layout->getView();

$config = new Zend_Config_Ini(APPLICATION_PATH .'/configs/application.ini', APPLICATION_ENV);

$view->doctype('HTML4_STRICT');
$view->headMeta()->appendHttpEquiv('Content-Type', 'text/html; charset=UTF-8')
->appendHttpEquiv('Content-Language', 'en-US')
->appendName('keywords', $config->head->meta->keywords)
->appendName('description', $config->head->meta->description);

$view->headLink()->appendStylesheet($config->head->css->site)
->appendStylesheet($config->head->css->menu)
->appendStylesheet($config->head->css->form)
->appendStylesheet($config->head->css->view);

$view->headTitle()->setSeparator(' - ');
$view->headTitle($config->head->title);

}

function _initNavigation()
{
$this->bootstrap('layout');
$layout = $this->getResource('layout');
$view = $layout->getView();

$navConfig = new Zend_Config_Xml(APPLICATION_PATH . '/configs/navigation.xml', 'nav');
$navigation = new Zend_Navigation($navConfig);

$view->navigation($navigation)->setAcl($this->_acl)
->setRole($this->_auth->getStorage()->read()->role);

}

}

configs/Navigation.xml

< ?xml version="1.0" encoding="UTF-8"?>