feat(auth)!: overhaul authentication and access control #1516
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Warning
This pull request depends on #1513, #1514 and #1515, so CI is expected to fail until I rebase it.This pull request is a complete overhaul of the auth package.
Authentication
My main goal was removing the assumption that an authenticated model is a user (it could be an access token, an external application, or anything else that is not necessarily a "user").
Moreover, the previous implementation's
SessionAuthenticator
assumed that users were in a database. While true in most cases, this new implementation offers the ability to easily override this logic by providing anAuthenticatableResolver
interface:The default implementation is a database implementation and requires the model to have a primary key (not necessarily named
id
). But it is possible to override it, for instance if the application interfaces with LDAP.The concept of authenticator is preserved, with a default session-based implementation that depends on the
AuthenticatableResolver
interface to resolve a unique ID from the authenticatable model or resolve the authenticatable model instance given the authenticated ID.Note to myself: I don't think it is possible, with this implementation, to support different authenticatable models at once (eg. a✅User
and aExternalApplication
or something). I should explore that before merging.Access control
My second goal was to not force a RBAC model on Tempest developers, as this is often very limiting in medium-sized+ applications. Instead, I implemented an ABAC (policy-based) model, which can use a RBAC model under the hood if desired. This is more flexible, and familiar with what Laravel and Symfony provide.
It is still possible to implement in userland the
#[Allow]
attribute that the previous implementation provided, but this is not included (for now). If we decide to provide an RBAC implementation, it should be done through an installer that would publish it to the user's application.The ABAC implementation uses
#[PolicyFor(Resource::class)]
attributes and is quite flexible. It also interfaces with the authenticator to provide the currently-authenticated model if a subject is not specified.Here are the usual ABAC terms:
view
,edit
, ordelete
.Here is what creating a policy looks like. They could be in any class (like controller actions or console commands). The
#[PolicyFor]
attribute requires the resource as its first argument and an optional action as its second one. If not provided, the action is the method name.The method associated to the attribute accepts the resource as its first argument and the subject as the second one. Both are optional: if the resource doesn't exist (eg. when creating it), it's not needed. And if the subject is not authenticated or provided, it's not needed either.
To determine if a subject has access to a resource, the
AccessControl
interface must be used. It has adenyAccessUnlessGranted
method that throws if access is denied, and anisGranted
method that returns a boolean.Note that in the
PolicyBasedAccessControl
implementation ofAccessControl
, the methods associated to#[PolicyFor]
are checked and a nice exception is thrown if the parameters don't accept the required types:Considerations
We should consider supporting multiple authenticatable models at once.✅I was thinking about providing different authentication strategies by default.For instance, frameworks usually provide a way to login with a username and a password. This implies too many assumptions that get in the way as soon as we use a slightly different authentication paradigm, so it's annoying. So currently, users have to use thePasswordChecker
and fetch the user from the database manually (which is perfectly fine and not a lot of LoC).If we find a way to do that, we could also provide other strategies, like passkeys, magic links, token-based auth, and maybe OAuth2 could fit this scope as well.