Authentication and Authorization¶
Overview¶
Phalcon\Auth provides authentication (who the user is) and authorization (what the user may do) behind a single facade, Phalcon\Auth\Manager.
The layer is composed of four building blocks:
- Guards answer "is there an authenticated user, and who is it?". Two ship in the box:
session(cookie/session backed, with remember-me and HTTP Basic) andtoken(bearer/API token). - Adapters are user providers - they load user records from a backing store. Three ship in the box:
memory,model(Phalcon ORM), andstream(JSON file). - Access gates answer "is this action allowed?". Three ship in the box:
auth(requires a logged-in user),guest(requires no logged-in user), andacl(checks the authenticated user's role against aPhalcon\Acladapter). AuthUseris the authenticated user value object returned by guards.
Guards resolve the framework services they need (request, cookies, session manager) from a container, so they wire against the real application singletons. The container can be the modern Phalcon\Container\Container or the classic Phalcon\Di\Di.
Bootstrapping with ManagerFactory¶
Phalcon\Auth\ManagerFactory builds a fully wired Manager from a configuration tree. It needs a password hasher (Phalcon\Encryption\Security) and a container that provides the framework services the guards consume - either a Phalcon\Container\Container or a Phalcon\Di\DiInterface. The built-in Phalcon\Container\Provider\Web registers those services on a Phalcon\Container\Container.
<?php
use Phalcon\Auth\Access\Auth;
use Phalcon\Auth\Access\Guest;
use Phalcon\Auth\ManagerFactory;
use Phalcon\Container\ContainerFactory;
use Phalcon\Container\Provider\Web;
use Phalcon\Encryption\Security;
$container = (new ContainerFactory())
->addProvider(new Web())
->newContainer();
/**
* Alternative
*
$container = new Phalcon\Di\FactoryDefault();
*/
$factory = new ManagerFactory(new Security(), $container);
$manager = $factory->load([
'guards' => [
'web' => [
'type' => 'session',
'default' => true,
'adapter' => [
'name' => 'model',
'options' => ['model' => Users::class],
],
],
'api' => [
'type' => 'token',
'adapter' => [
'name' => 'model',
'options' => ['model' => Users::class],
],
'options' => [
'inputKey' => 'api_token',
'storageKey' => 'api_token',
],
],
],
'access' => [
'auth' => Auth::class,
'guest' => Guest::class,
],
]);
load() accepts an array or a Phalcon\Config\ConfigInterface. Each guard entry declares its type (session or token), an adapter (name plus adapter options), and optional guard options. Exactly one guard should be marked default. A guard entry missing its type, its adapter, or the adapter name - or providing them with the wrong type - throws Phalcon\Auth\Exception with a message naming the offending guard and key.
The Container¶
Every entry point that takes a container - ManagerFactory::__construct(), the guards' fromOptions() factories, and the adapter, guard, and access locators - accepts either of two container types:
Phalcon\Container\Container, the modern service container (it implementsPhalcon\Contracts\Container\Service\Collection).Phalcon\Di\DiInterface, the classic dependency-injection container (for examplePhalcon\Di\FactoryDefault).
The intent is Container-first: pass a Phalcon\Container\Container. The classic Phalcon\Di\Di is supported with provisions - its service definitions must be pre-registered, since it does not autowire. Passing any other type throws a \TypeError.
Each guard resolves the framework services it needs as shared services from the container. For each service the guard tries three names in order and uses the first the container provides:
- an explicit override in the guard
optionsunder theserviceskey; - the service interface ID (its fully-qualified interface name);
- the conventional short name.
| Service | Interface ID | Short name | Required by |
|---|---|---|---|
| Request | Phalcon\Http\RequestInterface | request | session and token guards |
| Cookies | Phalcon\Http\Response\CookiesInterface | cookies | session guard |
| Session | Phalcon\Session\ManagerInterface | session | session guard |
This candidate order lets both container styles work without extra aliases. Phalcon\Container\Provider\Web binds the interface IDs on a Phalcon\Container\Container. A classic Phalcon\Di\FactoryDefault registers request and cookies under their short names, which the guards find directly; it ships no session manager, so a session guard needs one bound under session (or the interface ID).
When none of the candidate names resolves, the guard throws Phalcon\Container\Exceptions\Exception. Resolution failures raised by the classic Phalcon\Di\Di are re-issued as Phalcon\Container\Exceptions so a caller catches a single exception family regardless of the container in use.
<?php
use Phalcon\Auth\ManagerFactory;
use Phalcon\Di\FactoryDefault;
use Phalcon\Encryption\Security;
use Phalcon\Session\Adapter\Stream;
use Phalcon\Session\Manager as SessionManager;
$di = new FactoryDefault();
// request and cookies already exist on FactoryDefault under their short
// names, so the guards resolve them directly. Only the session manager has
// to be added.
$di->setShared('session', function () {
$session = new SessionManager();
$session->setAdapter(new Stream(['savePath' => '/var/app/cache/session']));
return $session;
});
$factory = new ManagerFactory(new Security(), $di);
A token-only setup needs only the request service, which FactoryDefault already provides.
To resolve a service from a custom container key, set the services override in the guard options:
<?php
$config = [
'guards' => [
'web' => [
'type' => 'session',
'default' => true,
'adapter' => [
'name' => 'model',
'options' => ['model' => Users::class],
],
'options' => [
'services' => [
'request' => 'my_request_service',
'cookies' => 'my_cookies_service',
'session' => 'my_session_service',
],
],
],
],
];
The Manager¶
The Manager is the entry point for both authentication and authorization. Authentication calls delegate to the active guard; authorization calls drive the active access gate.
<?php
// Authenticate against the default guard
if ($manager->attempt(['email' => 'jane@example.com', 'password' => 's3cret'])) {
$user = $manager->user();
echo $user->getAuthIdentifier();
}
$manager->check(); // bool - is someone authenticated on the default guard
$manager->id(); // int|string|null - the authenticated identifier
// Reach a specific guard by name
$manager->guard('api')->validate(['api_token' => $token]);
$manager->logout();
| Method | Returns | Purpose |
|---|---|---|
guard(?string $name = null) | Guard | The named guard, or the default guard when $name is null |
attempt(array $credentials = [], bool $remember = false) | bool | Authenticate against the default guard (requires a stateful guard) |
validate(array $credentials = []) | bool | Verify credentials without logging in |
check() | bool | Whether a user is authenticated |
user() | AuthUser or null | The authenticated user |
id() | int, string, or null | The authenticated identifier |
logout() | void | Log out of the default guard (requires a stateful guard) |
access(string $name) | self | Activate an access gate for the current request |
only(string ...$actions) | self | Allow only the listed actions; deny all others |
except(string ...$actions) | self | Apply the active gate to every action except those listed |
attempt() and logout() throw Phalcon\Auth\Exception when the default guard is not stateful. The token guard is stateless; use validate() with it.
Guards¶
Session Guard¶
Phalcon\Auth\Guard\Session keeps the authenticated identifier in the session manager and supports remember-me cookies and HTTP Basic. It implements GuardStateful and BasicAuth.
<?php
$session = $manager->guard('web');
// Verify credentials, start a session, set a remember-me cookie
$session->attempt(
['email' => 'jane@example.com', 'password' => 's3cret'],
true
);
// Log a known user in by identifier
$session->loginById(42);
// One-off authentication that does not persist a session
$session->once(['email' => 'jane@example.com', 'password' => 's3cret']);
// HTTP Basic authentication against the 'email' field
$session->basic('email');
$session->logout();
Remember-me requires an adapter that implements Phalcon\Contracts\Auth\Adapter\RememberAdapter (the model adapter does). Passing true to attempt()/login() with an adapter that does not implement it throws Phalcon\Auth\Exceptions\DoesNotImplement.
The session key and remember-cookie name are configurable through the guard options:
| Option | Default | Purpose |
|---|---|---|
name | auth | Session key holding the authenticated identifier |
rememberName | remember | Remember-me cookie name |
rememberTtl | 31536000 | Remember-me cookie lifetime in seconds (365 days) |
suffix | none | Derives auth_<suffix> / remember_<suffix> names for multi-guard apps |
Token Guard¶
Phalcon\Auth\Guard\Token authenticates API requests by a token. It reads the token from the request input key, or from an Authorization: Bearer <token> header. It is stateless - it has no login()/logout().
<?php
$api = $manager->guard('api');
// Resolves the user from the request token (input key or Bearer header)
$user = $api->user();
// Verify a token explicitly
$api->validate(['api_token' => 'the-token-value']);
| Option | Purpose |
|---|---|
inputKey | Request field that carries the token (for example api_token) |
storageKey | Column/field the adapter matches the token against |
Both options are required and must be non-empty; an empty value throws Phalcon\Auth\Exceptions\ConfigRequiresNonEmptyValue.
User Providers (Adapters)¶
Adapters load user rows and verify passwords. All three share password verification via Phalcon\Encryption\Security::checkHash() against the user's stored hash.
Model Adapter¶
Phalcon\Auth\Adapter\Model loads users through the Phalcon ORM. The model class must implement Phalcon\Contracts\Auth\AuthUser; for remember-me it must also implement Phalcon\Contracts\Auth\AuthRemember.
'adapter' => [
'name' => 'model',
'options' => [
'model' => Users::class,
'idColumn' => 'id', // optional, defaults to 'id'
],
],
A configured model that does not implement Phalcon\Contracts\Auth\AuthUser throws Phalcon\Auth\Exceptions\DoesNotImplement when a user is hydrated. The memory and stream adapters accept the same optional model key and enforce the contract identically.
Memory Adapter¶
Phalcon\Auth\Adapter\Memory serves an in-memory list of user rows. It is useful for tests and small read-only user sets.
'adapter' => [
'name' => 'memory',
'options' => [
'users' => [
['id' => 1, 'email' => 'jane@example.com', 'password' => '<hashed>'],
['id' => 2, 'email' => 'john@example.com', 'password' => '<hashed>'],
],
],
],
Stream Adapter¶
Phalcon\Auth\Adapter\Stream reads users from a JSON file containing an array of user records.
[
{"id": 1, "email": "jane@example.com", "password": "<hashed>"},
{"id": 2, "email": "john@example.com", "password": "<hashed>"}
]
The stream adapter throws Phalcon\Auth\Exceptions\FileDoesNotExist, FileCannotRead, FileNotValidJson, or FileDoesNotContainJson when the file is missing, unreadable, or not a JSON array.
The Authenticated User¶
When a model adapter is not configured, array-backed adapters (memory, stream) return a Phalcon\Auth\AuthUser value object. Its data must contain a scalar id key (int or string); otherwise the constructor throws Phalcon\Auth\Exceptions\DataMustContainIdKey.
<?php
$user = $manager->user();
$user->getAuthIdentifier(); // int|string - the 'id'
$user->getAuthPassword(); // string - the stored hash, or '' when absent
$user->toArray(); // the underlying user row
A model adapter returns your model instance directly, which implements the same Phalcon\Contracts\Auth\AuthUser contract.
Authorization (Access Gates)¶
An access gate decides whether the current action is allowed. A gate receives the active guard and a context describing the current dispatch (handler, module, and params); the enforcement listener builds that context. The two binary gates read only guard state and ignore the context:
Phalcon\Auth\Access\Authallows the action when a user is authenticated.Phalcon\Auth\Access\Guestallows the action when no user is authenticated.
The Phalcon\Auth\Access\Acl gate uses the context to authorize against a Phalcon\Acl adapter; it is documented below.
Activate a gate on the manager and scope it to specific actions with only() or except():
<?php
// Allow only 'dashboard' and 'profile' (when authenticated); deny all other actions
$manager->access('auth')->only('dashboard', 'profile');
// Gate every action except 'logout', which always passes
$manager->access('auth')->except('logout');
ACL Access Gate¶
Phalcon\Auth\Access\Acl authorizes an action against a Phalcon\Acl adapter, using the authenticated user's role. It brings role-based, per-handler authorization into the Auth layer.
- The ACL component is the request handler - the controller name (MVC), task name (CLI), or component name (Micro). When the dispatch carries a module, it is prefixed:
module+ separator + handler. - The ACL access is the action name.
- The role comes from the authenticated user. A user that implements
Phalcon\Acl\RoleAwareInterfacesupplies it throughgetRoleName(). When no user is authenticated, the gate uses the guest role (guestby default).
Dispatch or route parameters are passed through to the ACL adapter, so function-based ACL rules receive them.
The gate takes the ACL adapter and an options array. Activate it with setAccess():
<?php
use Phalcon\Acl\Adapter\Memory as AclMemory;
use Phalcon\Acl\Enum;
use Phalcon\Auth\Access\Acl;
$acl = new AclMemory();
$acl->setDefaultAction(Enum::DENY);
$acl->addRole('admins');
$acl->addRole('guests');
$acl->addComponent('invoices', ['index', 'edit', 'delete']);
$acl->allow('admins', 'invoices', ['index', 'edit', 'delete']);
$acl->allow('guests', 'invoices', 'index');
$manager->setAccess(new Acl($acl));
The authenticated user model supplies the role through Phalcon\Acl\RoleAwareInterface:
<?php
use Phalcon\Acl\RoleAwareInterface;
use Phalcon\Contracts\Auth\AuthUser;
use Phalcon\Mvc\Model;
class Users extends Model implements AuthUser, RoleAwareInterface
{
public function getAuthIdentifier(): int | string
{
return $this->id;
}
public function getAuthPassword(): string
{
return $this->password;
}
public function getRoleName(): string
{
return $this->role;
}
}
The gate reads two options:
| Option | Default | Purpose |
|---|---|---|
guestRole | guest | ACL role used when no user is authenticated |
moduleSeparator | : | Joins module and handler into the component (module:handler) |
<?php
use Phalcon\Auth\Access\Acl;
$manager->setAccess(
new Acl($acl, ['guestRole' => 'anonymous', 'moduleSeparator' => '-'])
);
A user that is authenticated but does not implement Phalcon\Acl\RoleAwareInterface throws Phalcon\Auth\Exception.
except() works as it does on the binary gates: the listed actions bypass the gate and are always allowed. only() differs: with the binary auth/guest gates an action not listed is denied, whereas with this gate an action not listed is allowed without an ACL check.
Enforcing Gates¶
Gates are enforced per dispatch by a listener. Phalcon\Auth\Mvc\AuthDispatcherListener guards MVC dispatch, Phalcon\Auth\Cli\AuthDispatcherListener guards CLI tasks, and Phalcon\Auth\Micro\AuthMicroListener guards Phalcon\Mvc\Micro applications. Attach the dispatcher listener to the dispatcher events manager:
<?php
use Phalcon\Auth\Mvc\AuthDispatcherListener;
use Phalcon\Events\Manager as EventsManager;
$eventsManager = new EventsManager();
$eventsManager->attach('dispatch', new AuthDispatcherListener($manager));
$dispatcher->setEventsManager($eventsManager);
Enforcement is opt-in and fail-open: the listener is a no-op when no gate is active. With no gate activated ($manager->getAccess() is null) every dispatch is allowed, so a gate must be activated for protection to apply. A gate activated with access() stays active for the rest of the request - including forwards and nested dispatches - until it is replaced. When a gate denies the action, the listener forwards to the gate's redirectTo() target if one is provided; otherwise it throws Phalcon\Auth\Exceptions\AccessDenied. The default redirectTo() returns null - override a gate to supply a forward target.
<?php
use Phalcon\Auth\Access\Auth;
class RedirectToLogin extends Auth
{
public function redirectTo(): array | null
{
return ['controller' => 'session', 'action' => 'login'];
}
}
Phalcon\Auth\Micro\AuthMicroListener enforces the active gate on a Phalcon\Mvc\Micro application. Attach it to the micro event space:
<?php
use Phalcon\Auth\Micro\AuthMicroListener;
use Phalcon\Events\Manager as EventsManager;
$eventsManager = new EventsManager();
$eventsManager->attach('micro', new AuthMicroListener($manager, 'Api'));
$app->setEventsManager($eventsManager);
The action name is the matched route's name, or the route pattern when the route is unnamed. The ACL component defaults to Micro; pass a custom name as the listener's second argument (Api above). redirectTo() targets are ignored - Micro has no forward mechanism, so a denied action always throws Phalcon\Auth\Exceptions\AccessDenied.
Events¶
The session guard fires events around login and logout on its events manager. Attach an events manager to the guard to observe them.
<?php
use Phalcon\Events\Manager as EventsManager;
$eventsManager = new EventsManager();
$eventsManager->attach('auth', function ($event, $guard) {
// $event->getType(): beforeLogin, afterLogin, beforeLogout, afterLogout
});
$manager->guard('web')->setEventsManager($eventsManager);
| Event | Fired |
|---|---|
auth:beforeLogin | Before a login is applied |
auth:afterLogin | After a successful login |
auth:beforeLogout | Before logout |
auth:afterLogout | After logout |
These events are notifications: they fire as non-cancellable, so returning false from a listener (or stopping the event) does not abort the login or logout.
Exceptions¶
Every exception thrown by the layer extends Phalcon\Auth\Exception. Granular subclasses live under Phalcon\Auth\Exceptions. Catch the parent to handle any failure, or a subclass to react to a single cause.
<?php
use Phalcon\Auth\Exception;
use Phalcon\Auth\Exceptions\AccessDenied;
try {
$manager->access('auth')->only('admin');
// ... dispatch ...
} catch (AccessDenied $ex) {
// authorization denied for an action with no redirect target
} catch (Exception $ex) {
// any other auth failure
}
| Exception | Thrown when |
|---|---|
AccessDenied | An access gate denies an action and no redirect target is set |
ConfigRequiresNonEmptyValue | A guard/adapter config receives a required empty value |
DataMustContainIdKey | An AuthUser is built from data with no scalar id |
DoesNotImplement | A required contract is missing: a remember-me adapter/model, a user model that is not an AuthUser, or a default guard that is not GuardStateful |
FileDoesNotExist | The stream adapter file is missing |
FileCannotRead | The stream adapter file cannot be read |
FileNotValidJson | The stream adapter file is not valid JSON |
FileDoesNotContainJson | The stream adapter file does not decode to an array |