Problem/Motivation

Follow-up to #2286971: Remove dependency of current_user on request and authentication manager. Revisit service construction. The AuthenticationManager currently hard-codes a list of global providers as a default argument to its constructor. Contrib authentication providers wishing to add an additional global provider need to alter the definition of the authentication manager from within a ServiceProvider class in order to add a new global provider. This may work if only one contrib module does this, it will likely break if multiple service provider classes try to alter the same argument of the definition.

Proposed resolution

  1. Modify the TaggedHandlersPass compiler pass to be able to inject additional extra parameters which the AuthenticationManager would use to define at compile time which providers are global.
  2. Replace the $id parameter with a $provider_id parameter on the addProvider method and remove the undocumented convention that names of authentication provider services need to be prefixed with authentication.

This also helps resolving #2456283: AuthenticationManager::getSortedProviders is protected in contrib and therefore renders #2456303: AuthenticationManager needs interface and ::getProviderKeys unnecessary.

Contrib modules known to be blocked by this issue:

Remaining tasks

Reviews
Commit

User interface changes

None

API changes

  • Contrib can now add global authentication providers by setting the global tag parameter to TRUE in the service definition. (API addition) E.g. in my_module.services.yml
    my_module.authentication.custom_authentication:
      class: Drupal\my_module\Authentication\CustomAuthenticationProvider
      arguments: ['@injected_service_1', '@injected_service_2']
      tags:
          - { name: authentication_provider, provider_id: 'provider_id', priority: 0, global: TRUE }
    
  • The provider id needs to be specified explicitely and is not derived from the service id anymore (i.e. the string used in the _auth route option to specify which providers are allowed on a route)

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Task
Issue priority Normal because it doesn't affect the normal operation the authentication system
Prioritized changes This issue is a follow-up from a critical issue and a blocker for contributed projects. It will improve contrib experience by easily allowing the implementation of alternate authentication providers and mechanisms.
Disruption Disruptive for contributed and custom modules because third-party authentication providers will need to update their service definitions and addProvider function signatures.

Comments

dawehner’s picture

One simple idea berdir had in mind which we could start with is to put the default into a container parameter, so you can override it much easier.

znerol’s picture

Tagged as contrib project blocker because at the moment a custom ServiceProvider class is necessary in order to add new global authentication providers.

znerol’s picture

Status: Postponed » Active
clemens.tolboom’s picture

znerol’s picture

Issue summary: View changes

One simple idea berdir had in mind which we could start with is to put the default into a container parameter, so you can override it much easier.

That works well for per-site use-cases. It does not help with contrib modules providing new default providers. Perhaps this should be configuration instead?

dawehner’s picture

Fair, I'm just a bit worried as thing inits config potentially earlier ... do you think about putting that information still in the container but retrieve it form config while rebuilding the container?

almaudoh’s picture

Status: Active » Needs review
StatusFileSize
new5.21 KB

Ok, so an initial patch to kick this off.
Started with a container parameter %authentication.global_providers% which is then updated by the AuthenticationManager::addProvider() call to incorporate providers that have been tagged as global. This allows contrib to update the list of global (default) authentication providers.
Had to modify the service collector TaggedHandlersPass to allow additional custom arguments in the tagged service definitions.
Nomenclature is based on existing code in HEAD.

znerol’s picture

  1. +++ b/core/core.services.yml
    @@ -7,6 +7,7 @@ parameters:
    +  authentication.global_providers: {}
    
    @@ -1200,6 +1201,7 @@ services:
    +    arguments: ['%authentication.global_providers%']
    
    +++ b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php
    @@ -75,7 +75,7 @@ class AuthenticationManager implements AuthenticationProviderInterface, Authenti
    -  public function __construct($global_providers = ['cookie' => TRUE]) {
    +  public function __construct($global_providers = []) {
         $this->globalProviders = $global_providers;
       }
    

    I think we do not need the container parameter if we can set the global flag on the tag. That means we could remove the constructor completely.

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
    @@ -94,16 +94,20 @@ public function process(ContainerBuilder $container) {
    +        $other_params = [];
    

    I'd use $extra_params or $additional_params as the name.

  3. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
    @@ -94,16 +94,20 @@ public function process(ContainerBuilder $container) {
    -          if ($param->getName() == 'id') {
    +          else if ($param->getName() == 'id') {
    

    We still need to use if here because the preceding condition is targeting the type-hint, not the parameter name. Also change the conditions to use the identity operator (===), instead of equality.

znerol’s picture

Oh, and we probably should add test coverage to TaggedHandlersPassTest.

znerol’s picture

And we probably should add back the AuthenticationManagerInterface because of the addProvider() method. I've removed it prematurely.

almaudoh’s picture

StatusFileSize
new10.38 KB
new10.46 KB

Thanks @znerol for the review
#9
1. Good point. Fixed.
2. Fixed
3. Fixed

Also added tests to TaggedHandlersPassTest for extra arguments and re-introduced the AuthenticationManagerInterface with addProvider() method in there.

Status: Needs review » Needs work

The last submitted patch, 12: revisit_authentication-2432585-12.patch, failed testing.

almaudoh’s picture

Status: Needs work » Needs review
StatusFileSize
new12.74 KB
new3.98 KB

Fixed some docs typos, used more explicit variable names and adjusted AuthenticationManagerTest to match the new interface.

Status: Needs review » Needs work

The last submitted patch, 14: revisit_authentication-2432585-14.patch, failed testing.

almaudoh’s picture

Status: Needs work » Needs review
StatusFileSize
new12.72 KB
new2.08 KB

Was a bit too over-zealous with the variable renames.

znerol’s picture

Thanks! Test coverage looks great now for the TaggedServiceProvider.

At devdays we also discuss the weird magic we use to derive the provider_id from the service name (i.e. the service id of an authentication provider must follow the schema authentication.provider_id and thus cannot be prefixed with the module name). A better way to resolve that problem would be to add another extra parameter provider_id and explicitly specify it in every service definition. E.g.:

  user.authentication.cookie:
    class: Drupal\user\Authentication\Provider\Cookie
    arguments: ['@session_configuration', '@database']
    tags:
      - { name: authentication_provider, provider_id: 'cookie', priority: 0, global: TRUE }

Thanks to the new extra parameters it will likely be enough to simply rename the $id paramater to $provider_id in AuthenticationManager::addProvider. Thought that might be a BC break. If we want to avoid this, then we'd need to add the an optional $provider_id parameter and initialize it with the current magic if NULL.

An explicit $provider_id on the service definition also would make it simpler for contrib to collect all provider ids (e.g. #2456283: AuthenticationManager::getSortedProviders is protected).

cpj’s picture

@znerol. #17.

I like the idea of an explicit $provider_id to the current magic, so +1 for that.

As you say, it might be a very minor BC without the fallback you suggest, but to avoid confusion, I personally think it's preferable to only support one way of defining the $provider_id.

almaudoh’s picture

+1 also to explicit $provider_id. Dunno how many contrib modules will break, but explicitness is always better.

almaudoh’s picture

StatusFileSize
new14.68 KB
new5.46 KB

Implemented explicit $provider_id and adjusted tests to match. Let's see what breaks.

znerol’s picture

Great!

+++ b/core/lib/Drupal/Core/Authentication/AuthenticationManagerInterface.php
@@ -0,0 +1,29 @@
+  /**
+   * Adds a provider to the array of registered providers.
+   *
+   * @param \Drupal\Core\Authentication\AuthenticationProviderInterface $provider
+   *   The provider object.
+   * @param string $provider_id
+   *   Identifier of the provider.
+   * @param int $priority
+   *   The providers priority.
+   * @param bool $global
+   *   TRUE if the provider is to be applied globally on all routes.
+   */
+  public function addProvider(AuthenticationProviderInterface $provider, $priority = 0, $provider_id, $global = FALSE);

Can you modify the function signature such that mandatory $provider_id is before the optional $priority? Also specify in the @param that $priority and $global are not required (prefix with (optional)).

almaudoh’s picture

Can you modify the function signature such that mandatory $provider_id is before the optional $priority?

Tried that but it breaks because of how TaggedHandlersPass is written. I will see how to make that work.

znerol’s picture

+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
@@ -148,6 +157,9 @@ public function process(ContainerBuilder $container) {
           ksort($arguments);
+          if (isset($extra_arguments[$id])) {
+            $arguments += $extra_arguments[$id];
+          }

I think we may want to merge the arguments before sorting.

almaudoh’s picture

StatusFileSize
new16.28 KB
new6.4 KB

I think we may want to merge the arguments before sorting.

Yeah, it was easier than I thought. Also improved the test coverage.

almaudoh’s picture

StatusFileSize
new15.99 KB
new1.05 KB

Simplified the code.

znerol’s picture

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
    @@ -125,6 +130,10 @@ public function process(ContainerBuilder $container) {
    +            $extra_arguments[$id][$pos] = isset($attributes[0][$name]) ? $attributes[0][$name] : NULL;
    

    I suspect that this overrides the default value of an optional extra paremeter. That parameter might then receive NULL instead of the default value.

  2. +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php
    @@ -242,6 +364,16 @@ class InvalidConsumer {
    +  public function addHandler(HandlerInterface $instance, $priority = 0, $extra1, $extra2) {
    ...
    +  public function addWithId(HandlerInterface $instance, $id, $priority = 0, $extra1, $extra2) {
    ...
    +  public function addWithDifferentOrder(HandlerInterface $instance, $extra1, $priority = 0, $extra2) {
    

    I do not intend to hold off the patch but it is a bit weird to have mandatory parameters after optional ones. In fact that makes the optional ones mandatory also. Perhaps add extra paremeters with default values?

almaudoh’s picture

StatusFileSize
new16.1 KB
new3.58 KB

I suspect that this overrides the default value of an optional extra paremeter. That parameter might then receive NULL instead of the default value.

You're right. Didn't see that initially. To fix it we have to re-introduce the else if after if ($param->getClass()) { since we assume that the only classed argument will be the handler itself, while others will be scalars.
Also added test coverage for the default values.

it is a bit weird to have mandatory parameters after optional ones.

Fixed.

Status: Needs review » Needs work

The last submitted patch, 27: revisit_authentication-2432585-27.patch, failed testing.

almaudoh’s picture

Status: Needs work » Needs review
StatusFileSize
new16.89 KB
new812 bytes

Missed out the early_translation_test authentication provider. (I wonder how it managed to pass before).

almaudoh’s picture

Issue summary: View changes

Updated IS.

Also automatically eliminate AuthenticationManager for sites which only use one authentication method.

I removed this from the IS scope considering that is is easy to swap out the authentication manager service with a single authentication provider where needed. Correct me if I'm wrong :).

znerol’s picture

Issue summary: View changes
znerol’s picture

I removed this from the IS scope considering that is is easy to swap out the authentication manager service with a single authentication provider where needed.

In fact it would be nice if we did that automatically. This might be easy to implement with a service alias: By default authentication points to user.authentication.cookie. If number of services with the authentication_provider tag is > 1, then authentication points to authentication.manager. This could be easily be implemented in a simple additional compiler pass.

The benefit would be that the wast majority of sites would automatically run without the authentication manager.

andypost’s picture

We use a sort of that for language manager, while no language module available we use very primitive one that shipped with core

znerol’s picture

Status: Needs review » Reviewed & tested by the community

So, I profiled how much #32 would bring us performance wise by hard-wiring the cookie authentication into authentication service by hand. In fact we save 65 function calls and 30 kilobyte for authenticated users (wall time report does not look reliable). Difference for anonymous users was zero function calls. I had to hack xhprof-kit index-perf.php in order to inject the session cookie directly via $_COOKIE superglobal.

=== baseline-8.0.x..8.0.x compared (553a3c1b20302..553a3c4aea704):

ct  : 43,786|43,786|0|0.0%
wt  : 126,756|128,081|1,325|1.0%
mu  : 19,262,960|19,262,960|0|0.0%
pmu : 19,428,496|19,428,496|0|0.0%

=== baseline-8.0.x..8.0.x-hard-wired-cookie compared (553a3c1b20302..553a3c6029bec):

ct  : 43,786|43,721|-65|-0.1%
wt  : 126,756|126,741|-15|-0.0%
mu  : 19,262,960|19,232,000|-30,960|-0.2%
pmu : 19,428,496|19,396,272|-32,224|-0.2%

That said I think the performance gain is too little to justify adding another compiler pass.

The changes in TaggedHandlerPass are fully covered by unit tests, this is good to go.

xjm’s picture

Title: Revisit authentication manager service construction » Improve authentication manager service construction to support multiple service providers
Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs change record

This looks good to me, and can be accepted during the beta potentially as a followup to #2286971: Remove dependency of current_user on request and authentication manager, and also if it is indeed blocking any contrib project (the summary doesn't indicate which).

However, we need a change record (or maybe an update to an existing change record). I think we also need a somewhat clearer picture of the scope of the disruption. As far as I can tell the disruption is the following things (none too major):

  1. +++ b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php
    @@ -20,7 +20,7 @@
    -class AuthenticationManager implements AuthenticationProviderInterface, AuthenticationProviderFilterInterface, AuthenticationProviderChallengeInterface {
    +class AuthenticationManager implements AuthenticationProviderInterface, AuthenticationProviderFilterInterface, AuthenticationProviderChallengeInterface, AuthenticationManagerInterface {
    

    This adds a new interface, which theoretically could break extending implementations. However, since the method was already provided in the class, existing implementations would continue to work.

  2. +++ b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php
    @@ -65,44 +65,23 @@ class AuthenticationManager implements AuthenticationProviderInterface, Authenti
    -  public function addProvider(AuthenticationProviderInterface $provider, $id, $priority = 0) {
    ...
    +  public function addProvider(AuthenticationProviderInterface $provider, $provider_id, $priority = 0, $global = FALSE) {
    

    A parameter is being added to this method. However, the parameter is optional, so existing calls to the method would not break. Only I think any overridden implementations of it, since AFAIK PHP does not let child methods have fewer parameters than the parent.

  3. +++ b/core/modules/basic_auth/basic_auth.services.yml
    @@ -1,9 +1,9 @@
    -  authentication.basic_auth:
    +  basic_auth.authentication.basic_auth:
    
    +++ b/core/modules/user/user.services.yml
    @@ -15,11 +15,11 @@ services:
    -  authentication.cookie:
    +  user.authentication.cookie:
    

    The name of the service is being changed, so any references to it will need to change as well.

  4. +++ b/core/modules/locale/tests/modules/early_translation_test/early_translation_test.services.yml
    @@ -1,6 +1,6 @@
    -  authentication.early_translation_test:
    +  early_translation_test.authentication.early_translation_test:
    

    Test service, so OK to have a BC break.

  5. +++ b/core/modules/locale/tests/modules/early_translation_test/early_translation_test.services.yml
    @@ -1,6 +1,6 @@
    -      - { name: authentication_provider, priority: 100 }
    +      - { name: authentication_provider, provider_id: 'early_translation_test', priority: 100 }
    

    The provider ID wasn't previously part of the tag... not sure what implication that has for BC.

Did I miss anything?

If we do update an existing change record, we should be sure to toggle the published field once the issue is fixed (uncheck -> save -> recheck -> save) so that a notification goes out on the feed about the API change. (Just mentioning this since I think it's been missed recently on what's probably the same CR.)

znerol’s picture

Title: Improve authentication manager service construction to support multiple service providers » Improve authentication manager service construction to support custom global service providers
Issue summary: View changes
Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs change record +Needs change record updates

Updated the issue summary and issue title.

I think we do not need a new change record for that, but update: https://www.drupal.org/node/2453449

AFAIK we do change record updates after commit, therefore resetting to RTBC.

xjm’s picture

@znerol, we usually update the change records before commit with a note that the change is pending, like:

  1. Find the change record and add the issue to it.
  2. Add the updated information with a note the change is pending, i.e. "In [#1234567] the previous foo is removed in favor of blah".
  3. Issue committed.
  4. Remove the note if needed (often we can word them so they're correct before or after the issue goes in) and toggle the checkbox (draft -> save -> republish -> resave).

...but the process for it is not well documented and followed inconsistently. :P Thanks! And thanks for updating the disruption information with the details.

znerol’s picture

Ok, thanks. I've added the pending api changes/additions to the change record.

  • xjm committed 44e8ffd on 8.0.x
    Issue #2432585 by almaudoh, znerol, dawehner, cpj: Improve...
xjm’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Fixed

Okay, this makes sense to me, and I think the small disruption is merited to unblock the contrib modules listed.

As a contrib project blocker and a critical followup, this is a prioritized change during the beta per https://www.drupal.org/core/beta-changes. Committed and pushed to 8.0.x (with credit for reviewers). Thanks all!

znerol’s picture

Thanks. Updated and (re-)published the change record.

znerol’s picture

almaudoh’s picture

Great!! Thanks @znerol, @xjm.

Status: Fixed » Closed (fixed)

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