Problem in using the Identity Helper

zend-view

#1

Hello, recently I got a error of
Call to a member function getUsername() on null

in module.config.php

    'service_manager' => [
        'aliases' => [
            'my_auth_service' => AuthenticationService::class,
        ],
        'factories' => [
            AuthenticationService::class => InvokableFactory::class,
        ],
    ],

in AdminTable.php which do the authentication logic

    public function auth(Admin $admin)
    {
        $authAdapter = new AuthAdapter($this->tableGateway->getAdapter(), 'admin', 'username', 'passwd', 'SHA1(CONCAT(?, salt)) AND valid=1');
        $authAdapter->setIdentity($admin->username);
        $authAdapter->setCredential($admin->passwd);
        $result = $authAdapter->authenticate();
        $columnsToReturn = ['id','roleid','username','nickname'];

        if ($result->isValid())
        {
            // .....
        }
    }

in the view add.phtml file (I wanna to test the identity helper):

                                    <?php
                                    $user = $this->identity();
                                    echo $user->getUsername();  // ====> line 46
                                    ?>

Then I got a error reporting when browse the page.

An error occurred
An error occurred during execution; please try again later.

Additional information:
Error
File: /opt/www/websites/mydomain.com/module/Application/view/application/role/add.phtml:46
Message: Call to a member function getUsername() on null

As the document says it will store something in some session.
https://docs.zendframework.com/zend-authentication/intro/ in which the following annotation:

// Authentication succeeded; the identity ($username) is stored
// in the session:
// $result->getIdentity() === $auth->getIdentity()
// $result->getIdentity() === $username

#2

Hi there,

without knowing all the details of your code, how do you assign $this->identity() within add.phtml?
I am sure if you to a var_dump($user); on line 45, it will return null.

In your view-controller, you might be missing something along the line of:

    public function addAction()
    {
        $authAdapter = new AuthAdapter($this->tableGateway->getAdapter(), 'admin', 'username', 'passwd', 'SHA1(CONCAT(?, salt)) AND valid=1');
        $authAdapter->setIdentity($admin->username);
        $authAdapter->setCredential($admin->passwd);
        $result = $authAdapter->authenticate();

        return [
            'identity' => $result,
        ];
    }

#3

@guidofaecke, thanks for your reply!

I thought that it just need one time authentication and always keep logined status in a certain period of time. It is not possible that I always input my username and password in each page of my application!

In the authAction I do the authentication with username and password, it should store the Identity inside some media like cookie or session. Then I can fetch it in other page from the cookie or session without input my username and password. My point is how to fetch the identity with the identity view helper.


#4

I might misunderstood you :slight_smile:

Ok… I assume you have zendframework/zend-session installed, otherwise session handling is not much fun.
Cookies, bad idea in my mind (not a big fan of sessions either, I use tokens with every request, but that would be to hard to explain in here).

Next of, you probably have something similar in your global.php or session.global.php file (filename doesn’t really matter, as long as it can be loaded)

return [
    // Session configuration.
    'session_config' => [
        // Session cookie will expire in 1 hour.
        'cookie_lifetime' => 60*60*1,     
        // Session data will be stored on server maximum for 30 days.
        'gc_maxlifetime'     => 60*60*24*30, 
    ],
    // Session manager configuration.
    'session_manager' => [
        // Session validators (used for security).
        'validators' => [
            RemoteAddr::class,
            HttpUserAgent::class,
        ]
    ],
    // Session storage configuration.
    'session_storage' => [
        'type' => SessionArrayStorage::class
    ],
    
    // ...
];

Probably add

$sessionManager = $serviceManager->get(SessionManager::class);

to your Module.php within the onBootstrap method.
That should get you your session. (with automated session-start).

For the actual authentication/session management, I recommend the following link:
[https://olegkrivtsov.github.io/using-zend-framework-3-book/html/en/User_Management__Authentication_and_Access_Filtering/Implementing_User_Authentication.html#Adding_AuthManager_Service]
It’s a good read with loads of examples.

Let me know if you need more help.


#5

Here are the key steps:

  1. Create the factory for the authentication service.
namespace MyModule;

use Interop\Container\ContainerInterface;
use Zend\Authentication\Adapter\DbTable\CredentialTreatmentAdapter;
use Zend\Authentication\AuthenticationService;
use Zend\Db\Adapter\AdapterInterface;
use Zend\ServiceManager\Factory\FactoryInterface;

class AuthenticationServiceFactory implements FactoryInterface
{
    public function __invoke(
        ContainerInterface $container,
        $requestedName,
        array $options = null
    ) {
        // Database adapter
        /** @var \Zend\Db\Adapter\Adapter $dbAdapter */
        $dbAdapter = $container->get(AdapterInterface::class);

        // Auth adapter
        $authAdapter = new CredentialTreatmentAdapter(
            $dbAdapter,
            'users', // table name
            'username', // identity column
            'password', // credential column
            'SHA1(?)' // credential treatment
        );

        return new AuthenticationService(null, $authAdapter);
    }
}
  1. Register the factory:
'service_manager' => [
    'factories'  => [
        Zend\Authentication\AuthenticationServiceInterface::class => MyModule\AuthenticationServiceFactory::class,
    ],
],

Then use it in your controller for login:

public function loginAction()
{
    // …
    
    if ($this->getRequest()->isPost()) {

        // Get form data…
        
        // Get authentication service
        /** @var \Zend\Authentication\AuthenticationService $authenticationService */
        $authenticationService = $this->plugin('identity')->getAuthenticationService();

        // Set identity and credential for auth service
        /* @var $adapter \Zend\Authentication\Adapter\DbTable\AbstractAdapter */
        $adapter = $authenticationService->getAdapter();
        $adapter->setIdentity($data['location'])
                ->setCredential($data['password']);
        
        // Validation
        if ($authenticationService->authenticate()->isValid()) {
            // Get result
            /** @var \Zend\Authentication\Storage\Session $storage */
            $storage = $authenticationService->getStorage();
            $result  = $adapter->getResultRowObject(
                [
                    'id',
                    'roleid',
                    'username',
                    'nickname',
                ]
            );

            // Create user
            $user = new User($result->id);

            // Write to session
            $storage->write($user);
            
            // Redirect…
        }
    }
    
    // …
}

and logout:

public function logoutAction()
{
    // Has identity?
    if (! $this->identity()) {
        return $this->redirect()->toRoute('…');
    }

    // Get authentication service
    /** @var \Zend\Authentication\AuthenticationService $authenticationService */
    $authenticationService = $this->plugin('identity')->getAuthenticationService();

    // Clear identity
    $authenticationService->clearIdentity();

    // Set success message
    $this->flashMessenger()->addSuccessMessage('Logout successful…');

    return $this->redirect()->toRoute('…');
}

And in your view scripts:

var_dump($this->identity()); // User::class (after login)

Attention

The above code has not been tested, it only shows the main process and needs to be customized!


#6

@jobsfan does not use Doctrine, therefore the example from Oleg Krivtsov is too heavy here and he does not use the Identity controller plugin and the Identity view helper in his example.


#7

@jobsfan does not use Doctrine, therefore the example from Oleg Krivtsov is too heavy here and he does not use the Identity controller plugin and the Identity view helper in his example.

Your are absolutely right!

Since I use doctrine on a daily basis, it just looks “normal” to me.
Should and will pay closer attention next time.


#8

Hi @froschdesign, wish you had a very happy Christmas day and New Year’s day holiday!!

Your operation seem very complicate for me.

I understood that, I need to

  1. create the factory for the anthentication service, However, namespace MyModule;, This namesplace confused me, I guess it should be namespace MyModule\Model; or namespace MyModule\Factory; or some other sub folder of MyModule.

I created a factory which located in namespace Application\Service\Factory;, is it correct?

if the loginAction you mentioned,

$authenticationService = $this->plugin(‘identity’)->getAuthenticationService();
does this part have relationship with the factory’s register? I mean whether I can use this way to get the authenticationService without the [Create the factory for the authentication service and Register the factory ] step you mentioned above?

As well, you use a User class in the loginAction, is the User class the built-in class of Zf3 framework? Or should I created the User Class in my project (I confused because my User class [in my project is the Admin class] is an entity with exchangeArray method, getArrayCopy method, setInputFilter method, getInputFilter method in it, that I learned from the zf3 's tutorial, remember a Alubm tutorial, even no construction method mentioned)?

Ps: I followed your steps to modify my project code, and got the following error:

Message: Call to a member function getStorage() on null

I use $authenticationService instead $this->authenticationService it works ok, so I guess it is a typo error.

Till now, all works okay. However I have another problem.
I have a plugin namespace Application\Controller\Plugin; which I registered in the module.php like:

    public function onBootstrap(MvcEvent $event)
    {
        $eventManager = $event->getApplication()->getEventManager();
        $eventManager->attach('route', [$this, 'doAuthorization'],3);

        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);
    }

    public function doAuthorization(MvcEvent $event)
    {
        $application = $event->getApplication();
        $serviceManager = $application->getServiceManager();
        $sharedManager = $application->getEventManager()->getSharedManager();
        $router = $serviceManager->get('router');
        $request = $serviceManager->get('request');
        $matchedRoute = $router->match($request);
        if (!preg_match('%^/'.__NAMESPACE__.'/.*%i', $request->getUri()->getPath())) return true;
        if ($matchedRoute !== null)
        {
            $sharedManager->attach(AbstractActionController::class, 'dispatch',function ($event) use ($serviceManager){
                $serviceManager->get('ControllerPluginManager')->get('Appauth')->doAuthorization($event);
            },4);
        }
    }

I need to use the logic like the following in the plugin Application\Controller\Plugin

I wonder how, I have never fetch the plugin in another plugin, as well don’t know how to fetch another viewhleper in another viewhelper. Pls tell me.


#9

@froschdesign, I am not familiar to Doctrine. I use just tutrorial-like adapter and tablegateway pattern.

I need to change my logic to the way you taught me.

class Appauth extends AbstractPlugin
{
    public function doAuthorization($e)
    {
        $matches = $e->getRouteMatch();
        $module = current(explode('\\', strtolower($matches->getParam('controller'))));
        $controller = str_replace('controller', '', end(explode('\\', strtolower($matches->getParam('controller'))))); //$matches->getParam('controller')  Application\Controller\IndexController
        $action = $matches->getParam('action'); //systemhomepage

        $application = $e->getApplication();
        $serviceManager = $application->getServiceManager();

        $adminTable = $serviceManager->get(AdminTable::class);

        $userInfo = $adminTable->userInfo(); //

        $response = $e->getResponse();
        $router = $e->getRouter();

        if ($module == 'application' && $controller == 'index' && $action == 'login')
        {
            if ($userInfo)
            {
                $url = $router->assemble(array('controller'=>'index','action'=>'index'),array('name'=>'application/index'));
                $response->getHeaders()->addHeaderLine('Location', $url);
                $response->setStatusCode(302);
                $e->stopPropagation(true);
                return $response;
            }
            else
            {
                return true;
            }
        }
        if ($module == 'application' && $controller == 'index' && $action == 'logout')
        {
            return true;
        }
        if (!$userInfo)
        {
            $url = $router->assemble(array('controller'=>'index','action'=>'login'),array('name'=>'application/index'));
            $response->getHeaders()->addHeaderLine('Location', $url);
            $response->setStatusCode(302);
            $e->stopPropagation(true);
            return $response;
        }
        else
        {
            if ($module == 'application' && $controller == 'index' && $action == 'index') return true;
            if ($userInfo['roleid'] == 1) return true;

            exit('if executed here, you need to hanle auth stuff!!!!!!');
            $resourcesTable = $serviceManager->get(ResourceTable::class);
            $resourcesArr = $this->getController()->getServiceLocator()->get('Members\Model\ResourcesTable')->formatResources('iu'); //$adminTable = $serviceManager->get(AdminTable::class);
            $sid = array_search($uri,$resourcesArr);
            if (!$sid)
            {
                $url = $router->assemble(array('controller'=>'index','action'=>'index'),array('name'=>'members'));
                $response->getHeaders()->addHeaderLine('Location', $url);
                $response->setStatusCode(302);
                $e->stopPropagation(true);
                return $response;
            }
            else
            {
                if ($this->getController()->getServiceLocator()->get('Members\Model\AclTable')->checkAcl($userInfo['rid'],$sid))
                {
                    return true;
                }
                else
                {
                    $url = $router->assemble(array('controller'=>'index','action'=>'index'),array('name'=>'members'));
                    $response->getHeaders()->addHeaderLine('Location', $url);
                    $response->setStatusCode(302);
                    $e->stopPropagation(true);
                    return $response;
                }
            }
        }
    }
}

#10

I know this and my comment was a reply to @guidofaecke.

The plugin is not needed. Please stay on basics until everything is running.


#11

The User class is not a part of the Zend Framework and it doesn’t matter here if a constructor is used or not, because it is only an example. I do not know anything about your application or can not see any code. That’s why I gave a simple and basic example.


#12

Hello @froschdesign, thanks for your reply!

Yes, it runs well. I want to further more, my next step is to use acl to manage the access of all the controllers and actions beyond the moudle “application”. I use controller plugin because I need to do the acl logic in one place. In some model, I define a method like checkAcl($roleid, $module,$controller,$action) to check whether this user with this role have the authrization to access some url. Then I use the checkAcl function in the so called one place logic, maybe onBootStrap or ondispatch, do you have any advises?

When I learning zf2, some friends suggest me to invoke this logic in the onBootstrap method in the module.php file, and was suggested to write the main logic in the controller plugin.

Now in zf3, if not in controller plugin, where is the best place for me to put the main logic of the acl judgement? A service or others? It is important that in which I can use the method of other model/entity/plugin/service. I realized that only use the DI from the factory entry is not very convenient. When I need to use some unexpected method from somewhere, I need to change the entry and add more params there. So I am looking forward to a more convenient way.


#13

Also here is no plugin needed. This is nothing to do with ZF2 or ZF3, it is the behaviour. A controller plugin must be called and is therefore intended for multiple executions. However, the access checks must always be performed.
Use a listener instead, which is added to the mvc-event EVENT_DISPATCH. In the listener you have access to the request, application with service-manager and so on.