Thursday, October 17, 2013

Authentication Restful Web Service with ZfcUser in Zend Framework 2

I'm a beginner to Zend Framework 2, so it take me like 4 days to create a module that do authentication service for my Android app. It's kind of painful for me because I cannot find any clear tutorial or example on doing those thing, maybe because it's too easy that no developer care to share it.
Luckily I found an issue thread in ZfcUser Git Project, and with some tutorial, I come to a solution that work for me. I know it's not the best way, but I want to share for anyone, beginner like me, find a simple way to do authentication and move on to more complex Zend stuff later.

Update: after a while of working with Zend, then I realized that I need not to use Restful for my authentication, I just need to use normal action that will get the POST and return a json file.
use Zend\View\Model\JsonModel;
// Your class 
// ...
public function jsonAction(){  
     // Your code  
     return new JsonModel(array(  
             'tag' => 'login',  
             'success' => 0,  
             'error' => 1,  
             'error_msg' => 0,//$this->failedLoginMessage,  
           ));  
   }  

Update 2: If you are thinking about Restful WS, you should investigate some about it first here . As I did from the beginning, trying to use Restful to do authentication is totally wrong. :)

So first, you create a "class AuthRestController extends AbstractRestfulController" like in the tutorial.
Here I do the login and register in a same post method, I use "tag" value in POST to distinguish them.

use Zend\Mvc\Controller\AbstractRestfulController;
use Zend\View\Model\JsonModel;
use Zend\Form\Form;
use Customer\Model\Customer;


class AuthRestController extends AbstractRestfulController {

    protected $userTable;
    protected $authservice;
    protected $userService;
    protected $loginForm;
    protected $failedLoginMessage = 'Incorrect email or password!';
    protected $failedRegisterMessage = 'User already existed';
    protected $failedMessage = 'Error occured';

    public function getAuthService() {
        if (!$this->authservice) {
            $this->authservice = $this->getServiceLocator()
                    ->get('AuthService');
        }

        return $this->authservice;
    }

    public function getList() {
        
    }

    public function get($id) {
        
    }

    public function create($dataIn) {
        // POST
        // Translating data
        $data = array(
            'tag' => $dataIn['tag'],
            'identity' => $dataIn['email'],
            'credential' => $dataIn['password'],
        );
        $this->getRequest()->getPost()->set('identity', $data['identity']);
        $this->getRequest()->getPost()->set('credential', $data['credential']);

        if ($data['tag'] == 'login') {
            $form = $this->getLoginForm();
            $form->setData($data);

            if (!$form->isValid()) {
                $this->flashMessenger()->setNamespace('zfcuser-login-form')->addMessage($this->failedLoginMessage);
                return new JsonModel(array(
                    'tag' => 'login',
                    'success' => 0,
                    'error' => 1,
                    'error_msg' => $this->failedLoginMessage,
                ));
            } else {
                $this->zfcUserAuthentication()->getAuthAdapter()->resetAdapters();
                $this->zfcUserAuthentication()->getAuthService()->clearIdentity();

                $adapter = $this->zfcUserAuthentication()->getAuthAdapter();

                $adapter->prepareForAuthentication($this->getRequest());
                $auth = $this->zfcUserAuthentication()->getAuthService()->authenticate($adapter);

                if (!$auth->isValid()) {
                    $this->flashMessenger()->setNamespace('zfcuser-login-form')->addMessage($this->failedLoginMessage);
                    $adapter->resetAdapters();
                    return new JsonModel(array(
                        'tag' => 'login',
                        'success' => 0,
                        'error' => 1,
                        'error_msg' => $this->failedLoginMessage,
                    ));
                }
                $userObject = $this->getUserTable()->findByEmail($data['identity']);
                return new JsonModel(array(
                    'tag' => 'login',
                    'success' => 1,
                    'error' => 0,
                    'uid' => $userObject->uid,
                    'user' => array(
                        'name' =>$userObject->display_name,
                        'email' => $data['identity'],
                        'created_at' => $userObject->created_at,
                        'updated_at' => null,
                    )
                ));
            }
        } else if ($data['tag'] == 'register') {
            //$prg = $this->prg('user/register', true);
            $dataIn['passwordVerify']= $data['credential'];
            $service = $this->getUserService();
            $user = $service->register($dataIn);
            
            if (!$user) {
                // Register not successful. Existing user.
                return new JsonModel(array(
                    'tag' => 'register',
                    'success' => 0,
                    'error' => 2,
                    'error_msg' => $this->failedRegisterMessage,
                ));
            }
            // Successful
            $userObject = $this->getUserTable()->findByEmail($dataIn['email']);
            return new JsonModel(array(
                'tag' => 'register',
                'success' => 1,
                'error' => 0,
                'user' => array(
                    'name' => $userObject->display_name,
                    'email' => $dataIn['email'],
                )
            ));
        } 
        return new JsonModel(array(
            // Error    
        ));
    }

    public function update($id, $data) {
        
    }

    public function delete($id) {
        
    }

    public function getUserTable() {
        if (!$this->userTable) {
            $sm = $this->getServiceLocator();
            $this->userTable = $sm->get('Customer\Model\CustomerTable');
        }
        return $this->userTable;
    }

    public function getLoginForm() {
        if (!$this->loginForm) {
            $this->setLoginForm($this->getServiceLocator()->get('zfcuser_login_form'));
        }
        return $this->loginForm;
    }

    public function setRegisterForm(Form $registerForm) {
        $this->registerForm = $registerForm;
    }

    public function setLoginForm(Form $loginForm) {
        $this->loginForm = $loginForm;
        $fm = $this->flashMessenger()->setNamespace('zfcuser-login-form')->getMessages();
        if (isset($fm[0])) {
            $this->loginForm->setMessages(
                    array('identity' => array($fm[0]))
            );
        }
        return $this;
    }
    public function getUserService()
    {
        if (!$this->userService) {
            $this->userService = $this->getServiceLocator()->get('zfcuser_user_service');
        }
        return $this->userService;
    }

}

In order to use some ZfcUser service, you must also need to add some config into YourModule\Module.php file. For me, I copy all the getServiceConfig() method from the ZfcUser\Module.php file (:. (I also do some small modification to my code to make it run, but I don't remember exactly. But I'm sure it's easy, you can do it).

use ZfcUser\Options;
use ZfcUser\Form;
use ZfcUser\Mapper;
use ZfcUser\Validator;

class Module {
    public function getAutoloaderConfig(){
        return array(
            'Zend\Loader\ClassMapAutoloader' => array(
                __DIR__ . '/autoload_classmap.php',
            ),
            'Zend\Loader\StandardAutoloader' => array(
                'namespaces' => array(
                    __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
                ),
            ),
        );
    }
    
    public  function getConfig(){
        return include __DIR__ .'/config/module.config.php';
    }
    
    public function getServiceConfig()
    {
        return array(
            'invokables' => array(
                'ZfcUser\Authentication\Adapter\Db' => 'ZfcUser\Authentication\Adapter\Db',
                'ZfcUser\Authentication\Storage\Db' => 'ZfcUser\Authentication\Storage\Db',
                'ZfcUser\Form\Login'                => 'ZfcUser\Form\Login',
                'zfcuser_user_service'              => 'ZfcUser\Service\User',
                'zfcuser_register_form_hydrator'    => 'Zend\Stdlib\Hydrator\ClassMethods',
            ),
            'factories' => array(

                'zfcuser_module_options' => function ($sm) {
                    $config = $sm->get('Config');
                    return new Options\ModuleOptions(isset($config['zfcuser']) ? $config['zfcuser'] : array());
                },
                // We alias this one because it's ZfcUser's instance of
                // Zend\Authentication\AuthenticationService. We don't want to
                // hog the FQCN service alias for a Zend\* class.
                'zfcuser_auth_service' => function ($sm) {
                    return new \Zend\Authentication\AuthenticationService(
                        $sm->get('ZfcUser\Authentication\Storage\Db'),
                        $sm->get('ZfcUser\Authentication\Adapter\AdapterChain')
                    );
                },

                'ZfcUser\Authentication\Adapter\AdapterChain' => 'ZfcUser\Authentication\Adapter\AdapterChainServiceFactory',

                'zfcuser_login_form' => function($sm) {
                    $options = $sm->get('zfcuser_module_options');
                    $form = new Form\Login(null, $options);
                    $form->setInputFilter(new Form\LoginFilter($options));
                    return $form;
                },

                'zfcuser_register_form' => function ($sm) {
                    $options = $sm->get('zfcuser_module_options');
                    $form = new Form\Register(null, $options);
                    //$form->setCaptchaElement($sm->get('zfcuser_captcha_element'));
                    $form->setInputFilter(new Form\RegisterFilter(
                        new Validator\NoRecordExists(array(
                            'mapper' => $sm->get('zfcuser_user_mapper'),
                            'key'    => 'email'
                        )),
                        new Validator\NoRecordExists(array(
                            'mapper' => $sm->get('zfcuser_user_mapper'),
                            'key'    => 'username'
                        )),
                        $options
                    ));
                    return $form;
                },

                'zfcuser_change_password_form' => function($sm) {
                    $options = $sm->get('zfcuser_module_options');
                    $form = new Form\ChangePassword(null, $sm->get('zfcuser_module_options'));
                    $form->setInputFilter(new Form\ChangePasswordFilter($options));
                    return $form;
                },

                'zfcuser_change_email_form' => function($sm) {
                    $options = $sm->get('zfcuser_module_options');
                    $form = new Form\ChangeEmail(null, $options);
                    $form->setInputFilter(new Form\ChangeEmailFilter(
                        $options,
                        new Validator\NoRecordExists(array(
                            'mapper' => $sm->get('zfcuser_user_mapper'),
                            'key'    => 'email'
                        ))
                    ));
                    return $form;
                },

                'zfcuser_user_hydrator' => function ($sm) {
                    $hydrator = new \Zend\Stdlib\Hydrator\ClassMethods();
                    return $hydrator;
                },

                'zfcuser_user_mapper' => function ($sm) {
                    $options = $sm->get('zfcuser_module_options');
                    $mapper = new Mapper\User();
                    $mapper->setDbAdapter($sm->get('zfcuser_zend_db_adapter'));
                    $entityClass = $options->getUserEntityClass();
                    $mapper->setEntityPrototype(new $entityClass);
                    $mapper->setHydrator(new Mapper\UserHydrator());
                    $mapper->setTableName($options->getTableName());
                    return $mapper;
                },
            ),
        );
    }
}

To test Restful web service, I use this tool: Postman for Chrome.
My module code here.

Hope it can help.

3 comments:

  1. Why do you believe using REST for Authentication is 'totally wrong' ?

    See here for why that's not the case necessarily, http://stackoverflow.com/questions/319530/restful-authentication

    ReplyDelete
    Replies
    1. Oh, thank you for giving me some more information :).
      I meant the way I used REST for my authentication is wrong. The way I did is not good, so people should avoid do like me.

      Delete
  2. Ban oi co ban hoan chinh cua module o tren khong ban. Module ban minh down ve bi thieu class customer

    ReplyDelete