Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

User login

Authentication is checking that a visitor is who they claim to be: they enter an email and a password, the site compares them with the database and remembers the result in the session. In Laminas the laminas-authentication package does this. It does not know where users are stored: the check is done by an adapter — a class you write yourself.

Installation and the table

composer require laminas/laminas-authentication

The package keeps the logged-in user in the session, so it needs laminas-session from the sessions lesson. Create the table:

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL
);

Get a password hash for the first row with php -r "echo password_hash('secret', PASSWORD_DEFAULT);" and insert the row with an INSERT query.

To read the table, create the classes Blog\Model\User and Blog\Model\UserTable modelled on Post and PostTable. The table needs one method — findByEmail(string $email): ?User — which runs select(['email' => $email])->current().

The adapter

// module/Blog/src/Auth/PasswordAdapter.php
namespace Blog\Auth;
 
use Blog\Model\UserTable;
use Laminas\Authentication\Adapter\AdapterInterface;
use Laminas\Authentication\Result;
 
class PasswordAdapter implements AdapterInterface
{
    private string $email = '';
    private string $password = '';
 
    public function __construct(private UserTable $users) {}
 
    public function setCredentials(string $email, string $password): void
    {
        $this->email = $email;
        $this->password = $password;
    }
 
    public function authenticate(): Result
    {
        $user = $this->users->findByEmail($this->email);
 
        if ($user === null || !password_verify($this->password, $user->passwordHash)) {
            return new Result(Result::FAILURE_CREDENTIAL_INVALID, null, ['Wrong email or password.']);
        }
 
        return new Result(Result::SUCCESS, ['id' => $user->id, 'email' => $user->email]);
    }
}

The authenticate() method returns a Result object. Its second argument is the identity: the data that stays in the session.

Registering the services

// module/Blog/config/module.config.php
// at the top of the file: use Laminas\Authentication\AuthenticationService;
'service_manager' => [
    'factories' => [
        AuthenticationService::class => InvokableFactory::class,
        Auth\PasswordAdapter::class => ReflectionBasedAbstractFactory::class,
    ],
],

AuthenticationService is a ready-made class of the package: it calls the adapter and keeps the identity in the session.

Login and logout

Create a controller and register it in controllers with ReflectionBasedAbstractFactory:

// module/Blog/src/Controller/AuthController.php
namespace Blog\Controller;
 
use Blog\Auth\PasswordAdapter;
use Laminas\Authentication\AuthenticationService;
use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\View\Model\ViewModel;
 
class AuthController extends AbstractActionController
{
    public function __construct(
        private AuthenticationService $auth,
        private PasswordAdapter $adapter,
    ) {}
 
    public function loginAction()
    {
        $request = $this->getRequest();
 
        if (!$request->isPost()) {
            return new ViewModel(['error' => null]);
        }
 
        $this->adapter->setCredentials(
            (string) $request->getPost('email'),
            (string) $request->getPost('password'),
        );
 
        $result = $this->auth->authenticate($this->adapter);
 
        if ($result->isValid()) {
            return $this->redirect()->toRoute('blog');
        }
 
        return new ViewModel(['error' => $result->getMessages()[0]]);
    }
 
    public function logoutAction()
    {
        $this->auth->clearIdentity();
 
        return $this->redirect()->toRoute('login');
    }
}

Add two Literal routes: login with the address /login and logout with the address /logout. The view script module/Blog/view/blog/auth/login.phtml holds a form with the email and password fields and prints $this->error. Open /login and enter the user's data — after logging in, /blog opens.

The current user

CallWhat it returns
$this->auth->hasIdentity()true if the visitor is logged in
$this->auth->getIdentity()The array from Result: id and email
$this->auth->clearIdentity()Forgets the user

In view scripts and the layout the $this->identity() helper returns the same identity; for a guest it returns null.

Common mistake

A password is stored only as a password_hash() hash and compared with password_verify(). Comparing plain passwords or md5() hashes turns a database leak into a disaster. The login error message must be a single one — "Wrong email or password" — without saying which of the two did not match.

Thanks for reading!

Donate to the site — we work hard for you.