Problem/Motivation

As it's implemented the negotiator Drupal\consumers\Negotiator is looking on either a custom header X-Consumer-ID or custom query argument _consumer_id

For any 3rd party module that will require this functionality, the discovery process can not be extended.

I am currently using simple_oauth that is working with consumers internally and logging a user, giving roles from the consumer and everything works. My issue at the moment is that the negotiator is not discovering the consumer that was activated / used by the authentication mechanism.

Sending the client ID in a request seems redundant, as the data needed is already part of the request tokens and it was already processed, so the simple_oauth can expose that information somehow.

Proposed resolution

Option 1: Introduce a plugin-based system for negotiators. Once a module get's activated, it's plug-in will provide client ID. First to pass a client ID wins. Plugins should have a priority value set, so some ordering can be enforced.

Option 2: Have a new interface added, so we can implement negotiator decorators. Any module that wants to change the behavior can implement a decorator over the interface. This will allow granular control over the behavior and execution order of the negotiators. Though it might give bigger permissions on implementing modules.

I prefer option 1, as more easy to scale-out, following many core examples in the matter.

Any other ideas will be appreciated.

Remaining tasks

Discussion (is this needed or not)
Decide on an approach.
Patch, etc...

User interface changes

None.

API changes

API addition, internal refactoring.
What is now a hard-coded negotiation logic will become a new extensible sub-system.

Data model changes

None.

Comments

ndobromirov created an issue. See original summary.

vtcore’s picture

+1

ndobromirov’s picture

Status: Needs work » Needs review
Issue tags: +DX (Developer Experience)
StatusFileSize
new10.13 KB

Here is a POC with a plug-in system for the negotiators.

vtcore’s picture

+++ b/src/Plugin/ConsumerNegotiator/QueryNegotiator.php
@@ -0,0 +1,26 @@
+ *   priority = 5

Shouldn't priorities of the two Negotiators be different?

ndobromirov’s picture

StatusFileSize
new10.13 KB
new612 bytes

Never do copy-paste :D. Yes they should be different, as the original order should be enforced.
First the header and then the query negotiator.

e0ipso’s picture

I'm hesitant to introduce flexibility that needs to be maintained further down the line. Can you highlight how this would work with simple_oauth? I may be willing to introduce simple_oauth's needs as the 3rd hard-coded scenario.

ndobromirov’s picture

Well simple_oauth is having the meta-data available to get the consumer UUID from the token being passed in.
I do not want to send consumer UUID on every request, as the data is already present and used during authentication.
Here is the decorator that solved it for me, changing simple_oauth behavior...

The main part is I am setting the header dynamically just after the successful authentication, so existing negotiators will kick in.

namespace Drupal\my_module;

use Drupal\simple_oauth\Authentication\Provider\SimpleOauthAuthenticationProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Class OauthProviderDecorator.
 *
 * @package Drupal\my_module
 */
class SimpleOauthProviderDecorator implements SimpleOauthAuthenticationProviderInterface {
  /**
   * Provider to decorate.
   *
   * @var \Drupal\simple_oauth\Authentication\Provider\SimpleOauthAuthenticationProviderInterface
   */
  private $decorated;

  /**
   * Symphony's request stack service (current request).
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  private $requestStack;

  /**
   * SimpleOauthProviderDecorator constructor.
   *
   * @param \Drupal\simple_oauth\Authentication\Provider\SimpleOauthAuthenticationProviderInterface $decorated
   *   Decorated provider instance.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   Symphony's request stack service (current request).
   */
  public function __construct(SimpleOauthAuthenticationProviderInterface $decorated, RequestStack $request_stack) {
    $this->decorated = $decorated;
    $this->requestStack = $request_stack;
  }

  /**
   * {@inheritdoc}
   *
   * @see \Drupal\consumers\Negotiator::negotiateFromRequest()
   */
  public function authenticate(Request $request) {
    /* @var $result \Drupal\simple_oauth\Authentication\TokenAuthUser */
    if (NULL === $result = $this->decorated->authenticate($request)) {
      return NULL;
    }

    // Hijack the success authentication flow to access the currently active
    // consumer's uuid. Pass it as a new dynamic header on the current request,
    // so we can fool Negotiator::negotiateFromRequest detection.
    /** @var \Drupal\consumers\Entity\Consumer $client */
    $client = $result->getToken()->get('client')->entity;
    $request = $this->requestStack->getCurrentRequest();
    $request->headers->set('X-Consumer-ID', $client->uuid());

    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function hasTokenValue(Request $request) {
    return $this->decorated->hasTokenValue($request);
  }

  /**
   * {@inheritdoc}
   */
  public function applies(Request $request) {
    return $this->decorated->applies($request);
  }

}
e0ipso’s picture

Title: Drupal\consumers\Negotiator is not extensible. » Set the 'X-Consumer-ID' header on successful authentication
Project: Consumers » Simple OAuth (OAuth2) & OpenID Connect
Version: 8.x-1.0-beta2 » 8.x-3.x-dev
Status: Needs review » Needs work

I think this is a very valid approach. This would be a great addition to the Simple OAuth module. The authentication code could set the header there.

Moving to the other issue queue.

ndobromirov’s picture

Related issues: +#2962050: Strange interface

This is related: #2962050: Strange interface.

ndobromirov’s picture

So your suggestion at the moment is to put the decorator directly in the authentication service.

ndobromirov’s picture

Status: Needs work » Needs review
StatusFileSize
new2.66 KB

Here is a patch to resolve that directly on the auth service.

Changes:
- Marked the interface as internal (same as the only class implementing it).
- New accessor method added on the interface and TokenAuthUser class.
- Set the header on successful authentication.

  • e0ipso committed 0cbbe60 on 8.x-3.x authored by ndobromirov
    Issue #2961782 by ndobromirov, vtcore, e0ipso: Set the '\''X-Consumer-ID...
e0ipso’s picture

Status: Needs review » Fixed

Thanks! I am pretty sure test fails are unrelated.

e0ipso’s picture

Also, it goes without saying. Thanks for the fantastic job.

fy1128’s picture

Get error like this:

The website encountered an unexpected error. Please try again later.
ng>Error: Call to a member function uuid() on null in Drupal\simple_oauth\Authentication\Provider\SimpleOauthAuthenticationProvider->authenticate() (line 81 of modules/contrib/simple_oauth/src/Authentication/Provider/SimpleOauthAuthenticationProvider.php).

seems like getConsumer() return null.

ndobromirov’s picture

It should be impossible to get a null, as the customer is set only when there is a valid consumer found in this method.
The rest is just property access.

/**
   * Constructs a TokenAuthUser object.
   *
   * @param \Drupal\simple_oauth\Entity\Oauth2TokenInterface $token
   *   The underlying token.
   *
   * @throws \Exception
   *   When there is no user.
   */
  public function __construct(Oauth2TokenInterface $token) {
    if (!$this->subject = $token->get('auth_user_id')->entity) {
      /** @var \Drupal\consumers\Entity\Consumer $client */
      if ($client = $token->get('client')->entity) {
        $this->subject = $client->get('user_id')->entity;
      }
    }
    if (!$this->subject) {
      throw OAuthServerException::invalidClient();
    }
    $this->token = $token;
    $this->consumer = $client;
  }

If you are getting an error as stated in the comment i think we need more info to resolve it...

fy1128’s picture

I just did some tests. found

$token->get('auth_user_id')->entity

got a 'Drupal\user\Entity\User' object,

so

!$this->subject = $token->get('auth_user_id')->entity

would be always false.

Here was the example request post by postman:

curl -X GET \
  http://localhost/api/testapi \
  -H 'Authorization: Bearer mytoken' \
  -H 'Cache-Control: no-cache' \
  -H 'Postman-Token: 8530f8b4-6019-45c5-9034-e49f6bdf17a0'
ndobromirov’s picture

Category: Task » Bug report
Status: Fixed » Needs review
Issue tags: +Needs tests
StatusFileSize
new1.12 KB

Yea, there was the underlying issue that there was not a client instance in all cases. Here is a patch that should resolve the issue.

On top of that it shows that we have missing test coverage in case where user is set on consumer level, resulting in somewhat different authentication process (unified to a big extent now).

司南’s picture

  public function __construct(Oauth2TokenInterface $token) {

      /** @var \Drupal\consumers\Entity\Consumer $client */
      $client = $token->get('client')->entity;

    if (!$this->subject = $token->get('auth_user_id')->entity) {
      if ($client) {
        $this->subject = $client->get('user_id')->entity;
      }
    }
    if (!$this->subject) {
      throw OAuthServerException::invalidClient();
    }
    $this->token = $token;
    $this->consumer = $client;
  }

yes, get the error too, hope to fix it soon.

ndobromirov’s picture

Are you using the patch, as I am seeing differences...
If there is no $client, you should be getting OAuthServerException in that case.

ndobromirov’s picture

  1. Here is a tweak that adds support for NULL consumers. Note that negotiators (needed by https://www.drupal.org/project/consumer_image_styles) will not trigger at that point.
  2. @e0ipso, is there a case, where the token will have NO consumer associated with it? As I see it consumer is crucial part of a token, so why are they getting NULL values at all is a more correct question for further investigation...
e0ipso’s picture

@e0ipso, is there a case, where the token will have NO consumer associated with it? As I see it consumer is crucial part of a token, so why are they getting NULL values at all is a more correct question for further investigation...

I am also confused by this. I don't see how that could be the case.

lawxen’s picture

I got the same error of #15

The website encountered an unexpected error. Please try again later.
<br />
<em class="placeholder">Error</em>: Call to a member function uuid() on null in
<em class="placeholder">Drupal\simple_oauth\Authentication\Provider\SimpleOauthAuthenticationProvider-&gt;authenticate()</em> (line
<em class="placeholder">81</em> of
<em class="placeholder">modules/contrib/simple_oauth/src/Authentication/Provider/SimpleOauthAuthenticationProvider.php</em>).
<pre class="backtrace">Drupal\simple_oauth\Authentication\Provider\SimpleOauthAuthenticationProvider-&gt;authenticate(Object) (Line: 52)
Drupal\Core\Authentication\AuthenticationManager-&gt;authenticate(Object) (Line: 78)
Drupal\Core\EventSubscriber\AuthenticationSubscriber-&gt;onKernelRequestAuthenticate(Object, &#039;kernel.request&#039;, Object)
call_user_func(Array, Object, &#039;kernel.request&#039;, Object) (Line: 111)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher-&gt;dispatch(&#039;kernel.request&#039;, Object) (Line: 127)
Symfony\Component\HttpKernel\HttpKernel-&gt;handleRaw(Object, 1) (Line: 68)
Symfony\Component\HttpKernel\HttpKernel-&gt;handle(Object, 1, 1) (Line: 67)
Drupal\simple_oauth\HttpMiddleware\BasicAuthSwap-&gt;handle(Object, 1, 1) (Line: 57)
Drupal\Core\StackMiddleware\Session-&gt;handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\KernelPreHandle-&gt;handle(Object, 1, 1) (Line: 99)
Drupal\page_cache\StackMiddleware\PageCache-&gt;pass(Object, 1, 1) (Line: 78)
Drupal\page_cache\StackMiddleware\PageCache-&gt;handle(Object, 1, 1) (Line: 40)
Drupal\jsonapi\StackMiddleware\FormatSetter-&gt;handle(Object, 1, 1) (Line: 47)
e0ipso’s picture

@caseylau When does it happen? What are you doing to trigger it?

lawxen’s picture

StatusFileSize
new267.61 KB
new470.69 KB

@e0ipso Happened on requesting jsonapi when using simple_oauth.
All works well until we updated our site to drupal8.5.3 and simple_oauth to last dev version(version: 20 Apr 2018 version) yesterday.

ndobromirov’s picture

Ok you are getting a token and an exception...
- How are the consumers configured?
- Any other modules that interact with simple_oauth?
- Anything specific to allow us to reproduce the issue?

br0ken’s picture

berdir’s picture

Status: Needs review » Fixed

Yeah, setting back to fixed, if someone still has a problem that is not fixed by updating to the latest version then I would suggest opening a new issue. The new call here just exposed the problem, it didn't really cause it.

Status: Fixed » Closed (fixed)

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