Due to requirements with our reverse proxies, I had to show the user login form on a different URL. Well, no problem, I thought, I'll just copy the relevant section from user.routing.yml and display it on a different path:

  path: '/ls/login'
    _form: '\Drupal\user\Form\UserLoginForm'
    _title: 'Log in'
    _user_is_logged_in: 'FALSE'
    _maintenance_access: TRUE

This works, but if an user accesses the login URL after having logged in (typically because they bookmarked the login page), they get an "Access denied" page. I can understand it – after all, there is the requirement that the user is NOT logged is. I was thinking of ways to circumvent this, but then I noticed that the original URL (still accessible if I access the server directly rather than through the proxies) does not have this problem: if I go to /usr/login it after logging in, I get redirected to the user page, /user/{{id}}.

I don't understand how the latter works, I looked at UserLoginForm.php in addition to user.login.form, but I found no clue. What is involved here?


1 Answer 1


I don't understand how the latter works, I looked at UserLoginForm.php in addition to user.login.form, but I found no clue. What is involved here?

In core there is an exception event subscriber catching the 403 and redirecting to the current user page:



  public function onException(ExceptionEvent $event) {
    $exception = $event->getThrowable();
    if ($exception instanceof AccessDeniedHttpException) {
      $route_name = RouteMatch::createFromRequest($event->getRequest())->getRouteName();
      $redirect_url = NULL;
      if ($this->account->isAuthenticated()) {
        switch ($route_name) {
          case 'user.login';
            // Redirect an authenticated user to the profile page.
            $redirect_url = Url::fromRoute('entity.user.canonical', ['user' => $this->account->id()], ['absolute' => TRUE]);


      if ($redirect_url) {
        $event->setResponse(new RedirectResponse($redirect_url->toString()));

You can copy the class to your custom module to do the same thing for a custom route, together with the service definition (to mymodule.services.yml):

    class: Drupal\user\EventSubscriber\AccessDeniedSubscriber
    arguments: ['@current_user']
      - { name: event_subscriber }

and adjust both with custom names and namespace.

Different approach

A different approach might be to alter the existing route instead of creating a new one, so that dependencies on the route name, like this redirect code, still work. See Change '/user(/*)' URLs to 'profile(/*)'

  • The first approached worked mostly out-of-the-box, so I didn't try the second – also, it was high time that I started to use event subscribers, since they appear to be the future of Drupal. Many thanks for the quick answer! Aug 9, 2022 at 12:51
  • Just a small additional question: I restricted my event to a specific route for authenticated users, and I checked that the redirections from core still works – I guess event subscribers are executed in the order in which they are registered, so the one in core takes the precedence over mine, correct? Aug 9, 2022 at 13:02
  • 1
    They are executed in order of the priority set in getSubscribedEvents(). But even with the same priority this is no problem, because you target different route names. Only if you had targeted the same route name the priority would have mattered. The first event subscriber setting a response wins.
    – 4uk4
    Aug 9, 2022 at 13:15

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.