There is a new CakePHP session authenticator on the block.
Status quo
Historically, the Session provider in CakePHP 3.x times using the AuthComponent stored an array of your user (entity) in the session upon login.
It was accessed using Auth.User
, so Auth.User.id
was the user’s ID.
With the split and adaptation of separate auth plugins and components, the data here became an Identity object, and in the session the
user got stored as the User entity object itself.
The session key also changed to Auth
, meaning the ID would now be accessed as Auth.id
, since Auth
is the session key for the User entity
(identity) directly.
On top of that, the access is now purely through the Identity in the request object, as it could also always come from Cookie or other authenticators.
This User session now being an Entity
object makes things more problematic.
As now we have more objects in the session directly (serialized). Not just DateTime
and (backed) enums, but also User
entity and possible contained relations.
If any of those change just slightly upon a deployment, whole sessions would be wiped out due to the unserialization not working out anymore.
These session invalidations can, of course, be mitigated to some extent by adding Cookie
auth on top.
For the last years, I stuck to my TinyAuth.Auth component.
So those changes didn’t really affect me.
But when I needed to actually integrate some new apps with the plugin approach, I started to look more into it.
In the process, I also rewrote the authenticators to prefer the identifier directly (normal dependency inversion), as this is also a safer approach.
Using a shared IdentifierCollection seems not only overkill, but it can also be harmful if you by accident have identifier keys colliding.
So best to stick to one specific Identifier (collection) per Authenticator. This has been released as v3.3.0 now.
For details on how the authenticator works, see the official docs.
toArray()/fromArray()
One idea I looked into recently was to always toArray()
or json_encode()
the session auth data before storing it.
And restoring it upon read.
From the outside the authenticator would not be any different.
This also worked actually, as this PR shows.
There were concerns that this could be an issue in edge cases, and that not all fields can be safely serialized.
E.g. blob/binary data in a column because its encrypted (and gets en/decrypted by table events).
So I eventually dropped this approach.
PrimaryKeySession authenticator
With those findings in mind, I explored the idea of storing only the user id in the session and freshly building the identity from it using the DB users table data.
It is working quite nicely:
$service->loadAuthenticator('Authentication.PrimaryKeySession', [
'identifier' => [
'Authentication.Token', [
'dataField' => 'key', // incoming data
'tokenField' => 'id', // lookup for DB table
'resolver' => 'Authentication.Orm',
],
],
'urlChecker' => 'Authentication.CakeRouter',
'loginUrl' => [
'prefix' => false,
'plugin' => false,
'controller' => 'Users',
'action' => 'login',
],
]);
This has another positive side effect: The data is now always up to date, no more issues when
- The user edits his account data and we have to "persist changes back into session identity"
- Any admin modifies the user’s data and they are now out of sync until a fresh login.
Custom finder
I personally always use a findActive
custom finder on top, to prevent logins of not activated or blocked users:
'identifier' => [
'Authentication.Token', [
'dataField' => 'key', // incoming data
'tokenField' => 'id', // lookup for DB table
'resolver' => [
'className' => 'Authentication.Orm',
'finder' => 'active',
],
],
],
with e.g. in UsersTable:
public function findActive(SelectQuery $query): SelectQuery
{
return $query->where(['email_verified IS NOT' => null]);
}
If you need to actually also fetch related data into the identity and contain e.g. Roles or alike, you can also wrap this as
'finder' => 'auth',
This allows you to use both contain()
and where()
in the same finder.
Caching
If you use a custom auth finder that does quite a few extra joins and query, e.g.
User "contains" Groups, Permissions, Roles, ProfileData, ...
you might want to add a Cache layer in between to mitigate the constant DB queries.
Then it will fetch this larger dataset from a quick (ideally memcache or redis) cache, and only require DB lookup once the data changed
and the cache got invalided.
If you use caching, you need to do the invalidation yourself. It is still much easier than having to manually rewrite the identity into the session.
All you need to do is SessionCache::delete($uid);
, given that you configured it using Configure.
Once the cached session data cannot be found, it will just look it up in the DB again and then re-cache for the next request.
Summary
Why I recommend switching to PrimaryKeySession:
- No more issues with deployments and changes to objects
- Always up to date User (session) data
- Much smaller session data storage size (across all users, especially if using DB session)
Things to look out for:
- Invalidating the cache if you add this layer to ensure users don’t have to log out and log back in manually
The official CakePHP plugin has the base version available, if you want caching included, use the TinyAuth plugin’s "extended edition".
Docs: github.com/dereuromark/cakephp-tinyauth/blob/master/docs/AuthenticationPlugin