We are in the process of implementing SSO and using Joomla as the IDP. In order to understand how we represent data, let's consider the following tables:
jos_sso_identities
: A table that stores all the identities of users, including non-Joomla identities. This is a new table and it has username, password and email among other fieldsjos_sso_users
: A table that maps identities to applications whereforeign_id
is a field that represents theid
of the user in the database of the SPjos_users
: Well-known Joomla table for usersjos_sso_apps
: A table where we store the apps an identity may have a user
The Joomla site is stored inside jos_sso_apps
with an id
of 1. All other apps have an id
> 1.
An identity may have a Joomla user (then this is a superuser) or may have only users in the SPs using the SSO. We have implemented full database support as well as plugins that handle authentication, registration, user save, etc and endpoints that directly respond to server-side curl requests from non-Joomla SPs.
So, for the purpose of this question we have two main kinds of identities:
- Superusers are identities who have a Joomla user as well as a
jos_sso_users
record withapp_id
being 1 andforeign_id
being equal to the correspondingjos_users.id
valu - Lower admins are identities who have no Joomla user, but
jos_sso_users
records whoseapp_id
is > 1 andforeign_id
being equal to the id said user has inside the SP
We intend to adjust Joomla's Forgot Password feature so that it will refer to jos_sso_identities
rather than jos_sso_users
. My first research found the file located at components/com_users/src/Model/ResetModel.php representing the class of Joomla\Component\Users\Site\Model\ResetModel
and I would like to override this model so that it will search for emails inside jos_sso_identities
rather than jos_users
so that even the identities who have no Joomla users can recover their passwords.
Currently it seems that I need to override processResetConfirm
:
public function processResetConfirm($data)
{
// Get the form.
$form = $this->getResetConfirmForm();
// Check for an error.
if ($form instanceof \Exception) {
return $form;
}
// Filter and validate the form data.
$data = $form->filter($data);
$return = $form->validate($data);
// Check for an error.
if ($return instanceof \Exception) {
return $return;
}
// Check the validation results.
if ($return === false) {
// Get the validation messages from the form.
foreach ($form->getErrors() as $formError) {
$this->setError($formError->getMessage());
}
return false;
}
// Find the user id for the given token.
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName(['activation', 'id', 'block']))
->from($db->quoteName('#__users'))
->where($db->quoteName('username') . ' = :username')
->bind(':username', $data['username']);
// Get the user id.
$db->setQuery($query);
try {
$user = $db->loadObject();
} catch (\RuntimeException $e) {
return new \Exception(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage()), 500);
}
// Check for a user.
if (empty($user)) {
$this->setError(Text::_('COM_USERS_USER_NOT_FOUND'));
return false;
}
if (!$user->activation) {
$this->setError(Text::_('COM_USERS_USER_NOT_FOUND'));
return false;
}
// Verify the token
if (!UserHelper::verifyPassword($data['token'], $user->activation)) {
$this->setError(Text::_('COM_USERS_USER_NOT_FOUND'));
return false;
}
// Make sure the user isn't blocked.
if ($user->block) {
$this->setError(Text::_('COM_USERS_USER_BLOCKED'));
return false;
}
// Push the user data into the session.
$app = Factory::getApplication();
$app->setUserState('com_users.reset.token', $user->activation);
$app->setUserState('com_users.reset.user', $user->id);
return true;
}
and processResetRequest
:
public function processResetRequest($data)
{
$app = Factory::getApplication();
// Get the form.
$form = $this->getForm();
$data['email'] = PunycodeHelper::emailToPunycode($data['email']);
// Check for an error.
if ($form instanceof \Exception) {
return $form;
}
// Filter and validate the form data.
$data = $form->filter($data);
$return = $form->validate($data);
// Check for an error.
if ($return instanceof \Exception) {
return $return;
}
// Check the validation results.
if ($return === false) {
// Get the validation messages from the form.
foreach ($form->getErrors() as $formError) {
$this->setError($formError->getMessage());
}
return false;
}
// Find the user id for the given email address.
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__users'))
->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)')
->bind(':email', $data['email']);
// Get the user object.
$db->setQuery($query);
try {
$userId = $db->loadResult();
} catch (\RuntimeException $e) {
$this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage()));
return false;
}
// Check for a user.
if (empty($userId)) {
$this->setError(Text::_('COM_USERS_INVALID_EMAIL'));
return false;
}
// Get the user object.
$user = User::getInstance($userId);
// Make sure the user isn't blocked.
if ($user->block) {
$this->setError(Text::_('COM_USERS_USER_BLOCKED'));
return false;
}
// Make sure the user isn't a Super Admin.
if ($user->authorise('core.admin')) {
$this->setError(Text::_('COM_USERS_REMIND_SUPERADMIN_ERROR'));
return false;
}
// Make sure the user has not exceeded the reset limit
if (!$this->checkResetLimit($user)) {
$resetLimit = (int) Factory::getApplication()->getParams()->get('reset_time');
$this->setError(Text::plural('COM_USERS_REMIND_LIMIT_ERROR_N_HOURS', $resetLimit));
return false;
}
// Set the confirmation token.
$token = ApplicationHelper::getHash(UserHelper::genRandomPassword());
$hashedToken = UserHelper::hashPassword($token);
$user->activation = $hashedToken;
$event = AbstractEvent::create(
'onUserBeforeResetRequest',
[
'subject' => $user,
]
);
$app->getDispatcher()->dispatch($event->getName(), $event);
// Save the user to the database.
if (!$user->save(true)) {
return new \Exception(Text::sprintf('COM_USERS_USER_SAVE_FAILED', $user->getError()), 500);
}
// Assemble the password reset confirmation link.
$mode = $app->get('force_ssl', 0) == 2 ? 1 : (-1);
$link = 'index.php?option=com_users&view=reset&layout=confirm&token=' . $token;
// Put together the email template data.
$data = $user->getProperties();
$data['sitename'] = $app->get('sitename');
$data['link_text'] = Route::_($link, false, $mode);
$data['link_html'] = Route::_($link, true, $mode);
$data['token'] = $token;
$mailer = new MailTemplate('com_users.password_reset', $app->getLanguage()->getTag());
$mailer->addTemplateData($data);
$mailer->addRecipient($user->email, $user->name);
// Try to send the password reset request email.
try {
$return = $mailer->send();
} catch (\Exception $exception) {
try {
Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror');
$return = false;
} catch (\RuntimeException $exception) {
$app->enqueueMessage(Text::_($exception->errorMessage()), 'warning');
$return = false;
}
}
// Check for an error.
if ($return !== true) {
return new \Exception(Text::_('COM_USERS_MAIL_FAILED'), 500);
}
$event = AbstractEvent::create(
'onUserAfterResetRequest',
[
'subject' => $user,
]
);
$app->getDispatcher()->dispatch($event->getName(), $event);
return true;
}
It is very much possible that we will need to override other assets too in order to achieve this goal and of course this could be achieved in a very hacky way that would involve changing the reference from jos_users
to the table of jos_sso_identities
in this context, but that would be a very volatile solution which would greatly increase the difficulty of applying Joomla updates.
So the question is as follows: How can I override ResetModel
methods in such a way that it will use my jos_sso_identities
table instead of jos_users
table in a neat way, that is, I would preferably create a new class
that extends ResetModel
and overrides the methods and then Joomla would put in use my new class
and execute its methods instead of ResetModel
. So how could I tell ResetController
to use my custom class instead of the class
Joomla has provided for ResetModel
?