laminas/laminas-authentication
Laminas Authentication provides a flexible API for authenticating users in PHP applications. It ships with adapters for HTTP Basic and Digest, LDAP, and relational databases, plus persistent identity storage and result objects for building custom login flows.
Install the package and a session backend if you want persistent login state:
composer require laminas/laminas-authentication laminas/laminas-session
Start by reading four things:
Laminas\Authentication\AuthenticationService — the orchestrator.Laminas\Authentication\Adapter\AdapterInterface — verifies credentials.Laminas\Authentication\Storage\StorageInterface — persists identity.Laminas\Authentication\Result — success/failure result object.The smallest useful flow is a login form. Build an adapter with the submitted identity and credential, pass it to AuthenticationService::authenticate(), then check the Result:
use Laminas\Authentication\AuthenticationService;
use Laminas\Authentication\Storage\Session;
$auth = new AuthenticationService(new Session('myapp_auth'));
$adapter = new App\Auth\UserRepositoryAdapter($users);
$adapter->setIdentity($email)->setCredential($password);
$result = $auth->authenticate($adapter);
if ($result->isValid()) {
// Identity is now written to session storage by AuthenticationService.
// $auth->hasIdentity() === true
// $auth->getIdentity() === whatever the adapter returned as identity (e.g. user ID)
header('Location: /dashboard');
exit;
}
foreach ($result->getMessages() as $message) {
// show login error
}
On later requests, the same AuthenticationService instance—wired with the same session storage—can answer:
if (!$auth->hasIdentity()) {
// redirect to /login
}
$userId = $auth->getIdentity();
Logout is just clearing storage:
$auth->clearIdentity();
That login → persist identity → check identity → logout loop is the core day-to-day use case.
Register one shared AuthenticationService per request. In Laminas MVC use service_manager.factories; in Mezzio use dependencies.factories. Give it session storage, and pass the adapter at login time:
use Laminas\Authentication\AuthenticationService;
use Laminas\Authentication\Storage\Session;
use Laminas\Session\SessionManager;
return [
'dependencies' => [
'factories' => [
AuthenticationService::class => static function ($container) {
$sessionManager = $container->get(SessionManager::class);
$storage = new Session('myapp_auth', 'storage', $sessionManager);
return new AuthenticationService($storage);
},
],
],
];
In Laminas MVC, the equivalent key is service_manager instead of dependencies.
Use a custom adapter for real applications. The built-in DbTable adapters are convenient, but most modern apps use password_hash()/password_verify(), which cannot be expressed well as a SQL credential treatment. A repository-backed adapter is usually cleaner:
namespace App\Auth;
use Laminas\Authentication\Adapter\AdapterInterface;
use Laminas\Authentication\Result;
final class UserRepositoryAdapter implements AdapterInterface
{
private string $identity = '';
private string $credential = '';
public function __construct(private UserRepository $users) {}
public function setIdentity(string $identity): self
{
$this->identity = $identity;
return $this;
}
public function setCredential(string $credential): self
{
$this->credential = $credential;
return $this;
}
public function authenticate(): Result
{
$user = $this->users->findByEmail($this->identity);
if ($user === null) {
return new Result(
Result::FAILURE_IDENTITY_NOT_FOUND,
null,
['Invalid credentials']
);
}
if (!password_verify($this->credential, $user->passwordHash)) {
return new Result(
Result::FAILURE_CREDENTIAL_INVALID,
null,
['Invalid credentials']
);
}
if (password_needs_rehash($user->passwordHash, PASSWORD_DEFAULT)) {
$this->users->updatePasswordHash(
$user->id,
password_hash($this->credential, PASSWORD_DEFAULT)
);
}
return new Result(Result::SUCCESS, $user->id, ['Authenticated']);
}
}
Then at login:
$adapter = new UserRepositoryAdapter($users);
$adapter->setIdentity($email)->setCredential($password);
$result = $auth->authenticate($adapter);
Store a stable identity, not a full mutable user object. The session storage serializes whatever the adapter returns. A user ID or UUID is safest. On each request, use getIdentity() to load the current user from your repository. This avoids stale session data and serialization problems when user classes change.
Use middleware/controller guards. In Mezzio, inject AuthenticationService into middleware and reject unauthenticated requests:
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (!$this->auth->hasIdentity()) {
return new RedirectResponse('/login');
}
return $handler->handle(
$request->withAttribute('user_id', $this->auth->getIdentity())
);
}
In Laminas MVC, inject the same service into controllers or wrap it in your own controller plugin.
Use NonPersistent storage for tests and stateless paths. It keeps identity only for the current object lifetime:
use Laminas\Authentication\Storage\NonPersistent;
$auth = new AuthenticationService(new NonPersistent(), $adapter);
$result = $auth->authenticate();
This is useful for API-style authentication where every request re-validates a token, or for unit tests where you do not want session side effects.
Use Chain storage when you need multiple persistence layers. For example, you can try a session storage first and fall back to another storage implementation. Chain reads the first non-empty storage and writes/clears across the configured chain—useful for custom “remember me” or migration scenarios.
For LDAP/HTTP/Digest, treat the built-in adapters as integration points. The LDAP adapter works with laminas/laminas-ldap; the HTTP adapter handles HTTP basic/digest auth; the Digest adapter is for legacy digest auth. In modern APIs, a custom bearer-token adapter plus NonPersistent storage is usually a better fit than the HTTP adapter.
$result->isValid(). authenticate() does not throw on bad credentials; it returns a Result. A failed result also does not write anything to storage.new AuthenticationService($storage, $adapter) — reversing them causes type errors or confusing behavior.Result::SUCCESS with a null identity can make hasIdentity() behave unexpectedly because many storage implementations treat null as empty.hasIdentity() instead of truthiness checks. getIdentity() can return 0, '', or another falsy-but-valid value. hasIdentity() checks the storage directly.authenticate(). If the session has not started, or headers have already been sent, login can fail or silently lose the identity. In Mezzio, start the session in middleware; in Laminas MVC, configure the session manager/bootstrap.new Session('myapp_auth').laminas-authentication does not do this for you. Call session_regenerate_id(true) or the equivalent on your session manager to reduce session fixation risk.Result code server-side.DbTable adapters. They are convenient for simple SQL-backed credentials, but credential treatments are SQL fragments and do not work with password_hash(). For new code, prefer a custom adapter using password_verify().Result::FAILURE_IDENTITY_NOT_FOUND, FAILURE_CREDENTIAL_INVALID, FAILURE_IDENTITY_AMBIGUOUS, and FAILURE_UNCATEGORIZED are useful for logging; user-facing messages should usually be vague.AuthenticationService across long-running requests. In RoadRunner, Swoole, or other persistent workers, create the service and session storage per request. Sharing them can leak one user’s identity into another request.clearIdentity() clears storage only. If you reuse the service, set or replace the adapter for the next authentication attempt.authenticate() with no adapter throws. Either pass an adapter to authenticate($adapter) or call setAdapter() first.laminas-authentication validates credentials; it does not protect login forms from brute force or CSRF. Put those controls in your login controller/middleware.NonPersistent storage in tests. It keeps tests fast and avoids session state bleeding between test cases.getCode() and getMessages() before inspecting adapters or storage. For session issues, inspect the configured namespace in $_SESSION; for adapter issues, reproduce the adapter call in isolation with a known-good and known-bad credential.How can I help you explore Laravel packages today?