Joomla 3.9.24

There's a similar question here but it deals with changing the language string, whereas I'd like to change the page content and / or redirect.

I have allowed user registration on my site and the New User Account Activation option is set to Admin, so the user will be emailed a link to activate their account before they can log in. Then all users set to receive system emails and who have the permission to create users will be notified to activate the user's account.

This works as expected. However if the user clicks the activation link a second time the following message is displayed;

Error 403 - verification code not found

The same applies to the admin approval stage, e.g. Staff Member A clicks the activation link to approve the user, all fine. Then Staff Member B clicks the same activation link later that day, they see;

Error 403 - verification code not found

It seems logical, and good UX, that rather than a 403 error (which normally means access is forbidden), there should be a custom message displayed to the user e.g.;

Sorry, there was a problem, we couldn't validate that code, it may be:

  • Already validated
  • Expired

Is there any way to display / redirect to a custom URL if the user has already been activated? Preferably without editing core files.

I'm aware that Joomla does not store the activation token once a user has been activated.

Presumably I'd have to check that the activation token didn't exist, then check that the user did exist and was activated? Then display the custom content? Not sure where or how to start this customisation.

Do I need to override core files? Create a custom plugin?

Could be reading into this too much but I think it should be a core feature of Joomla? Other sites I've registered on have this functionality (e.g. MailChimp).

  • Preferably without editing core files? No... Those 403 errors are hard coded, and you'd need to edit core files to get rid of them. You can edit the template or language file(s) to change the content, but short of editing core files you'll still have a 403 header error.
    – James
    Feb 25, 2021 at 16:42
  • This is possible with a system plugin. Either by "hijacking" the controller task in onAfterRoute event or by setting a custom error handler.
    – Sharky
    Feb 26, 2021 at 12:46
  • @Sharky that sounds interesting, have you done anything like this before? Would be interested to know more!
    – jonboy
    Feb 26, 2021 at 16:32
  • There are many examples of plugins with onAfterRoute event and for custom error handler take a look at System - Redirect plugin.
    – Sharky
    Mar 1, 2021 at 7:01

2 Answers 2


The issue still exists in Joomla 4, pretty much the same code it seems, and I think it is something that could easily be fixed in the core by someone with the access and knowledge as I think the issue comes down how differently two similar pieces of code handle an error with the activation code.

In components/com_users/controllers/registration.php this code snippet shows how it creates the 403 and stops you dead.

// Get the User ID
$userIdToActivate = $model->getUserIdFromToken($token);

if (!$userIdToActivate)
    JError::raiseError(403, JText::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND'));

    return false;

while in the file below it that handles a password reset, components/com_users/controllers/reset.php, any issues are handled much more gracefully.

// Confirm the password reset request.
$return = $model->processResetConfirm($data);

// Check for a hard error.
if ($return instanceof Exception)
    // Get the error message to display.
    if ($app->get('error_reporting'))
        $message = $return->getMessage();
        $message = JText::_('COM_USERS_RESET_CONFIRM_ERROR');

    // Go back to the confirm form.
    $this->setRedirect(JRoute::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'error');

    return false;
elseif ($return === false)
    // Confirm failed.
    // Go back to the confirm form.
    $message = JText::sprintf('COM_USERS_RESET_CONFIRM_FAILED', $model->getError());
    $this->setRedirect(JRoute::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'notice');

    return false;
    // Confirm succeeded.
    // Proceed to step three.
    $this->setRedirect(JRoute::_('index.php?option=com_users&view=reset&layout=complete', false));

    return true;

In my view the solution to this issue is not to come up with some hack work around, rather we should work to improve the core code so that it is more consistent and handle the issue with a message rather than an exception.

Maybe there is more to it than what I can see, but the second example seems to have been coded better with more thought put into how to handle the issues.


As mentioned in the comments, this can be done using a plugin to register a custom error handler. Most of the work consists of checking that we're dealing with the correct error.

defined('_JEXEC') || exit;

use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\Route;

final class PlgSystemCustomError extends CMSPlugin

    protected $app;
    private $previousHandler;

    public function onAfterRoute()
        // Some checks so we run only on the pages we need.
        if ($this->app->input->get('option') !== 'com_users' || $this->app->input->get('task') !== 'registration.activate')

        // Get previous error handler so we can restore it later.
        $this->previousHandler = JError::getErrorHandling(E_ERROR);

        // Set our custom error handler.
        JError::setErrorHandling(E_ERROR, 'callback', array($this, 'handleError'));

    public function handleError(JException $error)
         * Check that we are handling the error we want.
         * Note that this isn't really reliable as this completely depends on code in the controller.
         * This will break when language constant or error code is changed.
        if ($error->getCode() !== 403 || $error->getMessage() !== Text::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND'))
            if ($this->previousHandler)
                // Reset previous error handler.
                JError::setErrorHandling(E_ERROR, ...$this->previousHandler);

                // Rethrow the error.
                throw $error;

            // If previous error handler is not set, render default error page.

        // Now add our actual custom handling. In this case we redirect and show a message.
        $this->app->enqueueMessage($error->getMessage(), 'Warning');

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.