Skip to content

Commit

Permalink
[TASK] Add redirect to route after login
Browse files Browse the repository at this point in the history
The login controller now allows to set two new GET parameters
"redirect" (pointing to a valid route identifier) and "redirectParams"
(a list of query arguments which are rawurlencode'd).

This way, the login controller can redirect to another route
instead of dealing with a hard-coded "redirectURL", which can
be phased out in the future.

This is a first step towards allowing a defined redirect route
for all other places in the TYPO3 Backend, where currently
"redirectURL" is used. Also "linkThisScript" should become obsolete
after this change.

This change also removes the need to store a first click in
the install tool when loading the distributions within the "uc".

Resolves: #93674
Releases: master
Change-Id: Ic00113e528d00ab97215d750e1826d1ae8467a32
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/68198
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Benjamin Franzke <bfr@qbus.de>
Tested-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Benjamin Franzke <bfr@qbus.de>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Benni Mack <benni@typo3.org>
  • Loading branch information
bmack committed Mar 9, 2021
1 parent 5bd1f6c commit 732adf4
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 67 deletions.
55 changes: 32 additions & 23 deletions typo3/sysext/backend/Classes/Controller/BackendController.php
Expand Up @@ -19,6 +19,7 @@
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Domain\Repository\Module\BackendModuleRepository;
use TYPO3\CMS\Backend\Module\ModuleLoader;
use TYPO3\CMS\Backend\Routing\Router;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
Expand Down Expand Up @@ -452,32 +453,40 @@ protected function handlePageEditing(ServerRequestInterface $request): string
*/
protected function setStartupModule(ServerRequestInterface $request)
{
$startModule = preg_replace('/[^[:alnum:]_]/', '', $request->getQueryParams()['module'] ?? '');
$startModuleParameters = '';
if (!$startModule) {
$beUser = $this->getBackendUser();
// start module on first login, will be removed once used the first time
if (isset($beUser->uc['startModuleOnFirstLogin'])) {
$startModule = $beUser->uc['startModuleOnFirstLogin'];
unset($beUser->uc['startModuleOnFirstLogin']);
$beUser->writeUC();
} elseif ($this->moduleLoader->checkMod($beUser->uc['startModule']) !== 'notFound') {
$startModule = $beUser->uc['startModule'];
} else {
$startModule = $this->determineFirstAvailableBackendModule();
}
$redirectRoute = $request->getQueryParams()['redirect'] ?? '';
// Check if the route has been registered
if ($redirectRoute !== '' && isset(GeneralUtility::makeInstance(Router::class)->getRoutes()[$redirectRoute])) {
$startModule = $redirectRoute;
$moduleParameters = $request->getQueryParams()['redirectParams'] ?? '';
$moduleParameters = rawurldecode($moduleParameters);
} else {
$startModule = preg_replace('/[^[:alnum:]_]/', '', $request->getQueryParams()['module'] ?? '');
$startModuleParameters = '';
if (!$startModule) {
$beUser = $this->getBackendUser();
// start module on first login, will be removed once used the first time
if (isset($beUser->uc['startModuleOnFirstLogin'])) {
$startModule = $beUser->uc['startModuleOnFirstLogin'];
unset($beUser->uc['startModuleOnFirstLogin']);
$beUser->writeUC();
} elseif ($this->moduleLoader->checkMod($beUser->uc['startModule']) !== 'notFound') {
$startModule = $beUser->uc['startModule'];
} else {
$startModule = $this->determineFirstAvailableBackendModule();
}

// check if the start module has additional parameters, so a redirect to a specific
// action is possible
if (strpos($startModule, '->') !== false) {
[$startModule, $startModuleParameters] = explode('->', $startModule, 2);
// check if the start module has additional parameters, so a redirect to a specific
// action is possible
if (strpos($startModule, '->') !== false) {
[$startModule, $startModuleParameters] = explode('->', $startModule, 2);
}
}
}

$moduleParameters = $request->getQueryParams()['modParams'] ?? '';
// if no GET parameters are set, check if there are parameters given from the UC
if (!$moduleParameters && $startModuleParameters) {
$moduleParameters = $startModuleParameters;
$moduleParameters = $request->getQueryParams()['modParams'] ?? '';
// if no GET parameters are set, check if there are parameters given from the UC
if (!$moduleParameters && $startModuleParameters) {
$moduleParameters = $startModuleParameters;
}
}

if ($startModule) {
Expand Down
24 changes: 19 additions & 5 deletions typo3/sysext/backend/Classes/Controller/LoginController.php
Expand Up @@ -340,7 +340,7 @@ protected function init(ServerRequestInterface $request): void
$this->redirectToURL = $this->redirectUrl;
} else {
// (consolidate RouteDispatcher::evaluateReferrer() when changing 'main' to something different)
$this->redirectToURL = (string)$this->uriBuilder->buildUriFromRoute('main');
$this->redirectToURL = (string)$this->uriBuilder->buildUriWithRedirectFromRequest('main', [], $request);
}

// If "L" is "OUT", then any logged in is logged out. If redirect_url is given, we redirect to it
Expand Down Expand Up @@ -454,12 +454,26 @@ protected function createLoginLogoutForm(ServerRequestInterface $request): strin
// Might set JavaScript in the header to close window.
$this->checkRedirect($request);

// Start form
$formType = empty($this->getBackendUserAuthentication()->user['uid']) ? 'LoginForm' : 'LogoutForm';
// Show login form
if (empty($this->getBackendUserAuthentication()->user['uid'])) {
$action = 'login';
$formActionUrl = $this->uriBuilder->buildUriWithRedirectFromRequest(
'login',
[
'loginProvider' => $this->loginProviderIdentifier
],
$request
);
} else {
// Show logout form
$action = 'logout';
$formActionUrl = $this->uriBuilder->buildUriFromRoute('logout');
}
$this->view->assignMultiple([
'backendUser' => $this->getBackendUserAuthentication()->user,
'hasLoginError' => $this->isLoginInProgress($request),
'formType' => $formType,
'action' => $action,
'formActionUrl' => $formActionUrl,
'redirectUrl' => $this->redirectUrl,
'loginRefresh' => $this->loginRefresh,
'loginProviders' => $this->loginProviders,
Expand Down Expand Up @@ -531,7 +545,7 @@ protected function checkRedirect(ServerRequestInterface $request): void
break;
case 'backend':
// (consolidate RouteDispatcher::evaluateReferrer() when changing 'main' to something different)
$this->redirectToURL = (string)$this->uriBuilder->buildUriFromRoute('main');
$this->redirectToURL = (string)$this->uriBuilder->buildUriWithRedirectFromRequest('main', [], $request);
break;
}
} else {
Expand Down
28 changes: 20 additions & 8 deletions typo3/sysext/backend/Classes/Controller/MfaController.php
Expand Up @@ -56,7 +56,7 @@ public function handleRequest(ServerRequestInterface $request): ResponseInterfac
}
return $this->{$action . 'Action'}($request, $mfaProvider);
case 'cancel':
return $this->cancelAction();
return $this->cancelAction($request);
default:
throw new \InvalidArgumentException('Action not allowed', 1611879244);
}
Expand All @@ -70,6 +70,15 @@ public function authAction(ServerRequestInterface $request, MfaProviderManifestI
$view = $this->moduleTemplate->getView();
$view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/Mfa']);
$view->setTemplate('Auth');
$view->assign('formUrl', $this->uriBuilder->buildUriWithRedirectFromRequest(
'auth_mfa',
[
'action' => 'verify'
],
$request
));
$view->assign('redirectRoute', $request->getQueryParams()['redirect'] ?? '');
$view->assign('redirectParams', $request->getQueryParams()['redirectParams'] ?? '');
$view->assign('hasAuthError', (bool)($request->getQueryParams()['failure'] ?? false));
$propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
$providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::AUTH);
Expand All @@ -94,25 +103,28 @@ public function verifyAction(ServerRequestInterface $request, MfaProviderManifes
// Check if the provider can process the request and is not temporarily blocked
if (!$mfaProvider->canProcess($request) || $mfaProvider->isLocked($propertyManager)) {
// If this fails, cancel the authentication
return $this->cancelAction();
return $this->cancelAction($request);
}
// Call the provider to verify the request
if (!$mfaProvider->verify($request, $propertyManager)) {
$this->log('Multi-factor authentication failed');
// If failed, initiate a redirect back to the auth view
return new RedirectResponse($this->uriBuilder->buildUriFromRoute(
return new RedirectResponse($this->uriBuilder->buildUriWithRedirectFromRequest(
'auth_mfa',
[
'identifier' => $mfaProvider->getIdentifier(),
'failure' => true
]
],
$request
));
}
$this->log('Multi-factor authentication successfull');
$this->log('Multi-factor authentication successful');
// If verified, store this information in the session
// and initiate a redirect back to the login view.
$this->getBackendUser()->setAndSaveSessionData('mfa', true);
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('login'));
return new RedirectResponse(
$this->uriBuilder->buildUriWithRedirectFromRequest('login', [], $request)
);
}

/**
Expand All @@ -121,11 +133,11 @@ public function verifyAction(ServerRequestInterface $request, MfaProviderManifes
* other already gathered information and finally initiate a
* redirect back to the login.
*/
public function cancelAction(): ResponseInterface
public function cancelAction(ServerRequestInterface $request): ResponseInterface
{
$this->log('Multi-factor authentication canceled');
$this->getBackendUser()->logoff();
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('login'));
return new RedirectResponse($this->uriBuilder->buildUriWithRedirectFromRequest('login', [], $request));
}

/**
Expand Down
11 changes: 8 additions & 3 deletions typo3/sysext/backend/Classes/Http/RequestHandler.php
Expand Up @@ -93,9 +93,14 @@ public function handle(ServerRequestInterface $request): ResponseInterface
// Check if the router has the available route and dispatch.
return $this->dispatcher->dispatch($request);
} catch (InvalidRequestTokenException $e) {
// When token was invalid redirect to login
$loginPage = GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('login');
return new RedirectResponse((string)$loginPage);
// When token was invalid redirect to login, but keep the current route as redirect after login
$loginForm = GeneralUtility::makeInstance(UriBuilder::class)->buildUriWithRedirect(
'login',
[],
$request->getAttribute('route')->getOption('_identifier'),
$request->getQueryParams()
);
return new RedirectResponse($loginForm);
}
}
}
Expand Up @@ -69,6 +69,7 @@ class BackendUserAuthenticator extends \TYPO3\CMS\Core\Middleware\BackendUserAut
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
/** @var Route $route */
$route = $request->getAttribute('route');

// The global must be available very early, because methods below
Expand All @@ -80,15 +81,20 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
// If MFA is required and we are not already on the "auth_mfa"
// route, force the user to it for further authentication
if ($route->getOption('_identifier') !== 'auth_mfa') {
return $this->redirectToMfaAuthProcess($GLOBALS['BE_USER'], $mfaRequiredException->getProvider());
return $this->redirectToMfaAuthProcess($GLOBALS['BE_USER'], $mfaRequiredException->getProvider(), $request);
}
}

// Register the backend user as aspect and initializing workspace once for TSconfig conditions
$this->setBackendUserAspect($GLOBALS['BE_USER'], (int)$GLOBALS['BE_USER']->user['workspace_id']);
if ($this->isLoggedInBackendUserRequired($route)) {
if (!$this->context->getAspect('backend.user')->isLoggedIn()) {
$uri = GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('login');
$uri = GeneralUtility::makeInstance(UriBuilder::class)->buildUriWithRedirect(
'login',
[],
$route->getOption('_identifier'),
$request->getQueryParams()
);
$response = new RedirectResponse($uri);
return $this->enrichResponseWithHeadersAndCookieInformation($response, $GLOBALS['BE_USER']);
}
Expand Down Expand Up @@ -155,17 +161,25 @@ protected function sessionGarbageCollection(): void
*
* @param BackendUserAuthentication $user
* @param MfaProviderManifestInterface $provider
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
protected function redirectToMfaAuthProcess(
BackendUserAuthentication $user,
MfaProviderManifestInterface $provider
MfaProviderManifestInterface $provider,
ServerRequestInterface $request
): ResponseInterface {
// GLOBALS[LANG] needs to be set up, because the UriBuilder is generating a token, which in turn
// needs the FormProtectionFactory, which then builds a Message Closure with GLOBALS[LANG] (hacky, yes!)
$GLOBALS['LANG'] = LanguageService::createFromUserPreferences($user);
$uri = GeneralUtility::makeInstance(UriBuilder::class)
->buildUriFromRoute('auth_mfa', ['identifier' => $provider->getIdentifier()]);
->buildUriWithRedirectFromRequest(
'auth_mfa',
[
'identifier' => $provider->getIdentifier()
],
$request
);
$response = new RedirectResponse($uri);
// Add necessary cookies and headers to the response so
// the already passed authentication step is not lost.
Expand Down
56 changes: 56 additions & 0 deletions typo3/sysext/backend/Classes/Routing/UriBuilder.php
Expand Up @@ -83,6 +83,62 @@ public function buildUriFromRoutePath($pathInfo, $parameters = [], $referenceTyp
return $this->buildUriFromRoute($route->getOption('_identifier'), $parameters, $referenceType);
}

/**
* Creates a link to a page with a route targetted as a redirect.
* Currently works just fine for URLs built for "main" and "login" pages.
*
* @param string $name
* @param array $parameters
* @param ServerRequestInterface|null $currentRequest
* @param string $referenceType
* @return Uri
* @throws RouteNotFoundException
* @internal this is experimental API used for creating logins to redirect to a different route
*/
public function buildUriWithRedirectFromRequest(string $name, array $parameters = [], ServerRequestInterface $currentRequest = null, string $referenceType = self::ABSOLUTE_PATH): Uri
{
if ($currentRequest === null) {
return $this->buildUriFromRoute($name, $parameters, $referenceType);
}
return $this->buildUriWithRedirect($name, $parameters, $currentRequest->getQueryParams()['redirect'] ?? '', $currentRequest->getQueryParams()['redirectParams'] ?? '');
}

/**
* Creates a link to a page with a route targetted as a redirect.
* Currently works just fine for URLs built for "main" and "login" pages.
*
* @param string $name
* @param array $parameters
* @param string $redirectRouteName
* @param array $redirectParameters
* @param string $referenceType
* @return Uri
* @throws RouteNotFoundException
* @internal this is experimental API used for creating logins to redirect to a different route
*/
public function buildUriWithRedirect(string $name, array $parameters = [], string $redirectRouteName = '', $redirectParameters = [], string $referenceType = self::ABSOLUTE_PATH): Uri
{
if (empty($redirectRouteName)) {
return $this->buildUriFromRoute($name, $parameters, $referenceType);
}

$parameters['redirect'] = $redirectRouteName;
if (!empty($redirectParameters)) {
if (is_array($redirectParameters)) {
unset($redirectParameters['token']);
unset($redirectParameters['route']);
unset($redirectParameters['redirect']);
unset($redirectParameters['redirectParams']);
$redirectParameters = http_build_query($redirectParameters, '', '&', PHP_QUERY_RFC3986);
}
$redirectParameters = ltrim($redirectParameters, '&?');
if (!empty($redirectParameters)) {
$parameters['redirectParams'] = $redirectParameters;
}
}
return $this->buildUriFromRoute($name, $parameters, $referenceType);
}

/**
* Generates a URL or path for a specific route based on the given parameters.
* When the route is configured with "access=public" then the token generation is left out.
Expand Down
6 changes: 3 additions & 3 deletions typo3/sysext/backend/Resources/Private/Layouts/Login.html
Expand Up @@ -11,7 +11,7 @@ <h1 class="sr-only"><f:translate key="login.header" /></h1>
</div>
</header>
<main class="card-body">
<f:if condition="{formType} == 'LoginForm'">
<f:if condition="{action} == 'login'">
<f:then>
<f:if condition="{hasLoginError}">
<div class="t3js-login-error" id="t3-login-error">
Expand All @@ -31,7 +31,7 @@ <h1 class="sr-only"><f:translate key="login.header" /></h1>
<f:be.infobox message="{f:translate(key: 'login.error.referrer')}" state="2" />
</div>
<div class="typo3-login-form t3js-login-formfields">
<form action="?loginProvider={loginProviderIdentifier}" method="post" name="loginform" id="typo3-login-form">
<form action="{formActionUrl}" method="post" name="loginform" id="typo3-login-form">
<f:form.hidden name="login_status" value="login" />
<f:form.hidden name="userident" id="t3-field-userident" class="t3js-login-userident-field" value="" />
<f:form.hidden name="redirect_url" value="{redirectUrl}" />
Expand Down Expand Up @@ -70,7 +70,7 @@ <h1 class="sr-only"><f:translate key="login.header" /></h1>
<f:render section="ResetPassword" arguments="{_all}" />
</f:else>
<f:else>
<form action="index.php" method="post" name="loginform">
<form action="{formActionUrl}" method="post" name="loginform">
<input type="hidden" name="login_status" value="logout" />
<div class="t3-login-box-body">
<div class="t3-login-logout-form">
Expand Down

0 comments on commit 732adf4

Please sign in to comment.