No Code Attached Yet bug
avatar bernd5
bernd5
4 Jan 2023

Steps to reproduce the issue

I wrote my own authentication plugin as described in Creating_an_Authentication_Plugin_for_Joomla.

Expected result

I expected it would work.

Actual result

grafik

System information (as much as possible)

nothing special

Additional comments

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;
}
avatar bernd5 bernd5 - open - 4 Jan 2023
avatar joomla-cms-bot joomla-cms-bot - change - 4 Jan 2023
Labels Added: No Code Attached Yet
avatar joomla-cms-bot joomla-cms-bot - labeled - 4 Jan 2023
avatar bernd5
bernd5 - comment - 4 Jan 2023

A simple triggerEvent wouldn't be sufficient because of a missing isStopped

avatar joomdonation
joomdonation - comment - 8 Jan 2023

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:

  1. Passing a separate dispatcher object to Authentication class and use that dispatcher object to dispatch the event. By doing that, we don't have to worry about the method from other random plugins (for example, system plugin has that method).
  2. Convert our authentication plugins to new plugin structure so that we can use $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 :).

avatar bernd5
bernd5 - comment - 8 Jan 2023

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.

avatar bernd5
bernd5 - comment - 8 Jan 2023

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...

avatar joomdonation
joomdonation - comment - 8 Jan 2023

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 !

avatar laoneo
laoneo - comment - 9 Jan 2023

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.

avatar Hackwar Hackwar - change - 23 Feb 2023
Labels Added: bug
avatar Hackwar Hackwar - labeled - 23 Feb 2023
avatar Hackwar
Hackwar - comment - 25 Aug 2023

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.

avatar Fedik
Fedik - comment - 26 Aug 2023

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).

avatar Fedik Fedik - change - 27 Aug 2023
Status New Closed
Closed_Date 0000-00-00 00:00:00 2023-08-27 10:44:15
Closed_By Fedik
avatar Fedik Fedik - close - 27 Aug 2023
avatar Fedik
Fedik - comment - 27 Aug 2023

Please test #41485

Add a Comment

Login with GitHub to post a comment