I wrote my own authentication plugin as described in Creating_an_Authentication_Plugin_for_Joomla.
I expected it would work.
nothing special
The reason for this is because the class Authentication
does not use the dispatcher
for authenticate
It actually checks if the plugin has a onUserAuthenticate
method.
The method should use the dispatcher
or triggerEvent
.
E.g. it could look like this:
public function authenticate($credentials, $options = array())
{
// Create authentication response
$response = new AuthenticationResponse();
Factory::getApplication()->triggerEvent('onUserAuthenticate', array($credentials, $options, $response));
//force filled response type on status STATUS_SUCCESS
if ($response->status === self::STATUS_SUCCESS) {
if (empty($response->type)) {
$response->type = $plugin->_name ?? $plugin->name;
}
}
if (empty($response->username)) {
$response->username = $credentials['username'];
}
if (empty($response->fullname)) {
$response->fullname = $credentials['username'];
}
if (empty($response->password) && isset($credentials['password'])) {
$response->password = $credentials['password'];
}
return $response;
}
Labels |
Added:
No Code Attached Yet
|
Hello @bernd5
While the issue you raised is valid, I don't think we can solve it now due to backward compatible promise (maybe until Joomla 6). From what I can see by reading the code, the solution would be:
$event->stopPropagation()
method to stop the execution on first plugin returns success authentication. While we can do that for our core plugins, there could be third party authentication plugins out there not using that method, and that's why we could not implement this change in a backward compatible manner now.So for the time being, you would need to update the plugin code to implement onUserAuthenticate
method like our core authentication plugins. The code described in our documentation is not working yet :).
Authentication
already has a dispatcher (with the subscribed events in it). The existing authentication plugins do not need to be changed because the dispatcher already handles "legacy plugins" - but not during event notification - it is done when the plugins are registered.
To stop on first success we simply need to use a custom event class. Like:
namespace Joomla\CMS\Authentication;
use Joomla\Event\Event;
class AuthenticationEvent extends Event
{
public function __construct($name, array $arguments = [])
{
parent::__construct($name, $arguments);
}
public function isStopped()
{
if (parent::isStopped())
{
return true;
}
$response = $this->getArgument("response");
if (isset($response) && $response->status === Authentication::STATUS_SUCCESS){
return true;
}
return false;
}
}
The authenticate
function can then implemented like:
public function authenticate($credentials, $options = array())
{
// Create authentication response
$response = new AuthenticationResponse();
$authEvent = new AuthenticationEvent('onUserAuthenticate', array(
"credentials" => $credentials,
"options" => $options,
"response" => $response
));
$this->dispatcher->dispatch($authEvent->getName(), $authEvent);
//force filled response type on status STATUS_SUCCESS
if ($response->status === self::STATUS_SUCCESS) {
if (empty($response->type)) {
$response->type = $plugin->_name ?? $plugin->name;
}
}
if (empty($response->username)) {
$response->username = $credentials['username'];
}
if (empty($response->fullname)) {
$response->fullname = $credentials['username'];
}
if (empty($response->password) && isset($credentials['password'])) {
$response->password = $credentials['password'];
}
return $response;
}
Legacy and modern compatible authentication plugins can be written:
class PlgAuthenticationXXX extends CMSPlugin implements SubscriberInterface
{
/**
* Application object
*
* @var CMSApplication
* @since 1.0.0
*/
protected $app;
/**
* Database object
*
* @var JDatabaseDriver
* @since 1.0.0
*/
protected $db;
/**
* Affects constructor behavior. If true, language files will be loaded automatically.
*
* @var boolean
* @since 1.0.0
*/
protected $autoloadLanguage = true;
public static function getSubscribedEvents(): array
{
return [
'onUserAuthenticate' => 'authenticate',
];
}
public function authenticate(\Joomla\Event\Event $event): bool
{
// Convert to indexed array for unpacking.
$arguments = \array_values($event->getArguments());
//delegate to "legacy" impl.
$result = $this->onUserAuthenticate(...$arguments);
return $result;
}
public function onUserAuthenticate(array $credentials, array $options, \Joomla\CMS\Authentication\AuthenticationResponse $response): bool
{
//some auth logic...
}
}
In the documentation sample the arguments are extracted from the event - this is not done in the current implementation. Just the event is passed - it is only done for legacy plugins.
We should break existing code only if really necessary.
I had used drupal - which is a great cms, too. But because of very heavy backward compatibility issues - I stopped to use it. We should avoid that.
Especially in this case - old plugins can still easily supported...
Authentication already has a dispatcher (with the subscribed events in it)
As I can see, it's a global dispatcher, mean it will trigger the event for all imported plugins (system plugins for example) which has the method implemented. The current code only process plugins from authentication group. So that change an existing behavior and could be considered as backward in-compatible (someone even worried that it could be a security issue)
Nice idea about isStopped()
override, thanks !
I like the idea too and it was on the todo list for 4.0 already. If you create a pr, please create it for the 5.0 branch and as @joomdonation already said, it must be backwards compatible because of our BC policy.
Labels |
Added:
bug
|
I was once told, that the authentication plugins behave differently for security reasons. The idea is to prevent any third party plugin from subscribing to that event and to only allow that specifically for the authentication plugins.
This can be implemented as real event, and dispatched isolated. In way very close to what @joomdonation wrote.
This also can be fully backward compatible, with use of new event name like onUserAuthenticate2
. Or with customised event, which set event property stopped
as @bernd5 #39557 (comment) suggested, or when $response->status
changed (though magic __set
).
Status | New | ⇒ | Closed |
Closed_Date | 0000-00-00 00:00:00 | ⇒ | 2023-08-27 10:44:15 |
Closed_By | ⇒ | Fedik |
A simple triggerEvent wouldn't be sufficient because of a missing
isStopped