Summary
When a Joomla login menu item is configured with a Menu Item login redirect (the default redirect type), logging in via the System - Passkey (Passwordless) Login plugin (plg_system_webauthn) redirects the user to the site home page instead of the configured target page.
Username/password login on the same form works correctly and redirects to the configured page.
There is also a secondary issue: even when the redirect target is reached, the Webauthn plugin redirects to the raw non-SEF URL (e.g. index.php?Itemid=333) instead of the SEF equivalent (e.g. /student-home).
Steps to reproduce
- Create a Login Form menu item (Users → Login Form).
- In the Login Form menu item options, set Login Redirect → Menu Item → select any target menu item (e.g. a Page Builder page).
- Enable the System - Webauthn (Passkeys) plugin.
- Set up a passkey for a user account.
- Log out and navigate to the login page.
- Log in using the Passkey button.
Expected result
User is redirected to the selected menu item's SEF URL (e.g. https://example.com/student-home).
Actual result
User is redirected to the site home page (Uri::base()).
If the redirect is manually worked around (see below), the user reaches the correct page but via the raw non-SEF URL (https://example.com/index.php?Itemid=333) instead of the SEF URL.
System information
- Joomla: 6.1.3 Stable
- PHP: 8.x
- Plugin: System - Webauthn (Passkeys) — core plugin
- Login redirect type: Menu Item (stores a bare integer Itemid in the
return field)
Root cause
Two bugs in plg_system_webauthn, both in the PluginTraits files:
Bug 1: AjaxHandlerChallenge does not handle numeric menu item IDs
When the login redirect is configured as a "Menu Item", Joomla stores the target menu item's ID as a bare integer in the login form's hidden return field (base64-encoded). For example, if the target menu item is ID 333, the return field contains base64_encode("333").
The core com_users UserController::login() explicitly handles this case (components/com_users/src/Controller/UserController.php, lines 54-64):
if (is_numeric($data['return'])) {
$itemId = (int) $data['return'];
$data['return'] = 'index.php?Itemid=' . $itemId;
if (Multilanguage::isEnabled()) {
$language = $this->getModel('Login', 'Site')->getMenuLanguage($itemId);
if ($language !== '*') {
$data['return'] .= '&lang=' . $language;
}
}
} elseif (!Uri::isInternal($data['return'])) {
$data['return'] = '';
}
The Webauthn challenge handler (plugins/system/webauthn/src/PluginTraits/AjaxHandlerChallenge.php) decodes the returnUrl but skips the is_numeric() check and passes the value directly to Uri::isInternal(). Since Uri::isInternal("333") returns false (a bare integer has no host and doesn't start with index.php), the handler falls back to Uri::base() — the home page.
Bug 2: AjaxHandlerLogin does not route the redirect URL through Joomla's router
Even after fixing Bug 1, the login handler redirects with the raw internal URL:
$this->getApplication()->redirect($returnUrl);
The core UserController::login() routes the URL through Route::_() before redirecting (line 108):
$this->app->redirect(Route::_($this->app->getUserState('users.login.form.return'), false));
The Webauthn login handler does not, so the user sees index.php?Itemid=333 instead of the SEF URL.
Suggested fix
File 1: plugins/system/webauthn/src/PluginTraits/AjaxHandlerChallenge.php
Add the is_numeric() → index.php?Itemid=X conversion (mirroring UserController::login()), including multilanguage support.
Diff:
--- a/plugins/system/webauthn/src/PluginTraits/AjaxHandlerChallenge.php
+++ b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerChallenge.php
@@ -12,10 +12,13 @@
use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxChallenge;
use Joomla\CMS\Factory;
+use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserFactoryInterface;
use Joomla\CMS\User\UserHelper;
+use Joomla\Database\DatabaseInterface;
+use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
@@ -59,8 +62,36 @@
$returnUrl = $input->getBase64('returnUrl', $returnUrl);
$returnUrl = base64_decode($returnUrl);
- // For security reasons the post-login redirection URL must be internal to the site.
- if (!Uri::isInternal($returnUrl)) {
+ // Handle numeric menu item IDs (same as com_users UserController::login()).
+ // When the login redirect is configured as a "Menu Item" the stored value
+ // is a bare integer (the Itemid) which Uri::isInternal() rejects, causing
+ // the post-login redirect to fall back to the site root.
+ if (is_numeric($returnUrl)) {
+ $itemId = (int) $returnUrl;
+ $returnUrl = 'index.php?Itemid=' . $itemId;
+
+ if (Multilanguage::isEnabled()) {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+ $query = $db->createQuery()
+ ->select($db->quoteName('language'))
+ ->from($db->quoteName('#__menu'))
+ ->where($db->quoteName('client_id') . ' = 0')
+ ->where($db->quoteName('id') . ' = :id')
+ ->bind(':id', $itemId, ParameterType::INTEGER);
+ $db->setQuery($query);
+
+ try {
+ $language = $db->loadResult();
+ } catch (\Exception) {
+ $language = '';
+ }
+
+ if ($language !== '*' && !empty($language)) {
+ $returnUrl .= '&lang=' . $language;
+ }
+ }
+ } elseif (!Uri::isInternal($returnUrl)) {
+ // For security reasons the post-login redirection URL must be internal to the site.
// If the URL wasn't internal redirect to the site's root.
$returnUrl = Uri::base();
}
File 2: plugins/system/webauthn/src/PluginTraits/AjaxHandlerLogin.php
Wrap the redirect URL with Route::_() to produce SEF URLs, matching the core UserController::login() behavior.
Diff:
--- a/plugins/system/webauthn/src/PluginTraits/AjaxHandlerLogin.php
+++ b/plugins/system/webauthn/src/PluginTraits/AjaxHandlerLogin.php
@@ -17,6 +17,7 @@
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\PluginHelper;
+use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserFactoryInterface;
@@ -129,7 +130,7 @@
$session->set('plg_system_webauthn.userId', null);
// Redirect back to the page we were before.
- $this->getApplication()->redirect($returnUrl);
+ $this->getApplication()->redirect(Route::_($returnUrl, false));
}
}
Route::_() is safe for all returnUrl values because it returns the URL unchanged if it doesn't start with index.php (see libraries/src/Router/Route.php, Route::link() early return). This covers the Uri::base() fallback and any full-URL Uri::current() value.
Additional notes
- This affects all Joomla versions shipping the current
plg_system_webauthn (confirmed on 6.1.3; the code pattern has been unchanged since 4.x).
- The bug only manifests when the login redirect is configured as Menu Item (the default). The Internal URL redirect type works around Bug 1 but still hits Bug 2 (non-SEF redirect).
- The fix mirrors the existing, proven logic in
com_users UserController::login() — no new patterns introduced.
Summary
When a Joomla login menu item is configured with a Menu Item login redirect (the default redirect type), logging in via the System - Passkey (Passwordless) Login plugin (
plg_system_webauthn) redirects the user to the site home page instead of the configured target page.Username/password login on the same form works correctly and redirects to the configured page.
There is also a secondary issue: even when the redirect target is reached, the Webauthn plugin redirects to the raw non-SEF URL (e.g.
index.php?Itemid=333) instead of the SEF equivalent (e.g./student-home).Steps to reproduce
Expected result
User is redirected to the selected menu item's SEF URL (e.g.
https://example.com/student-home).Actual result
User is redirected to the site home page (
Uri::base()).If the redirect is manually worked around (see below), the user reaches the correct page but via the raw non-SEF URL (
https://example.com/index.php?Itemid=333) instead of the SEF URL.System information
returnfield)Root cause
Two bugs in
plg_system_webauthn, both in thePluginTraitsfiles:Bug 1:
AjaxHandlerChallengedoes not handle numeric menu item IDsWhen the login redirect is configured as a "Menu Item", Joomla stores the target menu item's ID as a bare integer in the login form's hidden
returnfield (base64-encoded). For example, if the target menu item is ID 333, thereturnfield containsbase64_encode("333").The core
com_usersUserController::login()explicitly handles this case (components/com_users/src/Controller/UserController.php, lines 54-64):The Webauthn challenge handler (
plugins/system/webauthn/src/PluginTraits/AjaxHandlerChallenge.php) decodes thereturnUrlbut skips theis_numeric()check and passes the value directly toUri::isInternal(). SinceUri::isInternal("333")returnsfalse(a bare integer has no host and doesn't start withindex.php), the handler falls back toUri::base()— the home page.Bug 2:
AjaxHandlerLogindoes not route the redirect URL through Joomla's routerEven after fixing Bug 1, the login handler redirects with the raw internal URL:
The core
UserController::login()routes the URL throughRoute::_()before redirecting (line 108):The Webauthn login handler does not, so the user sees
index.php?Itemid=333instead of the SEF URL.Suggested fix
File 1:
plugins/system/webauthn/src/PluginTraits/AjaxHandlerChallenge.phpAdd the
is_numeric()→index.php?Itemid=Xconversion (mirroringUserController::login()), including multilanguage support.Diff:
File 2:
plugins/system/webauthn/src/PluginTraits/AjaxHandlerLogin.phpWrap the redirect URL with
Route::_()to produce SEF URLs, matching the coreUserController::login()behavior.Diff:
Route::_()is safe for allreturnUrlvalues because it returns the URL unchanged if it doesn't start withindex.php(seelibraries/src/Router/Route.php,Route::link()early return). This covers theUri::base()fallback and any full-URLUri::current()value.Additional notes
plg_system_webauthn(confirmed on 6.1.3; the code pattern has been unchanged since 4.x).com_users UserController::login()— no new patterns introduced.