Examples

In this section some common situations are described:

Send visitors to login page and redirect to original page

A common situation is that visitors who go to a page with access restrictions should go to a login page first and after logging in should be send back to the page they originally requested.

Assume we have a login page with id 2.

Using TypoScript we can still display links to access restricted pages and send visitors to the login page:

config {
    typolinkLinkAccessRestrictedPages = 2
    typolinkLinkAccessRestrictedPages_addParams = &return_url=###RETURN_URL###
}
Copied!

On the login page the login form must be configured to redirect to the original page:

plugin.tx_felogin_login.settings.redirectMode = getpost
Copied!

(This option can also be set in the flexform configuration of the felogin content element)

If visitors will directly enter the URL of an access restricted page they will be sent to the first page in the rootline to which they have access. Sending those direct visits to a login page is not a job of the felogin plugin, but requires a custom page-not-found handler. In this sense, we refer to Custom error handler implementation for 403 redirects.

Custom error handler implementation for 403 redirects

This section explains how to utilize a custom error handler to catch 403 restricted page errors and allow to forward to a login form, and then redirect back to the originating page after successful login.

  1. You need the following site settings in the error handling

    Error Handling tab of Site Configuration module

    There you add the custom 403 error handler and configure the error handler, you create in the following steps.

  2. Look up the page ID where a login form (like with EXT:felogin) is placed

    This page ID is needed in the following step, so that the error handler will know, where to forward an unauthenticated user to, so that a login can be performed.

    Ideally, this should be done by configuring a page ID via the site settings, and referring back to a named ID. See PHP API: accessing site configuration for more information. For reduced complexity, this example uses a hard-coded page ID.

  3. Create a new error handler RedirectLoginErrorHandler.php

    Create a PHP error handler class like the following in a custom extension, like your own sitepackage:

    EXT:my_sitepackage/Classes/Error/PageErrorHandler/RedirectLoginErrorHandler.php
    <?php
    
    declare(strict_types=1);
    
    /*
     * This file is part of the TYPO3 CMS project.
     *
     * It is free software; you can redistribute it and/or modify it under
     * the terms of the GNU General Public License, either version 2
     * of the License, or any later version.
     *
     * For the full copyright and license information, please read the
     * LICENSE.txt file that was distributed with this source code.
     *
     * The TYPO3 project - inspiring people to share!
     */
    
    namespace MyVendor\MySitePackage\Error\PageErrorHandler;
    
    use Psr\Http\Message\ResponseInterface;
    use Psr\Http\Message\ServerRequestInterface;
    use TYPO3\CMS\Core\Context\Context;
    use TYPO3\CMS\Core\Controller\ErrorPageController;
    use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerInterface;
    use TYPO3\CMS\Core\Http\HtmlResponse;
    use TYPO3\CMS\Core\Http\RedirectResponse;
    use TYPO3\CMS\Core\LinkHandling\LinkService;
    use TYPO3\CMS\Core\Site\Entity\Site;
    use TYPO3\CMS\Core\Utility\GeneralUtility;
    use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
    
    /**
     * An error handler that redirects to a configured page, where the login
     * process is handled. Passes a configurable URL parameter (`return_url` or
     * `redirect_url`) to the target page.
     */
    final class RedirectLoginErrorHandler implements PageErrorHandlerInterface
    {
        private const PAGE_ID_LOGIN_FORM = 656;
    
        private readonly int $loginRedirectPid;
        private readonly string $loginRedirectParameter;
        private readonly Context $context;
        private readonly LinkService $linkService;
        private readonly ErrorPageController $errorPageController;
    
        public function __construct(private readonly int $statusCode)
        {
            $configuration = [
                // TODO: Replace with $siteSettings[...] or something else
                'loginRedirectTarget' => 't3://page?uid=' . self::PAGE_ID_LOGIN_FORM,
                'loginRedirectParameter' => 'return_url',
            ];
    
            $this->context = GeneralUtility::makeInstance(Context::class);
            $this->linkService = GeneralUtility::makeInstance(LinkService::class);
            $this->errorPageController = GeneralUtility::makeInstance(ErrorPageController::class);
    
            $urlParams = $this->linkService->resolve($configuration['loginRedirectTarget']);
            $this->loginRedirectPid = (int)($urlParams['pageuid'] ?? 0);
            $this->loginRedirectParameter = $configuration['loginRedirectParameter'];
        }
    
        public function handlePageError(
            ServerRequestInterface $request,
            string $message,
            array $reasons = []
        ): ResponseInterface {
            $this->checkHandlerConfiguration();
    
            if ($this->shouldHandleRequest($reasons)) {
                return $this->handleLoginRedirect($request);
            }
    
            // Show general error message with a 403 HTTP status code
            return $this->getGenericAccessDeniedResponse($message);
        }
    
        private function getGenericAccessDeniedResponse(string $reason): ResponseInterface
        {
            $reason = $reason ? ' Reason: ' . $reason : '';
            $content = $this->errorPageController->errorAction(
                'Page Not Found',
                sprintf('The page did not exist or was inaccessible.%s', $reason),
                0,
                $this->statusCode,
            );
            return new HtmlResponse($content, $this->statusCode);
        }
    
        private function handleLoginRedirect(ServerRequestInterface $request): ResponseInterface
        {
            if ($this->isLoggedIn()) {
                return $this->getGenericAccessDeniedResponse(
                    'The requested page was not accessible with the provided credentials'
                );
            }
    
            /** @var Site $site */
            $site = $request->getAttribute('site');
            $language = $request->getAttribute('language');
    
            $loginUrl = $site->getRouter()->generateUri(
                $this->loginRedirectPid,
                [
                    '_language' => $language,
                    $this->loginRedirectParameter => (string)$request->getUri(),
                ]
            );
    
            return new RedirectResponse($loginUrl);
        }
    
        private function shouldHandleRequest(array $reasons): bool
        {
            if (!isset($reasons['code'])) {
                return false;
            }
    
            $accessDeniedReasons = [
                PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED,
                PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED,
            ];
            $isAccessDenied = in_array($reasons['code'], $accessDeniedReasons, true);
    
            return $isAccessDenied || $this->isSimulatedBackendGroup();
        }
    
        private function isLoggedIn(): bool
        {
            if ($this->context->getPropertyFromAspect('frontend.user', 'isLoggedIn')) {
                return true;
            }
            return $this->isSimulatedBackendGroup();
        }
    
        protected function isSimulatedBackendGroup(): bool
        {
            if (!$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn')) {
                return false;
            }
            // look for special "any group"
            $groups = $this->context->getPropertyFromAspect('frontend.user', 'groupIds');
            return $groups[1] === -2;
        }
    
        private function checkHandlerConfiguration(): void
        {
            if ($this->loginRedirectPid === 0) {
                throw new \RuntimeException('No loginRedirectTarget configured for LoginRedirect errorhandler', 1700813537);
            }
    
            if ($this->statusCode !== 403) {
                throw new \RuntimeException(sprintf('Invalid HTTP status code %d for LoginRedirect errorhandler', $this->statusCode), 1700813545);
            }
        }
    }
    
    Copied!

    Adapt the constant PAGE_ID_LOGIN_FORM to match the page ID from the previous step. Since there is no proper way how to do it otherwise, we put in the page ID of the login form hard-coded into the file RedirectLoginErrorHandler.php and define a constant PAGE_ID_LOGIN_FORM for it. In the example above, this is set to 656.

  4. In your EXT:felogin plugin, make sure you selected "Defined by GET/POST Parameters" as first redirect mode

    Plugin > Redirects tab of Login Form content element

    You need to configure the login form that receives your redirect in a way, that allows to evaluate submitted URL parameters. In EXT:felogin, this is achieved via this Redirect Mode (which can also be set through TypoScript configuration, see redirectMode.

    Your login form will probably also need to define a specific target page for normal logins (independent from the error handler redirect), so you should also add a redirectMode like login to your list, and set a target page in redirectPageLogin.

  5. Testing the custom error handler

    Clear the caches, for example via the backend module Admin Tools > Maintenance.

    Then open any access-restricted page in an incognito browser window to be sure that you are not logged in yet. Here we will use the example URL https://example.org/restricted/page.

    When everything is configured correctly and if you are not logged in yet, then you should be redirected to your login page like https://example.org/login (example page ID 656).

    After entering proper frontend user credentials, you should be redirected back to https://example.org/restricted/page, the page where you wanted to get to initially.

This example was taken from [FEATURE] Introduce ErrorHandler for 403 errors with redirect option which works in TYPO3 v11 and v12, and has been integrated to TYPO3 v13, where it can be used without a custom implementation.