This issue is part of #2371629: [meta] Finalize Session and User Authentication API and #2047951: [META] Remove calls to deprecated global $user and $GLOBALS['user']

Problem/Motivation

The coupling of the 'current_user' service to AuthenticationManager creates a circular dependency that makes it difficult to implement alternative authentication schemes like basic_auth (see #2283637: Provide test coverage to prove that an AuthenticationProvider can initiate a session). The current_user service originally was a synthetic service that caused problems. #2180109: Change the current_user service to a proxy fixed these problems by
a) introducing a transparent proxy around the actual user object in order to remove the requirement to declare it synthetic and
b)introducing on-demand authentication (i.e. user authentication is triggered as soon as a method is called on the current_user object).

However, in order to solve the problems a) is sufficient. b), however, introduces the possibility of user authentication with different authentication methods at multiple points in the request/response cycle e.g. when the container is rebuilt during tests or when enabling/disabling modules. If there are credentials for multiple authentication methods on one request (e.g. a developer logged into a site, testing some requests with token authentication), the behavior of the authentication system is hard to predict (see #104).

Authentication must only be performed once during the request / response cycle. User authentication requires checking the credentials sent along with the initial HTTP request based on a pre-selected authentication method.

The current system allows for a mix of multiple authentication methods. Because not all of them are equally secure, the system needs to support the restriction of authentication methods to only a subset of the site. This is especially true for HTTP basic authentication or URL token based authentication which are commonly used for machine-to-machine communication.

On a 403 (access denied), if there are no credentials on the request, some authentication methods (e.g. basic auth) require that a challenge is sent to the client. For obvious reasons this functionality also needs to be restricted to those routes where such providers are active.

Currently this dilemma is resolved by resetting authentication from within AuthenticationEnhancer if there is a mismatch between the allowed authentication methods and the chosen one. In order to minimize the opportunity for security problems, it would be better to simply bail out and deny access in this case.

Proposed resolution

  • Remove the on-demand authentication of the current user in AccountProxy::setAccount() and implement authentication in an EventSubscriber that runs before language processing, routing and any other consumer of the current_user service. This provides a single point of entry for authentication which is easier to audit and control. This effectively removes the dependency of the current_user service on the authentication manager and the request object.
  • Remove the AuthenticationEnhancer and replace it with an additional request listener on the AuthenticationSubscriber. The new listener runs after routing and filters the selected authentication provider according to the list of allowed providers specified in the _auth option of the current route. Throws AccessDeniedHttpException exception if it detects a mismatch.
  • Make sure that all implementers of AuthenticationProviderInterface respect the liskov substitution principle, i.e. AuthenticationManager should not be a special flower, neither should BasicAuth or the Cookie provider. Also ensure that AuthenticationSubscriber accepts any AuthenticationProviderInterface.

Remaining tasks

Reviews and commit.

User interface changes

None

API changes

  1. AuthenticationManagerInterface is completely removed
  2. AuthenticationProviderInterface methods removed:
    • cleanup() (dead code)
    • handleException() (moved to new <code>AuthenticationProviderChallengeInterface)
  3. AuthenticationEnhancer replaced by implementors of AuthenticationProviderFilterInterface, called from AuthenticationSubscriber

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue priority Critical
Prioritized changes The main goal of this issue is security and reducing brittleness.
Disruption Mildly disruptive for core/contributed/custom modules
CommentFileSizeAuthor
#231 2286971-auth-request-subscriber-231-interdiff.txt7.07 KBBerdir
#231 2286971-auth-request-subscriber-231.patch56.51 KBBerdir
#222 2286971-auth-request-subscriber-222-interdiff.txt850 bytesBerdir
#222 2286971-auth-request-subscriber-222.patch55.4 KBBerdir
#221 2286971-auth-request-subscriber-221-interdiff.txt3.84 KBBerdir
#221 2286971-auth-request-subscriber-221.patch54.8 KBBerdir
#214 2286971-auth-request-subscriber-214-interdiff.txt1.68 KBBerdir
#214 2286971-auth-request-subscriber-214.patch51.99 KBBerdir
#211 2286971-auth-request-subscriber-211-interdiff.txt2.59 KBBerdir
#211 2286971-auth-request-subscriber-211.patch52 KBBerdir
#199 interdiff.txt5.49 KBznerol
#199 2286971-auth-request-subscriber-199.diff52.28 KBznerol
#188 auth-method-test-php.txt1.48 KBznerol
#188 at.tgz716 bytesznerol
#187 interdiff.txt1.74 KBznerol
#187 2286971-auth-request-subscriber-186.diff51.39 KBznerol
#185 interdiff.txt10.02 KBznerol
#185 2286971-auth-request-subscriber-185.diff50.88 KBznerol
#183 interdiff.txt9.46 KBznerol
#183 2286971-auth-request-subscriber-182.diff49.51 KBznerol
#182 interdiff.txt4.05 KBznerol
#182 2286971-auth-request-subscriber-182.diff49.51 KBznerol
#181 interdiff.txt23.77 KBznerol
#181 2286971-auth-request-subscriber-181.diff50.51 KBznerol
#179 interdiff.txt5.84 KBznerol
#179 2286971-auth-request-subscriber-179.diff43.09 KBznerol
#178 interdiff.txt13.32 KBznerol
#178 2286971-auth-request-subscriber-177.diff43.36 KBznerol
#174 2286971-auth-request-subscriber-174.diff42.8 KBznerol
#174 interdiff.txt6.13 KBznerol
#171 interdiff.txt13.16 KBznerol
#171 2286971-auth-request-subscriber-171.diff42.73 KBznerol
#164 interdiff.txt4.94 KBznerol
#164 2286971-auth-request-subscriber-164.diff43.56 KBznerol
#163 interdiff.txt908 bytesznerol
#163 2286971-auth-request-subscriber-162.diff41.89 KBznerol
#157 interdiff.txt2.16 KBznerol
#157 2286971-auth-request-subscriber-157.diff41.89 KBznerol
#157 2286971-auth-request-subscriber-156.diff42.35 KBznerol
#157 interdiff.txt2.16 KBznerol
#155 interdiff.txt1.72 KBznerol
#155 2286971-auth-request-subscriber-155.diff40.19 KBznerol
#151 interdiff.txt2.66 KBznerol
#151 2286971-auth-request-subscriber-151.diff40.68 KBznerol
#145 Content Content Site-Install.png31.24 KBBerdir
#130 interdiff.txt1.58 KBznerol
#130 2286971-auth-request-subscriber-130.diff38.45 KBznerol
#123 interdiff.txt6.07 KBznerol
#123 2286971-auth-request-subscriber-123.diff38.06 KBznerol
#122 interdiff.txt2 KBznerol
#122 2286971-auth-request-subscriber-122.diff34.27 KBznerol
#121 interdiff.txt2.34 KBznerol
#121 2286971-auth-request-subscriber-121.diff32.57 KBznerol
#120 interdiff.txt3.29 KBznerol
#120 2286971-auth-request-subscriber-120.diff30.53 KBznerol
#119 interdiff.txt4.09 KBznerol
#119 2286971-auth-request-subscriber-119.diff28.67 KBznerol
#115 interdiff.txt820 bytesznerol
#115 2286971-auth-request-subscriber-115.diff29.58 KBznerol
#108 interdiff.txt4.64 KBznerol
#108 2286971-auth-request-subscriber-108.diff28.78 KBznerol
#105 interdiff.txt1.25 KBznerol
#105 2286971-auth-request-subscriber-105.diff26.47 KBznerol
#99 interdiff.txt820 bytesznerol
#99 2286971-auth-request-subscriber-99.diff25.22 KBznerol
#96 2286971-auth-request-subscriber.diff24.42 KBznerol
#84 interdiff-73-84.txt3.49 KBalmaudoh
#84 2286971-fix-current-user-proxy-84.patch31.24 KBalmaudoh
#80 interdiff.txt691 bytesalmaudoh
#80 2286971-fix-current-user-proxy-80.diff32.71 KBalmaudoh
#73 interdiff.txt11.53 KBalmaudoh
#73 2286971-fix-current-user-proxy-73.diff32.71 KBalmaudoh
#70 interdiff.txt797 bytesalmaudoh
#70 2286971-fix-current-user-proxy-70.diff21.33 KBalmaudoh
#68 2286971-fix-current-user-proxy-68.diff21.21 KBalmaudoh
#63 interdiff.txt2.77 KBznerol
#63 2286971-fix-current-user-proxy-63.diff21.25 KBznerol
#62 interdiff.txt4.75 KBznerol
#62 2286971-fix-current-user-proxy-62.diff22.77 KBznerol
#61 2286971-fix-current-user-proxy-61.diff20.66 KBznerol
#44 remove_dependency_of-2286971-44.patch20.66 KBcilefen
#41 2286971-fix-current-user-proxy-40.diff20.62 KBznerol
#32 interdiff.txt3.63 KBznerol
#32 2286971-fix-current-user-proxy-32.diff20.69 KBznerol
#30 interdiff.txt4.34 KBznerol
#30 2286971-fix-current-user-proxy-30.diff18.9 KBznerol
#20 interdiff.txt1.2 KBznerol
#20 2286971-fix-current-user-proxy-20.diff16.08 KBznerol
#18 2286971-fix-current-user-proxy-17.diff15.85 KBznerol
#14 2286971-fix-current-user-proxy-14.patch15.73 KBznerol
#14 interdiff.txt386 bytesznerol
#12 interdiff.txt5.28 KBznerol
#12 2286971-fix-current-user-proxy-12.patch15.98 KBznerol
#8 interdiff-2286971-8.txt3.57 KBdamiankloip
#2 interdiff.txt1.14 KBznerol
#2 2286971-fix-current-user-proxy-2.patch14.12 KBznerol
#1 2286971-fix-current-user-proxy.patch14.09 KBznerol
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

znerol’s picture

Status: Active » Needs review
FileSize
14.09 KB

PoC. Note that this patch declares current_user service as persist. This is necessary because there is no reauthentication anymore.

znerol’s picture

+++ b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php
@@ -88,8 +88,16 @@ public function authenticate(Request $request) {
+    $route = isset($defaults[RouteObjectInterface::ROUTE_OBJECT]) ? $defaults[RouteObjectInterface::ROUTE_OBJECT] : NULL;
+    if ($route && ($route_name = $route->getOption('_auth'))) {
+      $allowed_providers = array($route_name => TRUE);
+      $auth_providers = array_intersect_key($auth_providers, $allowed_providers);
+    }

This is clearly a copy-paste fail.

znerol’s picture

The last submitted patch, 1: 2286971-fix-current-user-proxy.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 2: 2286971-fix-current-user-proxy-2.patch, failed testing.

znerol’s picture

znerol’s picture

Test fails in MenuRouterTest are a bit tricky. Currently MaintenanceModeSubscriber runs before routing and always triggers a user authentication. Moving it after routing and the authentication subscriber results in a whole lot more (contrib-)code being executed in maintenance mode. It would also be possible to trigger authentication from within MaintenanceModeSubscriber if maint-mode is enabled, but that would result in inconsistent behavior when it is on vs. when it is off.

damiankloip’s picture

FileSize
3.57 KB

Could we do something like this? Not tested.

EDIT: Meant to actually remove that onKernelRequestDetermineSiteStatus method.

shivanshuag’s picture

znerol’s picture

Filed #2288665: Remove _maintenance request attribute and replace it with a maintenance mode service, should help with disentangling the maintenance mode subscriber.

znerol’s picture

Filed #2288911: Use route name instead of system path in user maintenance mode subscriber on the maintenance mode subscriber in the user module.

znerol’s picture

Status: Needs work » Needs review
FileSize
15.98 KB
5.28 KB

Reroll. Due to the fact that access checking has been moved inside the AccessAwareRouter, authentication also needs to be performed there.

Status: Needs review » Needs work

The last submitted patch, 12: 2286971-fix-current-user-proxy-12.patch, failed testing.

znerol’s picture

Status: Needs work » Needs review
FileSize
386 bytes
15.73 KB

Status: Needs review » Needs work

The last submitted patch, 14: 2286971-fix-current-user-proxy-14.patch, failed testing.

catch’s picture

Priority: Normal » Critical

This really looks critical to me, bumping priority.

znerol’s picture

Status: Needs work » Needs review
znerol’s picture

This time with the rerolled patch.

Status: Needs review » Needs work

The last submitted patch, 18: 2286971-fix-current-user-proxy-17.diff, failed testing.

znerol’s picture

Status: Needs review » Needs work

The last submitted patch, 20: 2286971-fix-current-user-proxy-20.diff, failed testing.

znerol’s picture

Note to self: Test-fails from #20

Drupal\basic_auth\Tests\Authentication\BasicAuthTest          40 passes   2 fails
Drupal\book\Tests\BookTest                                   862 passes  80 fails
Drupal\language\Tests\LanguageUILanguageNegotiationTest      169 passes   4 fails
Drupal\Tests\Core\Routing\AccessAwareRouterTest                0 passes   3 fails
Drupal\system\Tests\System\SiteMaintenanceTest                72 passes   1 fails
Drupal\user\Tests\UserAdminLanguageTest                      126 passes   3 fails

Detailed results are currently not available on qa.drupal.org due to #2376039: Undefined property ContainerAwareEventDispatcherTest::results in run-tests.sh.

znerol’s picture

I'm a bit stuck with BookTest which I believe is caused by a rather nasty architecture problem. First let me repeat the problems with the current on-demand authentication by the authentication proxy.

  • Authentication can fire before routing. If that happens, the authentication restriction (_auth option on the route object) is not respected. In order to circumvent that, there is AuthenticationEnhancer which simply reverts authentication. The enhancer hack is known to cause #2283637: Provide test coverage to prove that an AuthenticationProvider can initiate a session
  • The account proxy depends on the authentication service but that again depends implicitly on the account proxy (because of its responsibility for setting the global $user which is to be replaced with the authentication proxy).

This patch aims to resolve both problems by removing the on-demand authentication of the proxy but instead perform authentication immediately after routing (when the _auth option is available) and before access is checked.

The failures in BookTest are a result of parameter conversion happening before authentication. When a book node is loaded before authentication happened, then node access grants are evaluated for the anonymous user. In the test this results in the node being loaded without book links.

It looks like authentication needs to happen after the route was loaded (because of the _auth option) but before the ParamConversionEnhancer. Hence it looks like we need to keep AuthenticationEnhancer in order to authenticate from there.

catch’s picture

Just looked at book_node_load(), which got me to https://api.drupal.org/api/drupal/core%21modules%21book%21src%21BookMana...

Since we have entity caching in core now, book module should absolutely not be doing anything access-dependent in book_node_load() - so I think the test fail is actually exposing a bug in book module and we should change book module instead.

Probably the quickest place to move that logic would be hook_entity_prepare_view().

pwolanin’s picture

So, the book code should be using the node access system to optimize these checks and never be calling access on a single object, normally.

But I guess this is using the really old and crappy node links API. Can we defer the access check to render time? Maybe a question for Wim?

Not sure how to make sure we don't see this general problem again in the future, or it will just break so devs will fix it?

pwolanin’s picture

So, I think the approach here is wrong, or we need to re-think when upcasting happens.

In general, we don't want to go to the trouble of loading objects (upcasting) if authentication is going to fail.

pwolanin’s picture

The bug in this case seems to be that's it's using hook_node_load() to essentially attach render information.

Maybe should be using hook_entity_prepare_view() instead?

pwolanin’s picture

Oops - no - I take all that back.

Book module is basically trying to load a data field. We should not be checking access at all during that - if access is denied for the node, you won't see the attached field anyhow.

znerol’s picture

I likely confused everybody (including myself) yesterday in IRC when I said that authentication happens before upcasting with that patch. The contrary is true:

The sequence of events with #20 is as follows:

  1. Routing (includes upcasting)
  2. Authentication
  3. Access check
znerol’s picture

Status: Needs work » Needs review
FileSize
18.9 KB
4.34 KB

The MaintenanceModeTest somehow manages to trigger a 404 which resulted in an exception thrown by the router. We also want to authenticate a request when routing fails, therefore lets use try ... catch ... finally. Oh, there is no finally, bummer.

Status: Needs review » Needs work

The last submitted patch, 30: 2286971-fix-current-user-proxy-30.diff, failed testing.

znerol’s picture

Status: Needs work » Needs review
FileSize
20.69 KB
3.63 KB

The basic authentication provider gets activated under the following condition (BasicAuth::applies()):

  • The basic auth provider is enabled on a route.
  • And credentials are provided by the client.

The basic authentication provider is supposed to return a 401 Unauthorized response under the following condition BasicAuth::handleException():

  • The basic auth provider is enabled on a route.
  • And no credentials are provided by the client.

However, currently BasicAuth::handleException() tries to determine whether or not to emit a 401 by checking !$this->applies(). Frankly I do not really understand how this is working at all in HEAD.

Status: Needs review » Needs work

The last submitted patch, 32: 2286971-fix-current-user-proxy-32.diff, failed testing.

znerol’s picture

With this patch LanguageRequestSubscriber is now running way before authentication. I wonder whether AccountProxy::setAccount should emit an event to notify everybody. This also might help with #2054203: HttpBasic authentication tests failing when date.timezone is not set.

Do we have to expect any drawbacks when moving language negotiation after routing (and upcasting)?

dawehner’s picture

I tried to write a beta evaluation for this issue:

However, the conclusion that such a use case would require authentication to be performed separately in a subrequest is wrong in my opinion. I'd like to point out that user authentication should not be confused with masquerading. The latter can be performed independently of the former and does not require separate authentication.

User authentication means checking the credentials sent along with the initial HTTP request. This step should be performed after routing and before access-control checks are fired. Not before and not afterwards.

This itself sounds not like a justification for this being a critical? Is the current way of doing it (on the fly) so brittle at the moment?

catch’s picture

The patch here has exposed some very dodgy assumptions elsewhere like #2380615: Result of book_node_load() should not vary depending on user permissions. I think we need to evaluate all of the issues on #2371629: [meta] Finalize Session and User Authentication API to figure out which if any are release blocking etc. not sure if this one is or how much of the other work it blocks.

I do think #2286591: [meta] Security audit the Authentication component needs to be done before release - authentication providers are pretty broken at the moment and it's a very security sensitive API. Tthat can only happen once the session and auth APIs are reviewable, which doesn't feel like the case without some of this range of issues being fixed.

znerol’s picture

@dawehner, see #2286591: [meta] Security audit the Authentication component

The AccountProxy currently triggers authentication automatically whenever one of the methods of the current_user service is accessed. As a result it has a dependency on the AuthenticationManager which is responsible for checking credentials on an incoming request and based on that setting the current user. This circular dependency is hidden because the AuthenticationManager acts on the global $user.

Thus in order to remove the global $user from AuthenticationManager, it is necessary to first remove automatic authentication from AccountProxy.

alexpott’s picture

xjm’s picture

dawehner’s picture

Do we have to expect any drawbacks when moving language negotiation after routing (and upcasting)?

You can't do it after upcasting, as upcasting behaves different depending on the language.

znerol’s picture

Status: Needs work » Needs review
FileSize
20.62 KB

The attached patch is a reroll.

You can't do it after upcasting, as upcasting behaves different depending on the language.

Proper language detection only can be performed after the user was authenticated. If language detection needs to be performed before upcasting, this essentially means that authentication needs to be moved in between routing and upcasting. Since upcasting is done in a route enhancer, it follows that we have the following options to resolve this problem:

  1. Make upcasting language-agnostic.
  2. Move authentication into a route enhancer which runs with a higher priority than upcasting.
  3. Decouple upcasting from routing and execute it after authentication and language negotiation but before access checking (i.e. in AccessAwareRouter::matchRequest().

Status: Needs review » Needs work

The last submitted patch, 41: 2286971-fix-current-user-proxy-40.diff, failed testing.

effulgentsia’s picture

Issue tags: +authentication
cilefen’s picture

Status: Needs work » Needs review
FileSize
20.66 KB

reroll

Status: Needs review » Needs work

The last submitted patch, 44: remove_dependency_of-2286971-44.patch, failed testing.

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 44: remove_dependency_of-2286971-44.patch, failed testing.

almaudoh’s picture

Per #41

  1. Make upcasting language-agnostic.
  2. Move authentication into a route enhancer which runs with a higher priority than upcasting.
  3. Decouple upcasting from routing and execute it after authentication and language negotiation but before access checking (i.e. in AccessAwareRouter::matchRequest().

#41.2 looks like the the only option that can be accomodated in the current issue scope, so I will try out that option.

znerol’s picture

I think #41.1 should be explored also. This is comparable to the book bug (#2380615: Result of book_node_load() should not vary depending on user permissions). The primary reason for introducing language-dependent upcasting is convenience, and therefore it might be okay to roll back #2160965: Content entities are not upcast in the page language, inconsistent with config entities.

#41.2 looks like the the only option that can be accomodated in the current issue scope, so I will try out that option.

@almaudoh: I'm willing to continue the coding work here. It would be awesome if you'd assume the reviewer/manager role here, this likely increases the chances that the patch eventually gets to a commitable state.

almaudoh’s picture

Awesome @znerol :) You've already done great work in this area, and I'd be happy to review.

znerol’s picture

dawehner’s picture

It would be still great to outline in the issue summary the following points. Its hard to understand what this issue is exactly about, beside of the state of HEAD

a) What is problematic in triggering authentication multiple times. The only thing I can read in the summary is: "in my opinion its wrong"
b) How can you ever resolve the problem that you need the current user to do path inbound processing which comes before routing
c) What is the problem in running authentication again once you have more information due to basic auth?

znerol’s picture

This is a child issue of #2371629: [meta] Finalize Session and User Authentication API, please read/comment over there concerning the architectural problems we are trying to resolve here.

dawehner’s picture

@znerol
... this is for everyone trying to understand the issue, please be fair and give people an easy life when they want to participate in any kind of way in this issue.

almaudoh’s picture

almaudoh’s picture

Issue summary: View changes
znerol’s picture

@dawehner: #52.b: I cannot find any inbound path processors which depend on the users language.

almaudoh’s picture

@znerol, when I was stepping through the code, I found the PathProcessorLanguage ran before the RouterListener. Maybe you could check?

znerol’s picture

Status: Needs work » Postponed

I'm postponing this on #2420523: Upcasting should not depend on the current user (make upcasting language agnostic), we need a decision over there before we can continue here.

Gábor Hojtsy’s picture

Status: Postponed » Needs work
znerol’s picture

Reroll.

znerol’s picture

Add back the AuthenticationEnhancer, but instead use it to actually trigger authentication there.

znerol’s picture

Status: Needs work » Needs review
FileSize
21.25 KB
2.77 KB

Some cleanup: Reuse $account variable in authorize.php, revert unnecessary changes to AuthenticationProvider.

znerol’s picture

I hate the try..catch in the AccessAwareRouter, maybe this could be avoided by registering AuthenticationEnhancer as an event subscriber for the KernelEvents::EXCEPTION, analogous to how ParamConvertsionEnhancer works.

Status: Needs review » Needs work

The last submitted patch, 63: 2286971-fix-current-user-proxy-63.diff, failed testing.

The last submitted patch, 63: 2286971-fix-current-user-proxy-63.diff, failed testing.

almaudoh’s picture

Status: Needs work » Needs review
FileSize
21.21 KB

Straight re-roll

Status: Needs review » Needs work

The last submitted patch, 68: 2286971-fix-current-user-proxy-68.diff, failed testing.

almaudoh’s picture

Status: Needs work » Needs review
FileSize
21.33 KB
797 bytes

Authentication providers need some of the $request attributes to be injected early. Let's see how many fails we have now.

Status: Needs review » Needs work

The last submitted patch, 70: 2286971-fix-current-user-proxy-70.diff, failed testing.

almaudoh’s picture

Okay. Looks like the Cookie auth provider is not doing that much in this patch - it's definitely not authenticating and this patch expects it to. Drupal cookie auth is being handled in SessionHandler.

This shouldn't be the case and should be moved to \Drupal\Core\Authentication\Provider\Cookie. #2228393: Decouple session from cookie based user authentication may need to go in first before we continue on this issue.

almaudoh’s picture

Status: Needs work » Needs review
FileSize
32.71 KB
11.53 KB

Pulled in some ideas from #2228393: Decouple session from cookie based user authentication (nice work @znerol) to separate out cookie auth from the SessionHandler into the Cookie auth provider. Also applied some fixes from #2328645: Remove remaining global $user (btw, it would help to commit that issue before this one).

This makes the patch much larger, but it's good to test that it will work. Afterwards the Cookie auth part (the interdiff mostly) can be pulled out to #2228393: Decouple session from cookie based user authentication and finished there.

Let's see how many tests fail now.

Status: Needs review » Needs work

The last submitted patch, 73: 2286971-fix-current-user-proxy-73.diff, failed testing.

andypost’s picture

  1. +++ b/core/lib/Drupal/Core/Authentication/Provider/Cookie.php
    @@ -28,13 +49,49 @@ public function applies(Request $request) {
    +      $values = $this->connection->query("SELECT u.*, s.* FROM {users_field_data} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE u.default_langcode = 1 AND s.sid = :sid", array(
    

    suppose better to query sessions first, then join user data, also please left only required columns

  2. +++ b/core/lib/Drupal/Core/Authentication/Provider/Cookie.php
    @@ -28,13 +49,49 @@ public function applies(Request $request) {
    +      if ($values && $values['uid'] > 0 && $values['status'] == 1) {
    ...
    +        $rids = $this->connection->query("SELECT ur.roles_target_id as rid FROM {user__roles} ur WHERE ur.entity_id = :uid", array(
    ...
    +        /** @var \Drupal\user\UserStorageInterface $storage */
    +        $storage = \Drupal::entityManager()->getStorage('user');
    +        $storage->updateLastAccessTimestamp($user, REQUEST_TIME);
    

    Not good for Core to know storage related details of user module.

    probably better to use entity query service (properly injected)

    Also entity manager should be injected

  3. +++ b/core/lib/Drupal/Core/Session/SessionHandler.php
    @@ -64,57 +74,30 @@ public function open($save_path, $name) {
    +    $record = $this->connection->select('sessions', 's')
    ...
    -    $values = $this->connection->query("SELECT u.*, s.* FROM {users_field_data} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE u.default_langcode = 1 AND s.sid = :sid", array(
    

    now this will execute a module handler for alter query hook

eojthebrave’s picture

It looks like these tests are failing because when you login with \Drupal\simpletest\WebTestBase::drupalLogin that it's not sticking. The login works, but then when the browser requests node/add/book, the session isn't found so you can't access the page, can't fill out the form, can't create a new node, and the tests start failing.

Also curious. After you run tests, with this patch applied, you're logged out of your session.

Not quite sure what is broken yet, but posting these notes in case it helps someone else see what is broken.

eojthebrave’s picture

Couple more things.

+++ b/core/authorize.php
@@ -50,8 +50,10 @@
+function authorize_access_allowed(Request $request) {

This patch adds a parameter to the authorize_access_allowed() function, but doesn't add the corresponding @docblock

+++ b/core/lib/Drupal/Core/Routing/Enhancer/AuthenticationEnhancer.php
@@ -7,72 +7,73 @@
+ * Authenticates the incomming request.

Typo, "incomming" should be "incoming"

+++ b/core/lib/Drupal/Core/Routing/Enhancer/AuthenticationEnhancer.php
@@ -7,72 +7,73 @@
+    // incomming master request.

"incomming" should be "incoming"

almaudoh’s picture

Thanks for the reviews @andypost and @eojthebrave. Will tackle them after fixing the test fails.


#77: Found the same thing too - still trying to figure out why the session gets lost. If you do:
$user = $this->drupalCreateUser(['appropriate', 'permissions']);
$this->drupalLogin($user);
$this->drupalGet('node/add/book'); // This one will work - 200 OK
$this->drupalGet('node/add/book'); // This one will fail - 403 Forbidden

The first one will work, the second one will fail with access denied. That's why ::drupalPostForm('node/add/book', ...) fails because the post is done on the second call.

Interestingly, this doesn't happen for all tests. I wrote a spanking new custom module with a test and copied BookTest::createBookNode() into it. And that module's version of ::createBookNode() worked, BookTest's version failed. Crazy...

Still working through the code...

almaudoh’s picture

Status: Needs work » Needs review
FileSize
32.71 KB
691 bytes

So I think I found the offending line of code:

+++ b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
@@ -86,12 +97,22 @@ public function getContext() {
+    catch (\Exception $e) {
+      try {
+        $this->accountProxy->setAccount($this->authenticationProvider->authenticate($request));
+      }
+      catch (\Exception $e) {
+        // Ignore exception thrown while trying to authenticate inside the
+        // enclosing catch clause.
+      }
+
+      throw $e;

Attempting to catch and re-authenticate after an exception in the AccessAwareRouter::matchRequest() fails, leading to the current user (as read from the session) being effectively logged out again. This is the cause of most of the test failures - sending to testbot to confirm.

The exceptions being thrown are of the nature:
None of the routers in the chain matched this request GET /node HTTP/1.1 Accept: text/html Accept-Charset: ISO-8859-1,...

I'm now drilling down to understand why authentication fails after these exceptions are thrown in the AccessAwareRouter::matchRequest() (and perhaps also why the route matching fails in the first place)

Is there any particular reason why we need to re-authenticate after the matcher fails to match? If there is a session already and the user is authenticated from the session, need we bother to re-authenticate after the route match fails?

cpj’s picture

I think the nested try/catch was added in #62, so perhaps the reason is explained in the comments and changes around #60 - #70 ?

Status: Needs review » Needs work

The last submitted patch, 80: 2286971-fix-current-user-proxy-80.diff, failed testing.

almaudoh’s picture

Drilled a bit more into the exceptions thrown by AccessAwareRouter and why authentication fails afterwards. Found that the PathBasedBreadcrumbBuilder is using the AccessAwareRouter to find matching routes as it is building the breadcrumb off of the current path. Unfortunately, the $request object passed in is incomplete (no route parameters) resulting in the exceptions as well as the auth failures.

Another undesirable side effect of this is that authentication is re-run multiple times as the breadcrumb links are being built.
A way to fix this would be to changing the PathBasedBreadcrumbBuilder to use a route matcher (e.g. NestedMatcher) instead of a router to build its links.

almaudoh’s picture

Removed authentication from AccessAwareRouter. Now auth is only on the AuthenticationEnhancer.

almaudoh’s picture

Status: Needs work » Needs review

Meh

Status: Needs review » Needs work

The last submitted patch, 84: 2286971-fix-current-user-proxy-84.patch, failed testing.

andypost’s picture

Found that the PathBasedBreadcrumbBuilder is using the AccessAwareRouter to find matching routes as it is building the breadcrumb off of the current path. Unfortunately, the $request object passed in is incomplete (no route parameters) resulting in the exceptions as well as the auth failures.

I think this needs own issue to solve.

#84 shows same failures are not caused by that

znerol’s picture

I do not think #73 was a good idea. Can we please go back to #70 and explore the idea in #64, i.e. get rid of the ugly try catch and instead implement an exception subscriber.

almaudoh’s picture

The really clean way to do authentication would have been to do it in a kernel event subscriber that runs before routing. We would avoid all of this route enhancer messiness. AFAICT the only thing making this hard is the _auth routing option. Maybe we can figure out a different approach to specifying the authentication provider that doesn't rely on the routing system.

I think this needs own issue to solve.

Raised #2428609: Refactor PathBasedBreadcrumbBuilder to not use AccessAwareRouter for building links

I do not think #73 was a good idea

Can u explain why? I was just trying out what could be possible causes of the test fails. We can move that change back to #2228393: Decouple session from cookie based user authentication and finish it there, I have no problem with that. But we need that change for authentication to work as it should.

znerol’s picture

Because #44 only had 7 fails. We need to find the minimal amount of changes necessary to get from there to something like #63.

dawehner’s picture

Just in general I'm wondering whether we can decouple the routing system from the authentication system by using a path
level override.

znerol’s picture

@dawehner This will probably not work with the current basic auth implementation when using the rest module, because that still insists on doing content negotiation on the same paths that browsers use. If you access node/1 on a rest enabled site, it is impossible to tell whether basic auth or cookie should be used to authenticate. If we want to move authentication before routing, then we need to implement it in a way that different auth providers can coexist throughout the whole site and will not interfere with each other.

Yet I think there is a technical solution which allows us to use the same list of authentication providers on the whole site (cookie and basic auth) if we slightly change the way basic auth is implemented. The reason for restricting basic auth to rest-routes is because it is necessary to send a 401 Unauthorized response if the request lacks credentials and the route is restricted to authenticated users. However, on pages accessed by web users we want to send 403 Access Denied. I think that we may resolve this dilemma by simply restricting the basic auth exception listener to rest-routes (the thing which issues 401).

As a consequence it would be possible to use basic auth on all routes on the site. Only if the request lacks credentials, the behavior is different for REST routes than it is for other ones.

Assuming the following authentication providers ordered according to their priority:

  1. cookie
  2. basic auth

We get the following result:

8.0.x

restricted non-rest route, correct credentials

no cookie, no ba credentials
no authentication provider applies, 403
has cookie, no ba credentials
cookie authentication provider applies, authentication succeeds, 200
no cookie, has ba credentials
no authentication provider applies, 403
has cookie, has ba credentials
cookie authentication provider applies, authentication succeeds, 200

restricted rest route, correct credentials

no cookie, no ba credentials
no authentication provider applies, ba exception listener applies, 401
has cookie, no ba credentials
cookie authentication provider applies, authentication succeeds, 200
no cookie, has ba credentials
basic authentication provider applies, authentication succeeds, 200
has cookie, has ba credentials
cookie authentication provider applies, authentication succeeds, 200

proposal

restricted non-rest route, correct credentials

no cookie, no ba credentials
no authentication provider applies, no special exception listener applies, 403
has cookie, no ba credentials
cookie authentication provider applies, authentication succeeds, 200
no cookie, has ba credentials
basic authentication provider applies, authentication succeeds, 200
has cookie, has ba credentials
cookie authentication provider applies, authentication succeeds, 200

restricted rest route, correct credentials

no cookie, no ba credentials
no authentication provider applies, ba exception listener applies, 401
has cookie, no ba credentials
cookie authentication provider applies, authentication succeeds, 200
no cookie, has ba credentials
basic authentication provider applies, authentication succeeds, 200
has cookie, has ba credentials
cookie authentication provider applies, authentication succeeds, 200
andypost’s picture

looks as solid idea, +1

almaudoh’s picture

+1 to this proposal. Some questions @znerol:

  1. has cookie, has ba credentials
    cookie authentication provider applies, authentication succeeds, 200

    Will basic auth trump over cookie auth in this case if the order of priority was 1) Basic Auth, 2) Cookie Auth?

  2. How would we distinguish a rest route from a non-rest route?
znerol’s picture

Will basic auth trump over cookie auth in this case if the order of priority was 1) Basic Auth, 2) Cookie Auth?

Yes.

How would we distinguish a rest route from a non-rest route?

Same as in HEAD, however we only need the information in the exception subscriber and not already upon authentication. Access denied exceptions are thrown by when checking access which is after routing.

znerol’s picture

Ok, let's try. This does not instantly blow up on my machine, see how far the bot comes.

No interdiff, this is a new approach.

znerol’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 96: 2286971-auth-request-subscriber.diff, failed testing.

znerol’s picture

$this->account->id(); in AccessAwareRouter.php is now a noop, remove that.

Should not have any effect on test results though.

znerol’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 99: 2286971-auth-request-subscriber-99.diff, failed testing.

almaudoh’s picture

This is a much more elegant approach than what we were doing before. Thanks.

  1. -   * Filters a list of providers and only return those allowed on the request.
    -   *
    -   * @param \Drupal\Core\Authentication\AuthenticationProviderInterface[] $providers
    -   *   An array of authentication provider objects.
    -   * @param Request $request
    -   *   The request object.
    -   *
    -   * @return \Drupal\Core\Authentication\AuthenticationProviderInterface[]
    -   *   The filtered array authentication provider objects.
    -   */
    -  protected function filterProviders(array $providers, Request $request) {
    -    $route = RouteMatch::createFromRequest($request)->getRouteObject();
    -    $allowed_providers = array();
    -    if ($route && $route->hasOption('_auth')) {
    -      $allowed_providers = $route->getOption('_auth');
    -    }
    -    elseif ($default_provider = $this->defaultProviderId()) {
    -      // @todo Mirrors the defective behavior of AuthenticationEnhancer and
    -      // restricts the list of allowed providers to the default provider if no
    -      // _auth was specified on the current route.
    -      //
    -      // This restriction will be removed by https://www.drupal.org/node/2286971
    -      // See also https://www.drupal.org/node/2283637
    -      $allowed_providers = array($default_provider);
    -    }
    -
    -    return array_intersect_key($providers, array_flip($allowed_providers));
    -  }
    -
    -  /**
    

    +1

  2.    public function applies(Request $request) {
    -    return $request->hasSession();
    +    return $request->hasSession() && $this->sessionConfiguration->hasSession($request);
       }
    

    These two look like they are doing the same thing, it's only after checking SessionConfiguration::hasSession() docs that we see that sessionConfiguration is actually checking for a session cookie. Not related, but it would be clearer if SessionConfiguration::hasSession() was called ::hasSessionCookie()

  3. +  /**
    +   * Authenticates user on request.
    +   *
    +   * @see \Drupal\Core\Authentication\AuthenticationProviderInterface::authenticate()
    +   */
    +  public function onKernelRequestAuthenticate(GetResponseEvent $event) {
    +    if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) {
    +      $request = $event->getRequest();
    +      $account = $this->authenticationProvider->authenticate($request);
    +      $this->accountProxy->setAccount($account);
    +    }
       }
    

    <3 Now that's what I'm talking 'bout.

  4. --- a/core/lib/Drupal/Core/Routing/Enhancer/AuthenticationEnhancer.php
    +++ /dev/null
    @@ -1,86 +0,0 @@
    -<?php
    -
    

    +1. Bye-bye authentication enhancer.

  5.    public function handleException(GetResponseForExceptionEvent $event) {
         $exception = $event->getException();
    -    if ($GLOBALS['user']->isAnonymous() && $exception instanceof AccessDeniedHttpException) {
    -      if (!$this->applies($event->getRequest())) {
    +    $request = $event->getRequest();
    +    if ($GLOBALS['user']->isAnonymous() && $exception instanceof AccessDeniedHttpException && !$this->hasCredentials($request)) {
    +      $route = RouteMatch::createFromRequest($request)->getRouteObject();
    

    We shouldn't be using global $user anymore after this issue considering #2328645: Remove remaining global $user will remove the last remaining uses that are not in this issue.

  6. @@ -50,11 +50,6 @@ public function testBasicAuth() {
         $this->drupalGet('admin');
         $this->assertResponse('403', 'No authentication prompt for routes not explicitly defining authentication providers.');
     
    -    $account = $this->drupalCreateUser(array('access administration pages'));
    -
    -    $this->basicAuthGet(Url::fromRoute('system.admin'), $account->getUsername(), $account->pass_raw);
    -    $this->assertNoLink('Log out', 0, 'User is not logged in');
    -    $this->assertResponse('403', 'No basic authentication for routes not explicitly defining authentication providers.');
    

    Looks like BasicAuth tests need to be updated to reflect the expectations in #92. Maybe in a follow up.

Is there any use case where we would want to disable Cookie Authentication completely?

cpj’s picture

I agree with @almaudoh, apart from the need to remove the call to $GLOBALS[user], this looks great, so +1 from me too. I will test it as soon as it gets to Needs Review again.

Is there any use case where we would want to disable Cookie Authentication completely?

Don't know if it's relevant here but I don't use it when we authenticate our REST calls.

znerol’s picture

FYI, I opened a security issue about this a couple of days ago in order to get assurance that we can discuss any level of detail of the security implications of the current design. It was decided that this should be public.

Drupal 8 Authentication is unpredictable

Drupal 8 has a new pluggable authentication system. Contrib and custom modules may register their own authentication providers. Route definitions may restrict the set of allowed providers via their _auth option. Core currently provides two authentication methods: session cookie and basic_auth, the latter only being used on REST routes by default.

It is likely that contrib modules will leverage the new API and will bring in many more providers, e.g. based on URL tokens or TLS client certificates. Since #2180109: Change the current_user service to a proxy authentication does not happen anymore at one defined point in time within a request, but it can happen anywhere. Authentication even may be triggered way before routing, and thus before it is known which authentication providers are allowed on a given request. Core currently works around this problem by simply clearing the authenticated user from within the AuthenticationEnhancer if necessary. This leads to the authentication being triggered again next time when any method on the current user proxy is called.

Imagine a situation where authentication of the same request leads to different results when using two different authentication providers. For example, a developer logged into the site (session cookie) is testing an API which uses URL tokens for authentication. She is troubleshooting a problem with the API and tries to use a token from an unprivileged user. In this situation Drupal will authenticate using the developers session cookies if authentication is triggered before routing. I.e., early request subscribers are potentially executed with the privileges of the develeoper.

As a consequence, we cannot possibly trust the result of authentication when it was run before routing.

I think it would be best to discuss this issue in public. I would like to use this text or parts of it in the issue summary of #2286971: Remove dependency of current_user on request and authentication manager. Since I'm not 100% on how this affects security of existing Drupal 8 sites, I'm first posting this here.

Please add @dawehner and @Berdir

Re #102: Maybe when some site wishes to use TLS client certificates exclusively.

znerol’s picture

The test failures in UpdateScriptTest are due to the fact that drupal_flush_all_caches triggers a container rebuild. After that the current_user service will point to an anonymous user. This then leads to the batch failing to redirect to update.php/results because anonymous does not have access to it

Extract from function _batch_finished() in core/includes/batch.inc around line number 456:

        else {
          $redirect = \Drupal::pathValidator()->getUrlIfValid($options['path']);
          if (!$redirect) {
            // Stay on the same page if the redirect was invalid.
            $redirect = Url::fromRoute('<current>');
          }
          $redirect->setOptions($options);
        }

The problem is that $redirect is set to FALSE because the anonymous user has no access to the update script. As a result the batch triggers a redirect to the current path (update.php/finished but without the (batch) id parameter. On the next page load, DbUpdateController::handle() is called again, the request is passed to _batch_page which returns FALSE because there is no id parameter on the request. That leads to the $output variable set to a boolean instead of an array and that blows up when passed to BareHtmlPageRenderer::renderBarePage().

Long story short, we need to restore current_user after a container rebuild.

znerol’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 105: 2286971-auth-request-subscriber-105.diff, failed testing.

znerol’s picture

Long story short, we need to restore current_user after a container rebuild.

Regrettably that is not as easy as it seems. This is mainly because the entity.manager service needs some additional setup/cache-clearing before it can be used after modules are updated. Currently HEAD relies on the lazyness of AccountProxy, therefore the best way of resolving this issue is to retain some of it.

This patch adds a setInitialAccountId() method to the AccountProxy which is called from within DrupalKernel::initializeContainer if appropriate.

znerol’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 108: 2286971-auth-request-subscriber-108.diff, failed testing.

znerol’s picture

Fun, apparently there is existing code which serializes the AccountProxy. Now that it is container aware, this obviously blows up. So we need to either remove the container awareness of AccountProxy and instead use a static call to Drupal::entityManager() or figure out is trying to serialize the proxy and whether this is justifiable (i bet not).

The failures in BlockLanguageTest are interesting too: #2430133: BlockLanguageTest tests non-existing pages. However that indicates that maybe there is a problem with subrequests in this patch because exceptions are rendered in a subrequest.

almaudoh’s picture

Long story short, we need to restore current_user after a container rebuild.

Regrettably that is not as easy as it seems. This is mainly because the entity.manager service needs some additional setup/cache-clearing before it can be used after modules are updated.

We could use the UserSession to restore the user account instead of trying to load the User entity and also creating an additional dependency on entity manager.

FWIW I'm not comfortable with making current_user container aware in #108.

almaudoh’s picture

Actually, I believe the preferred approach to setting the current user should be to use either new AnonymousUserSession() or new UserSession($user_id), so we don't have to load the huge User entity if we don't need to.

znerol’s picture

Actually, I believe the preferred approach to setting the current user should be to use either new AnonymousUserSession() or new UserSession($user_id)

This is at odds with the efforts in #2345611: Load user entity in Cookie AuthenticationProvider instead of using manual queries. According to @Berdir this should actually be faster because entity storage cache..

znerol’s picture

Status: Needs work » Needs review
FileSize
29.58 KB
820 bytes

The test failures in BlockLanguageTest actually expose two bugs: one in the test case and one in a weird interaction between the session system and LanguageNegotiationSession (#2430379: Add explicit test for session based language negotiation) which is fixed by this patch (and that's why the buggy BlockLanguageTest now fails).

Therefore let's fix BlockLanguageTest here. AccountProxy serialization issue is still present.

Status: Needs review » Needs work

The last submitted patch, 115: 2286971-auth-request-subscriber-115.diff, failed testing.

almaudoh’s picture

RE #111:

we need to either remove the container awareness of AccountProxy and instead use a static call to Drupal::entityManager() or figure out is trying to serialize the proxy and whether this is justifiable (i bet not).

Can we do both?

znerol’s picture

znerol’s picture

Status: Needs work » Needs review
FileSize
28.67 KB
4.09 KB

Rollback container awareness of account proxy.

znerol’s picture

Addresses #102.5, properly inject current user into provider for basic authentication.

znerol’s picture

Kill the _authentication_provider request attribute.

znerol’s picture

Remove AuthenticationManagerInterface and defaultProviderId(), it was only ever used by AuthenticationEnhancer.

znerol’s picture

Remove cleanup(), this was basically a noop since #2229145: Register symfony session components in the DIC and inject the session service into the request object. That also makes it possible to get rid of triggeredProvider.

The last submitted patch, 122: 2286971-auth-request-subscriber-122.diff, failed testing.

Status: Needs review » Needs work

The last submitted patch, 123: 2286971-auth-request-subscriber-123.diff, failed testing.

The last submitted patch, 122: 2286971-auth-request-subscriber-122.diff, failed testing.

The last submitted patch, 123: 2286971-auth-request-subscriber-123.diff, failed testing.

znerol’s picture

Status: Needs work » Needs review
FileSize
38.45 KB
1.58 KB

#122 had a spurious invocation of defaultProviderId() removed by #123 but that has one to cleanup().

After removing the invocation of cleanup() from AuthenticationSubscriber this should actually turn green again.

almaudoh’s picture

This is looking much better now. Just a few nits:

  1. +      // Save the id of the currently logged in user.
    +      if ($this->container->initialized('current_user')) {
    +        $current_user_id = $this->container->get('current_user')->id();
    +      }
    +
    

    Maybe we can expand on the comment to explain why we are saving the currently logged in user.

  2. +
    +    if (!empty($current_user_id)) {
    +      $this->container->get('current_user')->setInitialAccountId($current_user_id);
    +    }
    +
    .
    .
    .
    +  /**
    +   * Sets the id of the initial account.
    +   *
    +   * Never use this method, its sole purpose is to work around weird effects
    +   * during mid-request container rebuilds.
    +   *
    +   * @param int $account_id
    +   *   The id of the initial account.
    +   */
    +  public function setInitialAccountId($account_id);
    

    This is the only not-so-nice piece of this otherwise great patch. In a follow-up, we may want to look at how this can be improved.

  3. index 80f7796..4d622a7 100644
    --- a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
    +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
    @@ -8,8 +8,10 @@
    +  /**
        * Keep authentication manager as private variable.
        *
    -   * @param AuthenticationProviderInterface $authentication_manager
    +   * @param \Drupal\Core\Authentication\AuthenticationProviderInterface $authentication_manager
        *   The authentication manager.
    -  public function __construct(AuthenticationProviderInterface $authentication_provider) {
    +  public function __construct(AuthenticationProviderInterface $authentication_provider, AccountProxyInterface $account_proxy) {
    

    s/manager/provider/. Match docs to function signature. Also the constructor first line doc needs to be updated.

almaudoh’s picture

Updated IS and added related issue #2228393: Decouple session from cookie based user authentication because cookie auth is still being done in SessionHandler

znerol’s picture

Also the meaning/behavior of the _auth route parameter changed, and I think we should probably replace it with something sensible. It is only used in order to restrict the basic auth exception subscriber to rest routes.

$ git grep "Option.*_auth"
core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php:    return $route->hasOption('_auth') && in_array('basic_auth', $route->getOption('_auth'));
core/modules/rest/src/Routing/ResourceRoutes.php:          $route->setOption('_auth', $enabled_methods[$method]['supported_auth']);

Maybe we could replace it with something specific to basic auth or more generic HTTP authentication as defined by RFC 2617 (e.g. a pair of route parameters like _http_auth_challenge: 'TRUE', _http_auth_scheme: 'basic'). In my opinion it would be legit to get rid of AuthenticationProviderInterface::handleException completely and instead implement the basic auth case as standard exception event subscriber.

almaudoh’s picture

Issue summary: View changes
znerol’s picture

Regarding #2228393: Decouple session from cookie based user authentication, I think the best approach would be if AuthenticationProviderInterface::authenticate() simply would return a user-id (or NULL), that also might help with #2345611: Load user entity in Cookie AuthenticationProvider instead of using manual queries because the entity might be loaded in the AuthenticationSubscriber or the AuthenticationManager. Also the check for a blocked user neither should be performed by the session handler nor by the authentication provider (see basic auth).

znerol’s picture

#135 can easily be a follow-up. For #133 we need feedback from rest people, because their supported_auth parameter doesn't make sense either with the new approach.

Berdir’s picture

Hm, I'm not sure it can be this easy.

This makes every authentication method available on every part of Drupal. Take something like https://www.drupal.org/project/services_token_access, you don't want that to allow you to access every page?

I also think it is wrong to hardcode basic auth to rest routes, and you're not actually doing that yet, you're still relying on the _auth option. custom services/API's will want to use basic auth as well, and want to get a login prompt, somehow?

I'm not sure why the following is not an option, I've not read every comment/patch here:

1. Do the authentication like done here, the first one that matches wins, don't worry about routes.
2. When matching the route, compare the method that was used with the requirements. Possibly improve improve the fallback to a configurable list of default methods instead of that hardcoded last-in-list-wins assumption. (This should be able to deal with the "but I want to allow client cert and cookie, or only client cert" requirement)
3. If it doesn't match, access denied. No re-trying or anything, just bail out.

So then you try to use basic auth to access /user. With HEAD, you get a normal page, as anonymous, now you get a 403, so what? IMHO, that's even better. What worse can happen, how is that a problem?

znerol’s picture

Trying to rephrase @Berdirs idea: Introduce an access-checker which we can use to restrict some authentication methods to some routes. Would that be sufficient?

In order to do something like this we probably should store the authentication method in the current user (i.e. introduce AccountInterface::getAuthenticationMethod().

znerol’s picture

custom services/API's will want to use basic auth as well, and want to get a login prompt, somehow?

To be clear: that's supported with the current approach (using _auth parameter, or something better like proposed in #133).

Berdir’s picture

I don't know if access checker or built-in, but as written, we whould have to keep the current behavior more or less that by default, only the default method(s) are allowed?

A method on that interface might work, but we also need a way to set it, we can say that we only have that on actual implementations, but user entity is tricky, there we need it on the interface, which would actually be a bit weird?)

cpj’s picture

For our REST interfaces we use our own custom provider, which passes an OAuth2 token to an external authentication server. For "normal" access we use Cookie Authentication.

I am not sure what is being suggested here, but it would make sense to me that I can configure a route to use whatever provider I want.

Berdir’s picture

The latest patch removes that option, and I agree with you that is not an option, including the part that not specifying anything on a route needs to use a (limited, somehow configurable) list, not all.

HEAD is doing some strange things right now and tries to re-authenticate if you e.g. use basic auth for a route that does not allow it, so it switches to the cookie authentication, which then provides the default anon user. My suggestion was to simply drop that part and in that case, return a 403 and be done with it.

znerol’s picture

My suggestion was to simply drop that part and in that case, return a 403 and be done with it.

I think that makes sense, and I think that functionality belongs into the domain of access checking. I'm not yet sure whether it should be implemented as a *real* access checker automatically added to each route or just be plugged directly into the access manager.

Regarding the getAuthenticationMethod() on the user entity, that could be implemented to just return NULL (or FALSE or whatever we want to return for unauthenticated). Only the UserSession and AccountProxy actually need to return something useful. That said, it is also weird to have all those *session* methods on the user entity.

Berdir’s picture

I think that makes sense, and I think that functionality belongs into the domain of access checking. I'm not yet sure whether it should be implemented as a *real* access checker automatically added to each route or just be plugged directly into the access manager.

I'm not sure, access checking is also applied to all displayed links, and that's a pretty pointless thing to run those?

Regarding the getAuthenticationMethod() on the user entity, that could be implemented to just return NULL (or FALSE or whatever we want to return for unauthenticated). Only the UserSession and AccountProxy actually need to return something useful. That said, it is also weird to have all those *session* methods on the user entity.

Uhm, no? basic auth already returns loaded entities, and so would #2345611: Load user entity in Cookie AuthenticationProvider instead of using manual queries? And those need to return something too?

That's also not my point, I've no issue with adding getAuthenticationMethod(). I'm asking how basic_auth would tell the loaded $user entity that it was loaded through basic_auth. For that, we need "setAuthenticationMethod()" on UserInterface, and that's the weird part :)

Berdir’s picture

- Removed, wrong issue -

znerol’s picture

I'm not sure, access checking is also applied to all displayed links, and that's a pretty pointless thing to run those?

Not if you declare that the check in question needs the request, then it is only applied to incoming requests.

basic auth already returns loaded entities, and so would #2345611: Load user entity in SessionHandler instead of using manual queries? And those need to return something too?

Not really because you only ever access them through the request proxy (at least as soon as #2328645: Remove remaining global $user is in). I think it would be acceptable if the authentication method is passed to the account proxy upon setAccount and stored there.

Berdir’s picture

Right, we could possibly do AccountProxyInterface::getAccountMethod(), but that's not what you wrote :)

Ignore comment #145, wrong issue o.0

almaudoh’s picture

Maybe I'm not following the discussions well here, but there is still the first question: how do you associate _auth method with specific routes? And how is AuthenticationSubscriber aware of this since it is running before routing? Will AuthenticationManager build and maintain it's own mapping of route -> _auth when routes are being built?

Also, I don't think it makes sense to authenticate successfully with cookie auth, for example, only to 403 again just because the route specified basic auth since cookie auth never gave basic auth the chance to authenticate the user (cookie auth applied first).

Berdir’s picture

Maybe I'm not following the discussions well here, but there is still the first question: how do you associate _auth method with specific routes? And how is AuthenticationSubscriber aware of this since it is running before routing? Will AuthenticationManager build and maintain it's own mapping of route -> _auth when routes are being built?

No mapping, no knowing.

We authenticate first, and know user X, with authentication method Y.

When we check access to the route, we look for the _auth option and deny access if we have a mismatch.

Maybe I'm missing something, because it almost seems too easy ;)

Also, I don't think it makes sense to authenticate successfully with cookie auth, for example, only to 403 again just because the route specified basic auth since cookie auth never gave basic auth the chance to authenticate the user (cookie auth applied first).

No, basic_auth runs first. It looks like it doesn't in HEAD, but that is because we filter it out when we don't know yet what will be allowed. This is exactly what would change, removing that filtering.

It might not allow for every imaginable scenario, but I can currently not think of any that would be an actual problem.

I think what we should do is convert the comparison from #92 into a table with HEAD | znerol's idea | my idea and extend it with at least one more use case: trying to access /user with basic authentication.

I don't have time to do that right now, if someone can start that, that would be great, I'm happy to complete/review it later.

znerol’s picture

Maybe I'm not following the discussions well here, but there is still the first question: how do you associate _auth method with specific routes?

We only make it possible to customize the behavior on a per-route basis in those two cases:

  1. When access is denied (standard behavior: render 403 response, basic auth: return 401). This is already implemented in the current patch.
  2. When access is granted (standard behavior: do nothing, basic auth / url token: return 403 if not on a route which has been declared to support basic auth / url token

The second situation is what we are talking about at the moment. The use case for this is mentioned in #137: in many cases it is desirable to restrict some authentication methods to some routes.

Both of these cases can be handled with the current design of the patch. Authentication is performed in an early request subscriber, but the effective behavior is determined on a per-route basis (but without resetting the current user and reauthentication, like HEAD currently does).

znerol’s picture

Add very naive NoBasicAuthCheck.

Status: Needs review » Needs work

The last submitted patch, 151: 2286971-auth-request-subscriber-151.diff, failed testing.

znerol’s picture

ConfigImportAllTest throws Uncaught PHP Exception InvalidArgumentException: "No check has been registered for access_check.no_basic_auth" at core/lib/Drupal/Core/Access/CheckProvider.php line 99.

larowlan’s picture

+++ b/core/modules/basic_auth/src/Access/NoBasicAuthCheck.php
@@ -0,0 +1,69 @@
+  protected function hasCredentials(Request $request) {

+++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
@@ -152,4 +158,32 @@ public function handleException(GetResponseForExceptionEvent $event) {
+  protected function hasCredentials(Request $request) {

Could go in a trait to avoid duplication?

znerol’s picture

Status: Needs work » Needs review
FileSize
40.19 KB
1.72 KB

The failures in ConfigImportAllTest is due to the fact that the routes are not immediately rebuilt by the uninstall() on line 114, instead the module-installer just sets a flag that a rebuild is needed. Regrettably that rebuild then never happens. Because of that the subsequent call to drupalPostForm blows up during the access check because a the NoBasicAuthCheck is still registered on routes. This can be fixed by calling WebTestBase::resetAll() before attempting to load the next page from the SUT.

Status: Needs review » Needs work

The last submitted patch, 155: 2286971-auth-request-subscriber-155.diff, failed testing.

znerol’s picture

Status: Needs work » Needs review
FileSize
2.16 KB
42.35 KB
41.89 KB
2.16 KB

Fix rest auth test:

  1. There is no way to disable cookie authentication with the patch. This is the case pointed out in #92 (has cookie, no ba credentials: before - 403, after - 200)
  2. The second change actually looks like a bug in the old code. Basic authentication should only be enabled on routes that explicitely have it enabled, but it seems that this also worked on html-routes before - at least on test entity.

The last submitted patch, 157: 2286971-auth-request-subscriber-156.diff, failed testing.

znerol’s picture

Berdir’s picture

I'm confused why you add a custom check for basic auth? We want a generic solution, that is also applied if there is no _auth option, which means it is applied to all routes. And that would imho just cause unexpected troubles with ANY, so it should not be an access check?

znerol’s picture

I'm confused why you add a custom check for basic auth?

Because I'm just exploring the simplest solution. We can generalize later (especially after verifying the behavior on larger scale).

it should not be an access check?

I definitely do not agree here, it should be an access check if possible.

Berdir’s picture

But it isn't possible :)

It will apply to every route and return TRUE or FALSE. This will completely break the ANY/or mode, because those will always allow access.

znerol’s picture

We want a generic solution, that is also applied if there is no _auth option

Ok, perhaps its clearer when the logic is written the other way around (see interdiff).

znerol’s picture

In HEAD if there is an _auth option on the route, then only those authentication methods are performed which are explicitly enabled. This is currently not the case in the patch. However, that behavior could also be expressed by an access check for cookie authentication analogous to the one for basic authentication.

If this works, the difference between cookie auth and basic auth is that the former is allowed on routes by default (i.e. even if no _auth option is set).

znerol’s picture

But it isn't possible :)

It will apply to every route and return TRUE or FALSE. This will completely break the ANY/or mode, because those will always allow access.

Really? If this is true then this means that the access-check system is screwed up completely (at least the any-mode). How are we supposed to compose access checks if the checker function needs to have information on whether it is part of an or vs. and check?

Berdir’s picture

Not sure what you mean. ANY means that at least one check needs to return TRUE. If we add an access check that applies to all routes, and always returns TRUE, then the routes that use ANY always result in TRUE, because at least one check is always going to do so.

That's not screwed? That's ANY, by design. And the access checker knowing about it or not doesn't change anything?

znerol’s picture

If it is not possible to add an additional access checker function to whatever route without first inspecting the access check configuration and either add one that returns deny, allow (on AND checks) or deny, neutral (on ANY checks), then in my opinion that access check system is not so usable. It means for example that you cannot add CSRF protection to your route if you use the ANY access mode.

dawehner’s picture

Mh? For CSRF you always want to KILL. Other cases might want to consider other access checkers as well.

znerol’s picture

@dawehner: There is no problem when using forbidden() to deny access, but there is a problem when using allowed() in context of an access check in ANY mode.

The problem is that an access checker has no means to say I don't care, other checkers should decide in a way which works in ANY as well as in ALL mode. If you return allowed() then you allow too much in ANY mode and if you return neutral() than that evaluates to forbidden() in ALL mode.

znerol’s picture

znerol’s picture

Introduces AuthenticationMethodCheck replacing the NoCookieAuthCheck and NoBasicAuthCheck. This is very WiP and has some stupid design problems, especially the second optional pass-by-reference parameter in AuthenticationManager::authenticate().

From a conceptual pov, the result of authentication probably should be a tuple (user-id, provider-id). In fact AnonymousSession/UserSession is supposed to serve that purpose, as @almaudoh already mentioned in #113. This however that getAuthenticationMethod/setAuthenticationMethod needs to be added to AccountInterface.

Status: Needs review » Needs work

The last submitted patch, 171: 2286971-auth-request-subscriber-171.diff, failed testing.

znerol’s picture

So, the failure in RouterPermissionTest (Access denied by default if no access specified) confirms @Berdirs concerns. I see two options to move forward:

  1. Refactor AuthenticationMethodCheck into a request subscriber which runs after the router.
  2. Reintroduce AuthenticationEnhancer but with the logic of AuthenticationMethodCheck.
znerol’s picture

Status: Needs work » Needs review
FileSize
6.13 KB
42.8 KB

Implementation of option 1 (event subscriber), I've opted for a new subscriber instead of moving the logic into the existing AuthenticationSubscriber in order to allow for site-specific optimizations. If only ever one authentication method is used, the AuthenticationCheckMethodSubscriber could be removed while leaving the other in place.

vijaycs85’s picture

Few review comments/questions:

  1. +++ b/core/lib/Drupal/Core/DrupalKernel.php
    @@ -731,6 +737,12 @@ protected function initializeContainer($rebuild = FALSE) {
    +    if (!empty($current_user_id)) {
    

    so, we are trying to set account and auth_method only for authenticated user here?

  2. +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationMethodCheckSubscriber.php
    @@ -0,0 +1,76 @@
    +   * Constructs a new authentictaino method check.
    

    Minor: typo

  3. +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationMethodCheckSubscriber.php
    @@ -0,0 +1,76 @@
    +    // @todo: Collect list of providers in a compiler pass and inject them into
    

    Do we need a follow up for this todo? if there is one, can we update the d.o URL?

  4. +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationMethodCheckSubscriber.php
    @@ -0,0 +1,76 @@
    +   * Checks whether authentication was performed with a method allowed on the route.
    

    More than 80 chars?

  5. +++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
    @@ -152,4 +158,32 @@ public function handleException(GetResponseForExceptionEvent $event) {
    +  protected function hasCredentials(Request $request) {
    

    Why isn't this part of applies()? do we really need a new method here?

almaudoh’s picture

Re #171: I still think the AnonymousUserSession / UserSession should be used for authentication instead of the User entity. This decouples the authentication system from the entity system and keeps code less complex.

We could re-purpose the (Anonymous)UserSession into value objects to represent sessions as the name implies. We don't have to pollute the AccountInterface with getAuthenticationMethod/setAuthenticationMethod since it's not needed there, just extend a UserSessionInterface from it to hold those methods, then AuthenticationProviderInterface::authenticate() would return an instance of UserSessionInterface

znerol’s picture

Introduces AuthenticationProviderAccessInterface which is used by AuthenticationProviderCheckSubscriber and implemented in AuthenticationManager. That makes it possible to get rid of the weird setAuthenticationMethod/getAuthenticationMethod on AccountProxy.

That looks now very close to how I'd like it to be - except for the setInitialAccountId weirdness in AccountProxy.

Does not yet address any of #176.

znerol’s picture

Tidy up the BasicAuth methods as suggested by #176. So it looks like appliesToRoute does in fact the same as AuthenticationProviderAccessInterface::access() and if we use that in AuthenticationManager::access, then there is no need anymore for AuthenticationManager::$defaultProviders. Bam, two birds with one stone!

klausi’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Authentication/AuthenticationProviderAccessInterface.php
    @@ -0,0 +1,37 @@
    +   *
    +   * @var \Symfony\Component\HttpFoundation\Request
    +   *   The request.
    +   *
    

    This should be @param.

  2. +++ b/core/lib/Drupal/Core/Authentication/AuthenticationProviderAccessInterface.php
    @@ -0,0 +1,37 @@
    +  /**
    +   * Checks whether the authentication method is allowed on a given route/path.
    +   *
    +   * While authentication itself is run before routing, this method is called
    +   * after routing, hence RouteMatch is available and can be used to inspect
    +   * route options.
    +   *
    +   * @var \Symfony\Component\HttpFoundation\Request
    +   *   The request.
    +   *
    +   * @return bool
    ...
    +   *   FALSE.
    +   */
    +  public function access(Request $request);
    

    I'm not sure if "access" is a good name here. Maybe "isAllowed()" instead?

    @return should say "TRUE if this authentication method is allowed ..."

  3. +++ b/core/lib/Drupal/Core/Session/AccountProxy.php
    @@ -75,10 +52,17 @@ public function setAccount(AccountInterface $account) {
    +        // After the container is rebuilt, DrupalKernel sets the initial
    +        // account to the id of the logged in user. This is necessary in order
    +        // to refresh the user account reference here.
    +        $this->account = \Drupal::entityManager()->getStorage('user')->load($this->initialAccountId);
    

    Could you add a comment here why we cannot inject the user storage here and must call the global \Drupal?

Otherwise looks like an improvement to me and the changes to the REST tests look fine.

znerol’s picture

Status: Needs work » Needs review
FileSize
50.51 KB
23.77 KB

AuthenticationManager as well as BasicAuth still smell, especially because of the weird exception handling due to the basic auth challenge. Also the AuthenticationSubscriber cannot really take any AuthenticationProviderInterface but relies on AuthenticationManager deviating from its interface.

BasicAuth also has the provider id hard-coded in its access() method, which is really a bad thing. The provider id is a concept which is actually private to the AuthenticationManager. There is no means on how we could pass around provider ids in the current interfaces.

Both of these problems can be fixed by moving some of the basic auth exception logic to the authentication subscriber and rewrite the interface according to the special needs of basic auth - i.e. it desires to send a special response on a 403 (access denied), if there are no credentials on the request.

As a result any implementer of AuthenticationProviderInterface now can be used without AuthenticationManager directly in AuthenticationSubscriber, even BasicAuth. On the other hand the only entity which has knowledge about the _auth route option is now AuthenticationManager.

#180 not yet addressed.

znerol’s picture

Addresses #180.3, also removes current user from basic auth again.

znerol’s picture

Rename AuthenticationProviderAccessInterface to AuthenticationProviderFilterInterface and access() to appliesToRoutedRequest(), addresses the remaining comments in #180.

Berdir’s picture

  1. +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
    @@ -7,71 +7,139 @@
    +    // The priority for authentictaion must be higher than the highest event
    

    typo.

  2. +++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
    @@ -131,25 +132,14 @@ public function authenticate(Request $request) {
    +    global $base_url;
    

    you should be able to get this from the request context, getCompleteBaseUrl()

znerol’s picture

When implementing an authentication provider collection (like authentication manager basically does), it is important that the filter-method (a.k.a. appliesToRoutedRequest) knows whether it is evaluated for authenticated requests (before throwing eventually throwing the 403) or for unauthenticated requests (before eventually sending the 401).

Addressed #184. Replaced the $base_url (the fallback for when the site name is not configured, which is very unlikely) with a well known static string. The realm can be chosen freely, so let's make use of that fact.

Service construction / global providers still need some work. At least sorting of the providers should be done in the compiler pass.

znerol’s picture

Rewriting service construction requires the introduction of a new compiler pass. Filed a follow-up: #2432585: Improve authentication manager service construction to support custom global service providers.

znerol’s picture

znerol’s picture

FileSize
716 bytes
1.48 KB

Add a module and a script to help exercising before/after scenarios. Test numbers can be specified on command line. If none are given, all requests are performed.

znerol’s picture

The at module from the previous comment has a bug, look for s/both_auth/basic_auth/g in at.routing.yml.

Results so far:

Table removed, too much visual clutter.

znerol’s picture

This is the same table but ordered authentication credentials on the request.

Table removed, too much visual clutter.

Status: Needs review » Needs work

The last submitted patch, 187: 2286971-auth-request-subscriber-186.diff, failed testing.

cosmicdreams’s picture

This is hard to follow. The tables seem to have different labels and structure.

Can you bold the items that demonstrate the differences / bugs?

Berdir’s picture

Here's my attempt at displaying the results.

Note, I misunderstood first, so explanation of the columns:

access: allow = anyone, auth = only when logged in, deny = nobody is allowed to access
_auth: what the route defines as the _auth option
cred: credentials the request sends, (both means both cookie and basic auth in the same request)
response: the response status code

Output is a "diff -up head.txt patch.txt", so the changes are as the patch changes it:

--- head.txt	2015-02-23 18:53:45.301784493 +0100
+++ patch.txt	2015-02-23 18:54:10.129958403 +0100
@@ -2,13 +2,13 @@ No. access  _auth   cred    response
  1. allow   none    none    200
  2. allow   none    cookie  200
  3. allow   none    basic   200
- 4. allow   none    both    200
+ 4. allow   none    both    403
  5. allow   cookie  none    200
  6. allow   cookie  cookie  200
  7. allow   cookie  basic   200
- 8. allow   cookie  both    200
+ 8. allow   cookie  both    403
  9. allow   basic   none    200
-10. allow   basic   cookie  200
+10. allow   basic   cookie  403
 11. allow   basic   basic   200
 12. allow   basic   both    200
 13. allow   both    none    200
@@ -18,13 +18,13 @@ No. access  _auth   cred    response
 17. auth    none    none    403
 18. auth    none    cookie  200
 19. auth    none    basic   403
-20. auth    none    both    200
+20. auth    none    both    403
 21. auth    cookie  none    403
 22. auth    cookie  cookie  200
 23. auth    cookie  basic   403
-24. auth    cookie  both    200
+24. auth    cookie  both    403
 25. auth    basic   none    401
-26. auth    basic   cookie  401
+26. auth    basic   cookie  403
 27. auth    basic   basic   200
 28. auth    basic   both    200
 29. auth    both    none    401
@@ -40,7 +40,7 @@ No. access  _auth   cred    response
 39. deny    cookie  basic   403
 40. deny    cookie  both    403
 41. deny    basic   none    401
-42. deny    basic   cookie  401
+42. deny    basic   cookie  403
 43. deny    basic   basic   403
 44. deny    basic   both    403
 45. deny    both    none    401

The differences are easy to explain:
4: sends both, basic auth is checked first, authenticated, then denied (because non means the default aka cookie). This is exactly the change that I mentioned above, IMHO OK and actually expected.
8: same except it explicitly wants cookie
10: This is an interesting change, I think this makes sense, wrong auth method should not allow, even if the route allows anyone access (that combination does not make sense, but it's also not relevant that it would allow anyone, just that it does if you're authenticated). No idea why it allowed that before?

20 & 24 are the same as 4 & 8, 26 is the same as 10 (it makes no difference for all of those if only auth or all are allowed)

42. is the same as 10 really, we would have allowed access but then basic auth would have kicked in with the challenge I think. Now we deny access.

So all those changes are IMHO valid, and I like the new Authentication subscriber :)

znerol’s picture

This is strange, my results also indicate that 3 and 7 change. I'd also expect that because of the same reason like 4 and 8.

--- head.txt	2015-02-23 19:38:19.385737416 +0100
+++ patch.txt	2015-02-23 19:38:48.009918553 +0100
@@ -1,14 +1,14 @@
 No. access  _auth   cred    response
  1. allow   none    none    200
  2. allow   none    cookie  200
- 3. allow   none    basic   200
- 4. allow   none    both    200
+ 3. allow   none    basic   403
+ 4. allow   none    both    403
  5. allow   cookie  none    200
  6. allow   cookie  cookie  200
- 7. allow   cookie  basic   200
- 8. allow   cookie  both    200
+ 7. allow   cookie  basic   403
+ 8. allow   cookie  both    403
  9. allow   basic   none    200
-10. allow   basic   cookie  200
+10. allow   basic   cookie  403
 11. allow   basic   basic   200
 12. allow   basic   both    200
 13. allow   both    none    200

znerol’s picture

Status: Needs work » Needs review
Berdir’s picture

Confirmed, sorry. Looks like the page cache interfered locally, I think this is the security issue that was recently closed and has yet to be fixed in 8.x (aka, basic auth requests must not be page cached, ever).

znerol’s picture

Remove dead code and stale methods.

almaudoh’s picture

@znerol uploading patches at superspeed - like The Flash :)

Some reviews and some comments:

  1. -   * @var string
    +   * @var \Drupal\Core\Authentication\AuthenticationAccessProviderInterface[]
        */
    -  protected $triggeredProviderId = '';
    +  protected $filters;
    

    Doc should be AuthenticationProviderFilterInterface.

  2. +   * List of providers which implement the challenge interface.
    +   *
    +   * @var \Drupal\Core\Authentication\AuthenticationChallengeProviderInterface[]
    +   */
    +  protected $challengers;
    +
    

    The name $challengers threw me off at first, and I was wondering "what are they challenging". Couldn't figure out from the name until I read the code - challenging the AccessDeniedException? Maybe a more descriptive name?

  3. +   * @see https://www.drupal.org/node/2432585
    +   */
    +  public function __construct($global_providers = ['cookie' => TRUE]) {
    +    $this->globalProviders = $global_providers;
    +  }
    

    Hard-coding cookie auth, what if there's a use case where cookie auth is not wanted? Guess we can discuss that more at #2432585: Improve authentication manager service construction to support custom global service providers

  4. @@ -72,139 +97,175 @@ public function addProvider(AuthenticationProviderInterface $provider, $id, $pri
    +    if ($provider instanceof AuthenticationProviderFilterInterface) {
    +      $this->filters[$id] = $provider;
    +    }
    +    if ($provider instanceof AuthenticationProviderChallengeInterface) {
    +      $this->challengers[$id] = $provider;
    +    }
    .
    .
    .
    index 80f7796..3619e2b 100644
    --- a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
    +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
    @@ -7,71 +7,139 @@
    +  public function __construct(AuthenticationProviderInterface $authentication_provider, AccountProxyInterface $account_proxy) {
         $this->authenticationProvider = $authentication_provider;
    +    $this->filter = ($authentication_provider instanceof AuthenticationProviderFilterInterface) ? $authentication_provider : NULL;
    +    $this->challengeProvider = ($authentication_provider instanceof AuthenticationProviderChallengeInterface) ? $authentication_provider : NULL;
    

    Wonder if we couldn't just inject these as separate arguments since we have separate interfaces, this makes the dependencies more explicit. Of course, this implies @authentication would be repeated three times in the service definition arguments. But this just makes it clearer that AuthenticationManager is playing a triple role.

  5. +   * @return string|NULL
    +   *   The id of the first authentication provider which applies to the request.
    +   *   If no application detects appropriate credentials, then NULL is returned.
        */
    .
    .
    +  protected function getChallenger($request) {
    +    if (!empty($this->challengers)) {
    

    @return doc doesn't match method.

Overall, these are great changes.


Re #193 & #194: The behavior in 4 & 8 is not satisfactory. Without specifying an _auth method, basic authenticates and then denies. Moreover, the route / path is allowed for all users. That is not proper behavior IMHO

If basic is not among the default auth providers, it shouldn't be the one to authenticate in the first place, even though it is higher in priority.

This is my concern and will cause problems in a use case I have right now.

[Edit: completed sentence]

znerol’s picture

Issue summary: View changes
znerol’s picture

Issue summary: View changes
znerol’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update
Berdir’s picture

Re #193 & #194: The behavior in 4 & 8 is not satisfactory. Without specifying an _auth method, basic authenticates and then denies. Moreover, the route / path is allowed for all users. That is not proper behavior IMHO

I respectfully disagree ;)

Note that it is *not* possible authenticate with basic auth on HEAD if no _auth option is provided either. What happens instead is that basic_auth fails to authenticate in the first place*, we fall back to session, "authenticate" as anon user and are granted access.

* To clarify even more: basic auth *always* fails at that point. We throw away the result and repeat it after we get the route, and only then basic auth can authenticate.

The fact that all users would be allowed to access the page is a detail that we don't know yet at that point.

This is also visible in the fact that those cases are no longer different in second group of checks, where we actually require a user. There both deny access, if for a different reason.

If basic is not among the default auth providers, it shouldn't be the one to authenticate in the first place, even though it is higher in priority.

This is my concern and will cause problems in a use case I have right now.

Care to share your use case for this? I've trouble imaging a use case that relies on this.

Also, you +1'd the suggestion from @znerol in #92, which would have removed the concept of a limited list of default auth providers completely, so basic auth would have allowed access to all routes.

We just can't have it both ways. We can't require the authenticated user before routing and make authentication depending on the matched route.

See also #104, which explains possible issues with the current behavior. This issue IMHO solves that, because we only have one authentication process and we can rely on the result, even if we end up denying access because of the used method.

cpj’s picture

In the hope that the discussion has completed, I reviewed & tested the code, and using the test module from #188, I believe I have been able to reproduced the results in #193.

But I don't feel confident enough about what is going on to mark this RTBC. I wonder if some sort of flow chart, maybe something like https://www.drupal.org/node/2212069#comment-9651765 would be helpful ?

znerol’s picture

I do not think that this is RTBC yet. There are still some code style issues pointed out in #200. I also do neither like the challengeException method nor the challenger instance variable. I think it might be worth extracting the creation of the UnauthorizedHttpException to the AuthenticationManager and only leave generation of the $challenge up to the implementing provider. The interface could be renamed to AuthenticationChallangeGeneratorInterface, and the method to generateChallenge().

almaudoh’s picture

Re #204: We want to see the new authentication system as something that should be flexible enough to apply to other (possibly not yet envisioned) use cases. In this patch we are basing it on the specific use cases that assume basic auth would run before cookie auth - I realize this is based on what we already have in HEAD.

While this patch is a big improvement on what we have in HEAD, we do have to acknowledge that the use of the _auth parameter in routing is the key limitation in implementing a more flexible and robust authentication system. Because we should only do authentication once, with the appropriately selected auth method, prior to routing.

In order not to hold up this patch, which I would not want to do, we can continue this line of reasoning in a separate issue.

Also, you +1'd the suggestion from @znerol in #92, which would have removed the concept of a limited list of default auth providers completely, so basic auth would have allowed access to all routes.

I actually saw that as an intermediate step, after which we could figure out how to limit auth methods to routes without using the _auth routing parameter (mentioned in #89). But the ideas took a different direction, which is why I got confused thereafter.

We just can't have it both ways. We can't require the authenticated user before routing and make authentication depending on the matched route.

Will experiment with other different possibilities in a separate issue.


Re #205: A flow chart would be nice. Let's see what I can come up with.
almaudoh’s picture

Edit: removed the comment

cpj’s picture

@almaudoh, #207

While this patch is a big improvement on what we have in HEAD, we do have to acknowledge that the use of the _auth parameter in routing is the key limitation in implementing a more flexible and robust authentication system.

I think flexibility & robustness are essential. I find the current system & proposed changes difficult to understand. The 45 step manual check in #193 illustrates how complex the system is. So some kind of picture would be very helpful - thanks for offering.

Berdir’s picture

Re #206: Are those things really blocking this (a critical which I think is blocking further work on ~3 related/meta criticals)? Can we put some things like anything that's not a public API to follow-ups?

Updated patch with some documentation fixes.

1. Fixed, also a few other documentation improvements, verified by going through the class, PhpStorm can now identify each method call as valid based on the documentation.

2. See http://en.wikipedia.org/wiki/Challenge%E2%80%93response_authentication, but I agree that the name isn't perfect, one thing is that I'm wondering is whether it is too specific/narrow.. could there be other things an authentication provider might do that's not a challenge? Maybe we can improve the documentation on the interface/property as a start?

3. Yeah, I think we should look into that in a follow-up issue. In the meantime, you could write a ServiceModifier implementation that would pass in a different global provider. It's not really hardcoded, just the default constructor value :)

4. No we can't, not without writing a custom service collector compiler pass, that only supports a single method/interface. I'm not sure where you mean to repeat @authentication thrice, this method is called implicitly due to service tags. Doesn't seem like a big problem to me, it's essentially just a internal optimization to store them differently, we could also loop over all providers and check if they implement a certain interface when we access it. There's no need for multiple public methods and someone outside of AuthenticationManager to know about this.

almaudoh’s picture

Thanks @Berdir. I think we can move forward with this - the other issues can be follow-ups. Was hoping to have some time to review and test this but I can't git pull now (been behind a firewall for the last 3 weeks). Hope to look at the patch in some detail before the weekend.

jibran’s picture

+++ b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php
@@ -43,16 +39,43 @@ class AuthenticationManager implements AuthenticationProviderInterface, Authenti
+   * @todo: Revisit service construction. Especially write a custom compiler
+   * pass which is capable of collecting, sorting and injecting all providers
+   * (including global/vs non global), filters and challengers on compile time.

This is not according to doc standards.

Berdir’s picture

klausi’s picture

Status: Needs review » Needs work
Issue tags: +Needs change record

The patch makes lots and lots of sense to me.

  1. +++ b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php
    @@ -72,70 +94,165 @@ public function addProvider(AuthenticationProviderInterface $provider, $id, $pri
    +  protected function defaultFilter(Request $request, $authenticated, $provider_id) {
    +    $route = RouteMatch::createFromRequest($request)->getRouteObject();
    +    $has_auth_option = isset($route) && $route->hasOption('_auth');
    +
    +    $allowed_by_default = !$has_auth_option && isset($this->globalProviders[$provider_id]);
    +    $allowed_by_option = $has_auth_option && in_array($provider_id, $route->getOption('_auth'));
    +    return $allowed_by_default || $allowed_by_option;
       }
    

    Can we have test coverage for this? Do we already have an AuthenticationManager (unit?)test somewhere that we can expand?

  2. +++ b/core/lib/Drupal/Core/Authentication/AuthenticationProviderFilterInterface.php
    @@ -0,0 +1,39 @@
    +/**
    + * Restrict authentication methods to a subset of the site.
    + *
    + * Some authentication methods should not be available throughout a whole site.
    + * E.g., there are good reasons to restrict insecure methods like HTTP basic
    + * auth or an URL token authentication method to API-only routes.
    + */
    +interface AuthenticationProviderFilterInterface {
    

    so noone except the authentication manager implements this interface in core? Do we have test coverage for this?

Before we RTBC this we also need a change record draft.

dawehner’s picture

This looks great! I'm working now on a change record.

+++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
@@ -7,71 +7,139 @@
+    // The priority for authentication must be higher than the highest event
+    // subscriber accessing the current user. Especially it must be higher than
+    // LanguageRequestSubscriber as LanguageManager accesses the current user if
+    // the language module is enabled.
+    $events[KernelEvents::REQUEST][] = ['onKernelRequestAuthenticate', 300];

So this means we could move that onto a middleware at some point.

dawehner’s picture

jibran’s picture

Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs change record

Sorry @Berdir for such a use less review. I hoped that by bumping it someone would actually review it. Let's see what @alexpott thinks about it.

jibran’s picture

Status: Reviewed & tested by the community » Needs work

Sorry old browser tab. NW as per #215

Berdir’s picture

Thanks for the reviews.

1. I had a quick look at unit tests, but mocking RouteMatch::createFromRequest($request)->getRouteObject(); would be pretty painful I think, lots of object and methods to set up even when creating real Request object.

2. We have no other implementation, but we're testing the one we do I think.

What we could do to improve test coverage is convert the script from #188 into a web test, and hardcode and document the expected results.

Berdir’s picture

Status: Needs work » Needs review
FileSize
54.8 KB
3.84 KB

Ok, writing a unit test for that method is actually not that hard.

I've also refactored that method, I think it is much easier to understand that way, and it is actually by far not as complex as it looked before.

The unit tests I think actually cover both points, at least the relevant implementation in AuthenticationManager (that you can implement the filter interface, although I'm not quite sure what the use case for that is)

Berdir’s picture

Grml, had some left-overs in the unit test class.

The last submitted patch, 221: 2286971-auth-request-subscriber-221.patch, failed testing.

jibran’s picture

Status: Needs review » Reviewed & tested by the community

Thanks for the test @Berdir. I think it is ready now.

Berdir’s picture

Updated the change record a bit with a more detailed before/after description of what the change means.

dawehner’s picture

berdir++

cosmicdreams’s picture

In reading the code last night I found a @todo that mentions this issue. It reads:

from Drupal/Core/Authentication/Provider/Cookie.php

/**
   * {@inheritdoc}
   */
  public function authenticate(Request $request) {
    if ($request->getSession()->start()) {
      // @todo Remove global in https://www.drupal.org/node/2286971
      global $_session_user;
      return $_session_user;
    }

    return NULL;
  }

This global also exists in:
Drupal/Core/Session/SessionHandler.php
Drupal/Core/Session/SessionManager.php

As far as I can tell, this patch does not remove global $_session_user. Should it?

Removing it was the focus of my efforts in this area last year and I wasn't able to remove it because of the problem this issue outlines.

Should we simply create a follow up and update the todo to point to a fresh issue where we remove the globals?

Berdir’s picture

#2228393: Decouple session from cookie based user authentication will take care of that, I think. This issue temporarily merged that in but that was reverted again. We should update the @todo to point to that.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php
    @@ -43,16 +39,42 @@ class AuthenticationManager implements AuthenticationProviderInterface, Authenti
    +  /**
    +   * Constructs an authentication manager.
    +   *
    +   * @todo Revisit service construction. Especially write a custom compiler pass
    +   *   which is capable of collecting, sorting and injecting all providers
    +   *   (including global/vs non global), filters and challengers on compile
    +   *   time in https://www.drupal.org/node/2432585.
    +   */
    +  public function __construct($global_providers = ['cookie' => TRUE]) {
    

    Missing @param documentation - yes we have a todo but in case this remains...

  2. +++ b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php
    @@ -72,70 +94,168 @@ public function addProvider(AuthenticationProviderInterface $provider, $id, $pri
    +   * @param bool $authenticated
    +   *   Whether or not the request is authenticated.
    ...
    +  protected function defaultFilter(Request $request, $authenticated, $provider_id) {
    

    $authenticated is not used - do we need it?

  3. +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
    @@ -7,71 +7,139 @@
    +   * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event
    

    Missing leading slash

  4. +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
    @@ -7,71 +7,139 @@
    +   * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event
    

    Missing leading slash

  5. +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
    @@ -7,71 +7,139 @@
    +  public function onExceptionSendChallange(GetResponseForExceptionEvent $event) {
    ...
    +    $events[KernelEvents::EXCEPTION][] = ['onExceptionSendChallange', 75];
    

    Challenge is misspelt.

  6. +++ b/core/modules/config/src/Tests/ConfigImportAllTest.php
    @@ -119,6 +119,8 @@ public function testInstallUninstall() {
    +    $this->resetAll();
    +
    

    Why is this required? At the very least there should be a comment.

  7. +++ b/core/modules/rest/src/Tests/AuthTest.php
    @@ -84,7 +84,10 @@ public function testRead() {
    -  protected function basicAuthGet(Url $url, $username, $password) {
    +  protected function basicAuthGet(Url $url, $username, $password, $mime_type = NULL) {
    

    Missing param documentation.

  8. Lets fix the @todo mentioned in #227/#228
znerol’s picture

See #155 regarding the changes in ConfigImportAllTest. This hunk might be not necessary anymore though because we do not rely on access checks in the latest patch.

Berdir’s picture

Status: Needs work » Needs review
FileSize
56.51 KB
7.07 KB

Thanks for finding all that stuff.

1; Added documentation.
2: Removed it.
3-5: Fixed.
6: Removed, let's see.
7: Added.
8: Updated all @todo's.

cpj’s picture

@Berdir - Is the manual testing from #193 still needed ?

Berdir’s picture

I don't think so.. as mentioned above, we might want to convert that test script into a test to verify the expected behavior, but I think we can do that in a follow-up, we have enough existing coverage IMHO.

almaudoh’s picture

Status: Needs review » Reviewed & tested by the community

Nice work @Berdir. Back to RTBC after addressing Alex's comments. I have uploaded the interdiff from #73 at #2228393: Decouple session from cookie based user authentication to move cookie auth from SessionHandler to the Cookie auth provider.

cpj’s picture

I have also reviewed the code again, so +1 for RTBC

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue addresses a critical bug and is allowed per https://www.drupal.org/core/beta-changes. Committed 531f95e and pushed to 8.0.x. Thanks!

  • alexpott committed 531f95e on 8.0.x
    Issue #2286971 by znerol, Berdir, almaudoh, cilefen: Remove dependency...
clemens.tolboom’s picture

Status: Fixed » Closed (fixed)

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

xjm’s picture

znerol’s picture