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-authenticationThe 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
| Call | What 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.