Problem/Motivation

  • In MaintenanceModeSubscriber, there is code that redirects /user to /user/login if the site is in maintenance mode.
  • When not in maintenance mode, UserController::userPage() duplicates the login page if the user is anonymous. This duplication of the login page at two URLs causes #2346671: Two templates necessary in order to customize the login page and can potentially be penalized by search engines.
  • There is no benefit in /user duplicating rather than redirecting to /user/login, because Drupal can output links directly to /user/login, so accessing /user as an anonymous user should be rare, and a redirect in those rare circumstances isn't harmful.

Proposed resolution

  • Restrict the /user route to authenticated users only.
  • Add an AccessDenied exception listener that redirects to /user/login.
  • Remove that redirect from MaintenanceModeSubscriber, since the above can happen regardless of maintenance status.
  • Change the default front page path from user to user/login.
  • Fix tests that test the login page from testing /user to testing /user/login.

Remaining tasks

Follow-up: #2360971: The user.page route only redirects, so remove it.

User interface changes

API changes

CommentFileSizeAuthor
#62 interdiff.txt2.46 KBeffulgentsia
#62 2294571-fix-anonymous-user-login-redirect-62.patch26.42 KBeffulgentsia
#60 2294571-fix-anonymous-user-login-redirect-59.diff25.83 KBznerol
#60 interdiff.txt3.39 KBznerol
#52 interdiff.txt905 bytesznerol
#52 2294571-fix-anonymous-user-login-redirect-52.diff26.37 KBznerol
#46 2294571-fix-anonymous-user-login-redirect-46.diff26.27 KBznerol
#44 2294571-fix-anonymous-user-login-redirect-44.diff26.27 KBznerol
#40 user-acount.png6.93 KBandypost
#40 user-anon.png7.49 KBandypost
#36 2294571-fix-anonymous-user-login-redirect-36.diff26.58 KBznerol
#36 interdiff.txt771 bytesznerol
#34 2294571-fix-anonymous-user-login-redirect-34.diff26.45 KBznerol
#30 interdiff.txt848 bytesznerol
#30 2294571-fix-anonymous-user-login-redirect-30.diff26.46 KBznerol
#26 interdiff.txt622 bytesznerol
#26 2294571-fix-anonymous-user-login-redirect-26.diff26.12 KBznerol
#23 interdiff.txt800 bytesznerol
#23 2294571-fix-anonymous-user-login-redirect-23.diff26.08 KBznerol
#21 interdiff.txt2.75 KBznerol
#21 2294571-fix-anonymous-user-login-redirect-21.diff26.11 KBznerol
#19 2294571-fix-anonymous-user-login-redirect-19.diff25.95 KBznerol
#16 interdiff.txt605 bytesznerol
#16 2294571-fix-anonymous-user-login-redirect-16.diff25.93 KBznerol
#13 interdiff.txt1.59 KBznerol
#13 2294571-fix-anonymous-user-login-redirect-14.diff25.92 KBznerol
#12 interdiff.txt550 bytesznerol
#12 2294571-fix-anonymous-user-login-redirect-12.diff24.33 KBznerol
#10 2294571-fix-anonymous-user-login-redirect-10.diff25.03 KBznerol
#9 interdiff.txt7.01 KBznerol
#9 2294571-fix-anonymous-user-login-redirect-9.diff24.89 KBznerol
#7 interdiff.txt1.09 KBznerol
#7 2294571-fix-anonymous-user-login-redirect-7.diff19.29 KBznerol
#4 interdiff.txt7.52 KBznerol
#4 2294571-fix-anonymous-user-login-redirect-4.diff18.46 KBznerol
#1 2294571-fix-anonymous-user-login-redirect.diff10.94 KBznerol

Comments

znerol’s picture

Status: Active » Needs review
StatusFileSize
new10.94 KB

Status: Needs review » Needs work

The last submitted patch, 1: 2294571-fix-anonymous-user-login-redirect.diff, failed testing.

sun’s picture

Hm. This change seems to imply that

1. while you can access /user as anonymous, you are always redirected to /user/login
2. while you can access /user as authenticated, you are always redirected to /user/{id}
3. /user always redirects and no longer shows page output on its own.

I can't remember why we have the special output behavior on /user. IIRC, there have been attempts to change it in similar ways in the past already but they were shot down. Might be worth to search for old/closed issues with keywords "make /user path always redirect"…

znerol’s picture

StatusFileSize
new18.46 KB
new7.52 KB

The login-form is still rendered on /user because some tests POST there. A second reason is that the installer sets the front page to /user since the node module is not mandatory anymore.

Accessing /user via a GET request results in a redirect - this is already true for HEAD.

Let's set the front page to /user/login by default.

znerol’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 4: 2294571-fix-anonymous-user-login-redirect-4.diff, failed testing.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new19.29 KB
new1.09 KB
effulgentsia’s picture

#2288911 introduces an exception subscriber (AccessDeniedSubscriber) to redirect authenticated users from user/login and user/register to the users profile page and profile form respectively. Let's also remove the redirection of anonymous users from user to user/login from the maintenance mode subscriber to the UserController::userPage.

What about for symmetry, moving this redirect into an AccessDeniedSubscriber rather than UserController::userPage(), so that we can set access for /user to be only for authenticated users? That would allow us to remove the stupid hack we currently have in user_translated_menu_link_alter(), and the corollary concept of a MenuLinkInterface::isHidden() being carried over into #2256521: [META] New plan, Phase 2: Implement menu links as plugins, including static admin links and views, and custom links with menu_link_content entity, all managed via menu_ui module and that per #2256521-91: [META] New plan, Phase 2: Implement menu links as plugins, including static admin links and views, and custom links with menu_link_content entity, all managed via menu_ui module will cause us grief in #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees.

I can't remember why we have the special output behavior on /user

Yeah, I'd love to know that. HEAD's behavior of /user duplicating rather than redirecting to /user/login seems stupid to me, so if digging through issue history turns up something useful, it would be good to get that info into the docs of UserController::userPage().

znerol’s picture

StatusFileSize
new24.89 KB
new7.01 KB

Let's try that.

znerol’s picture

Reroll.

Status: Needs review » Needs work

The last submitted patch, 10: 2294571-fix-anonymous-user-login-redirect-10.diff, failed testing.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new24.33 KB
new550 bytes

Restore missing use-statement in form builder test.

znerol’s picture

Remove account-menu-link-hide-hack.

The last submitted patch, 12: 2294571-fix-anonymous-user-login-redirect-12.diff, failed testing.

Status: Needs review » Needs work

The last submitted patch, 13: 2294571-fix-anonymous-user-login-redirect-14.diff, failed testing.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new25.93 KB
new605 bytes

Fix the user accounts link test.

znerol’s picture

Title: Redirect anonymous users to login page in UserController::userPage instead of in MaintenanceModeSubscriber » Redirect anonymous users to login page from an exception listener instead of in MaintenanceModeSubscriber and restrict access to the my-account link to authenticated users
Issue summary: View changes
pwolanin’s picture

I think I like the solution in #2306991: MyAccountMenuLink should dynamically adjust route parameters, not hidden property better so we can actually link to the user's account and avoid any redirects.

znerol’s picture

Reroll.

Re #18: No matter how the current unfortunate user-redirect is resolved, it will be necessary to fix the tests, such that we can get rid of the duplicate user-login form on /user and /user/login. Most of the changes in this patch are for that reason.

dawehner’s picture

This looks really great!

  1. +++ b/core/modules/user/src/EventSubscriber/AccessDeniedSubscriber.php
    @@ -0,0 +1,90 @@
    +   * @todo: Use UrlGeneratorTrait (see https://www.drupal.org/node/2282161)
    +   */
    +  protected function url($route_name, $route_parameters = array(), $options = array()) {
    +    return $this->urlGenerator->generateFromRoute($route_name, $route_parameters, $options);
    +  }
    

    That todo is in already.

  2. +++ b/core/modules/user/user.links.menu.yml
    @@ -3,7 +3,6 @@ user.page:
       menu_name: account
    -  class: Drupal\user\Plugin\Menu\MyAccountMenuLink
     user.logout:
    

    <3

znerol’s picture

After rereading the discussion in #2282161: Split off link/url generation trait I have the impression that UrlGeneratorTrait should not be used here. Also the docs of the trait pretty clearly state that it should only be used in controllers.

Therefore let's follow @Crell's comment over in #2288911-32: Use route name instead of system path in user maintenance mode subscriber and instead implement a redirect helper method. Note that according to #355157: 302 response code from drupal_redirect_form() violates HTTP/1.1 spec we should use 303 here instead of 302. Also changed array-syntax in the new class.

Status: Needs review » Needs work

The last submitted patch, 21: 2294571-fix-anonymous-user-login-redirect-21.diff, failed testing.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new26.08 KB
new800 bytes

Fix parameters to redirect() method.

dawehner’s picture

+++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php
@@ -39,7 +39,7 @@ public function processInbound($path, Request $request) {
       $path = $this->config->get('system.site')->get('page.front');
       if (empty($path)) {
-        $path = 'user';
+        $path = 'user/login';
       }

Didn't we agreed that should just drop the empty check here? If you specify "" as frontpage, well it is what you wanted, maybe.

    if ($this->account->isAuthenticated()) {
      if ($path == 'user/login') {
        // If user is logged in, redirect to 'user' instead of giving 403.
        $event->setResponse(new RedirectResponse(url('user', array('absolute' => TRUE))));
        return;
      }

There is no reason to redirect to user first in order to later redirect to user/{user}.
It is also odd that this lives inside the MaintenanceModeSubscriber in general, all those user/login user/register user.page etc.
directects should happen

znerol’s picture

StatusFileSize
new26.12 KB
new622 bytes

Didn't we agreed that should just drop the empty check here?

Yes, in fact we expect page.front being set also in other places. The config value is always set during installation and also when migrating from previous versions.

There is no reason to redirect to user first in order to later redirect to user/{user}. It is also odd that this lives inside the MaintenanceModeSubscriber in general

True, however redirection for authenticated users is being worked on in #2288911: Use route name instead of system path in user maintenance mode subscriber. That one is quite a bit more complex though, see comment #27 for more details.

Status: Needs review » Needs work

The last submitted patch, 26: 2294571-fix-anonymous-user-login-redirect-26.diff, failed testing.

dawehner’s picture

But can't we simply redirect to user.page directly and be done? This doesn't seem to be a big conflict to be honest.

znerol’s picture

StatusFileSize
new26.46 KB
new848 bytes

Done.

wim leers’s picture

Status: Needs work » Needs review
dawehner’s picture

This really looks great!

+++ b/core/modules/user/src/EventSubscriber/MaintenanceModeSubscriber.php
@@ -69,7 +69,7 @@ public function onKernelRequestMaintenance(GetResponseEvent $event) {
-        $event->setResponse(new RedirectResponse(url('user', array('absolute' => TRUE))));
+        $event->setResponse(new RedirectResponse(url('user/' . $this->account->id(), array('absolute' => TRUE))));

Can't we use the url gen. directly?

+++ b/core/modules/simpletest/src/Tests/SimpleTestBrowserTest.php
@@ -86,9 +86,13 @@ public function testInternalBrowser() {
-    $HTTP_path = $system_path .'/tests/http.php?q=node';
-    $https_path = $system_path .'/tests/https.php?q=node';
+    $HTTP_path = $system_path .'/tests/http.php/user/login';
+    $https_path = $system_path .'/tests/https.php/user/login';

Another dump question, are we sure we don't loose test coverage for sites without mod rewrite?

znerol’s picture

Can't we use the url gen. directly?

I think this is out of scope, the redirects will be removed in #2288911: Use route name instead of system path in user maintenance mode subscriber.

In Drupal 8 the URL-style for non-clean URLs changed from index.php?q=/node to index.php/node, see this change record.

znerol’s picture

Status: Needs review » Needs work

The last submitted patch, 34: 2294571-fix-anonymous-user-login-redirect-34.diff, failed testing.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new771 bytes
new26.58 KB

Since #2323759: Modularize kernel exception handling the maintenance page is also displayed on pages generating exceptions (including 403, 404 etc.). Therefore it is necessary to rise the priority of the AccessDeniedSubscriber such that redirection can happen before the core MaintenanceModeSubscriber kicks in.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

I really like the general idea to not make the default behaviour of the frontpage not special for anonymous users.

+++ b/core/modules/user/src/EventSubscriber/AccessDeniedSubscriber.php
@@ -0,0 +1,97 @@
+  /**
+   * Returns a redirect response object for the specified route.
+   *
+   * @param string $route_name
+   *   The name of the route to which to redirect.
+   * @param array $route_parameters
+   *   Parameters for the route.
+   * @param int $status
+   *   The HTTP redirect status code for the redirect. The default is 303 See
+   *   Other.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect response object.
+   */
+  public function redirect($route_name, array $route_parameters = [], $status = RESPONSE::HTTP_SEE_OTHER) {
+    $url = $this->urlGenerator->generateFromRoute($route_name, $route_parameters, ['absolute' => TRUE]);
+    return new RedirectResponse($url, $status);
+  }

Out of scope but maybe worthwhile for the future: I wonder whether we should move this into the UrlGeneratorTrait?

andypost’s picture

I think that's regression, /user now does 2 redirects and if user has no access to profiles then wtf could happen.
Also "/user" a path that pointed in a lot of manuals and ddos point (no cache) ...

+++ b/core/modules/system/config/install/system.site.yml
@@ -5,7 +5,7 @@ slogan: ''
-  front: user
+  front: user/login

+++ b/core/modules/user/src/Controller/UserController.php
@@ -131,20 +131,11 @@ public function resetPass($uid, $timestamp, $hash) {
   public function userPage() {
...
+    return $this->redirect('entity.user.canonical', array('user' => $this->currentUser()->id()));

+++ b/core/modules/user/user.routing.yml
@@ -129,9 +129,9 @@ user.page:
   path: '/user'
...
-    _title: 'Log in'
+    _title: 'My account'
...
-    _access: 'TRUE'
+    _user_is_logged_in: 'TRUE'

front page is for anonymous user?

andypost’s picture

znerol’s picture

Re #38: Commented in #2344491: Move ControllerBase::redirect() to UrlGeneratorTrait.

Re #39:

if user has no access to profiles then wtf could happen.

The redirect from user to user/login will only happen for anonymous users, no problem there.

The behavior when accessing user/login as an authenticated user did not change, except that there is one redirection less. Before the browser was first redirected to user and then further to user/X from within the userPage controller action.

Also "/user" a path that pointed in a lot of manuals and ddos point (no cache) ...

It would be very easy to add cache-control headers for that redirect. It can be cached for anonymous users and I agree it should be.

front page is for anonymous user?

I admit that this looks silly. But please keep in mind that the front-page is set to node as soon as the node module is enabled. So we have this situation mainly during tests. Perhaps the system module could provide a front-page which only exists as long as the front-page settings is not yet customized, that would be cleaner.

Re #40: Regarding the http status code see #355157: 302 response code from drupal_redirect_form() violates HTTP/1.1 spec. The 302 is generated by ControllerBase::redirect().

catch’s picture

Status: Reviewed & tested by the community » Needs work

This still needs an issue summary update.

It's not clear to me at all from the discussion why the extra redirect for anonymous users is necessary. We could override the route used for the page if we have to no?

In Drupal 7 we do not have any of these redirects (I think they mainly got added because the new routing system doesn't have an equivalent of to_arg).

znerol’s picture

Issue summary: View changes

Filed #2346671: Two templates necessary in order to customize the login page. I think HEAD is broken in this regard and this patch fixes that.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new26.27 KB

Reroll.

Status: Needs review » Needs work

The last submitted patch, 44: 2294571-fix-anonymous-user-login-redirect-44.diff, failed testing.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new26.27 KB

Reuploading #44, this should pass after #2347111: StackKernelIntegrationTest is not testing a successful request got committed.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Given #2346671: Two templates necessary in order to customize the login page I would consider that this solves a bug.

alexpott’s picture

re #41

I admit that this looks silly. But please keep in mind that the front-page is set to node as soon as the node module is enabled.

I don't think this is true - in the standard profile the frontpage is node but just installing the node module in minimal does not change the frontpage. I think that this is the correct behaviour as I don't think installing a module should change the front page of your website.

This patch makes complete sense to me - having two login pages at /user and /user/login as we do in HEAD is silly.

The outstanding question is what happens if the user has no access to user profiles - I think that this question is moot because a user can always view their own profile and this patch does nothing to change that.

alexpott’s picture

Assigned: Unassigned » catch

Assigning to @catch because of his concerns about redirects. I can't see a way of avoiding them.

catch’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/user/src/Controller/UserController.php
@@ -131,20 +131,11 @@ public function resetPass($uid, $timestamp, $hash) {
+   *   Returns either a redirect to the user page or the to the user login form.

This doesn't redirect to either the user page or the login form, it's only redirecting to user.canonical.

That does make me wonder why we need to have the exception listener, as opposed to handling the redirect for anonymous users here. Having one redirect in the controller, and one in the exception listener, which checks whether we're on this route anyway seems very indirect.

Also the end result of this, regardless of the above, is that /user only ever redirects. What's the use case for the route now? Wondering if we could maybe get rid of it altogether in a follow-up.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new26.37 KB
new905 bytes

That does make me wonder why we need to have the exception listener, as opposed to handling the redirect for anonymous users here.

When an anonymous user tries to access /user (the my-account-page), a 403 exception is thrown. Therefore the redirect for anonymous users needs to happen in the exception listener.

Also the end result of this, regardless of the above, is that /user only ever redirects. What's the use case for the route now? Wondering if we could maybe get rid of it altogether in a follow-up.

The route is necessary for the my-account link. That link points to a different profile for each user.

catch’s picture

When an anonymous user tries to access /user (the my-account-page), a 403 exception is thrown. Therefore the redirect for anonymous users needs to happen in the exception listener.

That's due to the access on the route though. If we're going to redirect from the route for both cases, we can change the access. The the title callback can switch between "My Account" and "Login".

The route is necessary for the my-account link. That link points to a different profile for each user.

Yeah I'm wondering if we can change that so it actually points to the specific URL for each user - would need some special handling but would likely be less complex than a route that only does a redirect and an exception handler.

znerol’s picture

If we're going to redirect from the route for both cases, we can change the access.

This does not work in maintenance mode.

Yeah I'm wondering if we can change that so it actually points to the specific URL for each user - would need some special handling but would likely be less complex than a route that only does a redirect and an exception handler.

Introducing the exception handler is the whole point of this issue. It removes the duplicated redirect logic in the user-controller and maintenance mode subscriber by restricting the /user route to exactly one purpose.

#18 also asked for a more intelligent route on /user but as pointed out in #19 we would still need to fix the tests.

catch’s picture

This does not work in maintenance mode.

Why exactly doesn't it work?

duplicated redirect logic in the user-controller and maintenance mode subscriber

Returning the redirect response is only a single line of code, I don't think DRY applies here.

znerol’s picture

Maintenance mode happens in a request subscriber, i.e. it is before the controller is executed. If we want to redirect anonymous users to the login page, we need to do that in a high priority request event subscriber - or use an exception subscriber. #2288911: Use route name instead of system path in user maintenance mode subscriber wants to move the maintenance mode subscriber of the user module to a low priority request subscriber.

The exception subscriber approach allows us to actually assign proper access permissions and restrict the routes to their purpose. This in turn lets us remove the dubious MyAccountMenuLink class and also fixes #2346671: Two templates necessary in order to customize the login page.

I see the AccessDeniedSubscriber as a UX pattern which can be reused in similar situations. Instead of showing a 403 we want to redirect anonymous users to a meaningful target page (we do the same for authenticated users in #2346671). This is also what HEAD does - but it (mis-)uses the maintenance mode subscriber for that purpose.

effulgentsia’s picture

That's due to the access on the route though. If we're going to redirect from the route for both cases, we can change the access.

So long as we have a "My account" link that points to the /user route, and we want that link visible for authenticated and not visible for anonymous, then I think the best way to model that is via route access, as this patch is doing, which lets us remove the MyAccountMenuLink::isEnabled() hack as this patch does.

Yeah I'm wondering if we can change that so it actually points to the specific URL for each user

That might be good, but there's no working patch for that. If someone writes a patch for that, then great, but until that happens, I agree with #56's reasoning that #52 is an improvement over HEAD, and doesn't preclude removing the route entirely in a followup if someone wants to pursue that.

effulgentsia’s picture

Issue summary: View changes
effulgentsia’s picture

Status: Needs review » Needs work
Issue tags: -Needs issue summary update
Related issues: +#2360971: The user.page route only redirects, so remove it

I updated the summary and opened #2360971: The user.page route only redirects, so remove it as a follow-up. I think all of catch's other concerns have been answered, but @catch: please correct me if I'm wrong on that.

I was about to RTBC this patch, except:

The behavior when accessing user/login as an authenticated user did not change, except that there is one redirection less.

From what I can tell, that's not true. There's the same amount of redirection because MaintenanceModeSubscriber has this code that runs regardless of maintenance status that is unchanged by this patch:

if ($this->account->isAuthenticated()) {
      if ($path == 'user/login') {
        // If user is logged in, redirect to 'user' instead of giving 403.
        $event->setResponse(new RedirectResponse($this->url('user.page', [], ['absolute' => TRUE])));
        return;
      }

However, due to the above, this code:

+++ b/core/modules/system/config/install/system.site.yml
@@ -5,7 +5,7 @@ slogan: ''
-  front: user
+  front: user/login

creates one additional redirection for authenticated users: instead of HEAD's (<front> = user) -> user/N, the patch does (<front> = user/login) -> user -> user/N. But I think this can be fixed by changing the MaintenanceModeSubscriber code to redirect to entity.user.canonical directly, so setting to "needs work" for that.

znerol’s picture

Status: Needs work » Needs review
StatusFileSize
new3.39 KB
new25.83 KB

@effulgentsia thanks for the issue summary update and also for the review.

#2288911: Use route name instead of system path in user maintenance mode subscriber is supposed to address the authenticated users case, but I'm okay with fixing that line here.

Meanwhile the UrlGeneratorTrait has been introduced into MaintenanceModeSubscriber and also got a redirect() method. For the sake of removing duplicated code let's also use it in the AccessDeniedSubscriber.

effulgentsia’s picture

Status: Needs review » Reviewed & tested by the community

Thanks!

effulgentsia’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new26.42 KB
new2.46 KB

Sorry for unRTBCing, but I wanted to fix some docs. Please re-RTBC if these docs are ok.

catch’s picture

OK with #2360971: The user.page route only redirects, so remove it open and the extra redirect removed I'm happy with this otherwise, thanks for all the clarifications.

alexpott’s picture

Status: Needs review » Reviewed & tested by the community

New docs look good to me. Thanks @effulgentsia

wim leers’s picture

index 11c66fd..e8ac5d9 100644
--- a/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php
+++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php
@@ -38,9 +38,6 @@ public function __construct(ConfigFactoryInterface $config) {

@@ -38,9 +38,6 @@ public function __construct(ConfigFactoryInterface $config) {
   public function processInbound($path, Request $request) {
     if (empty($path)) {
       $path = $this->config->get('system.site')->get('page.front');
-      if (empty($path)) {
-        $path = 'user';
-      }
     }
     return $path;
   }

YAY!

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.0.x, thanks!

  • catch committed abcfdc1 on 8.0.x
    Issue #2294571 by znerol, effulgentsia: Redirect anonymous users to...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.