Skip to content

1.6 version incompatible with simple_oauth 6.x #851

Open
@thomjjames

Description

@thomjjames

Package containing the bug

next (Drupal module)

Describe the bug

A clear and concise description of what the bug is.

simple_oauth 6.x contains breaking changes to the /oauth/token route which appear not to work with next-drupal 1.6 and maybe version 2.x as well. This results in previews returning 500 errors to the end user when DRUPAL_CLIENT_ID and DRUPAL_CLIENT_SECRET authentication.

The Drupal logs contain warnings like:
The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Hint: Check the `client_id` parameter.

simple_oauth/src/Controller/Oauth2Token.php:

public function token(Request $request): ResponseInterface {
    $server_request = $this->httpMessageFactory->createRequest($request);
    $server_response = new Response();
    $client_id = $request->get('client_id');
    $grant_type = $request->get('grant_type');
    $scopes = $request->get('scope');

    $lock_key = $this->createLockKey($request);

    try {
      // Try to acquire the lock.
      while (!$this->lock->acquire($lock_key)) {
        // If we can't acquire the lock, wait for it.
        if ($this->lock->wait($lock_key)) {
          // Timeout reached after 30 seconds.
          throw OAuthServerException::accessDenied('Request timed out. Could not acquire lock.');
        }
      }

      if (empty($client_id)) {
        throw OAuthServerException::invalidRequest('client_id');
      }
      $client_entity = $this->clientRepository->getClientEntity($client_id);
      if (empty($client_entity)) {
        throw OAuthServerException::invalidClient($server_request);
      }
      $client_drupal_entity = $client_entity->getDrupalEntity();

      // Omitting scopes is not allowed when dealing with client_credentials
      // and no default scopes are set.
      if (
        $grant_type === 'client_credentials' &&
        empty($scopes) &&
        $client_drupal_entity->get('scopes')->isEmpty()
      ) {
        throw OAuthServerException::invalidRequest('scope');
      }

      // Respond to the incoming request and fill in the response.
      $server = $this->authorizationServerFactory->get($client_drupal_entity);
      $response = $server->respondToAccessTokenRequest($server_request, $server_response);
    }
    catch (OAuthServerException $exception) {
      $this->logger->log(
        $exception->getCode() < 500 ? LogLevel::NOTICE : LogLevel::ERROR,
        $exception->getMessage() . ' Hint: ' . $exception->getHint() . '.'
      );
      $response = $exception->generateHttpResponse($server_response);
    }
    finally {
      // Release the lock.
      $this->lock->release($lock_key);
    }

    return $response;
  }

next-drupal package client:

async getAccessToken(
    opts?: DrupalClientAuthClientIdSecret
  ): Promise<AccessToken> {
    if (this.accessToken && this.accessTokenScope === opts?.scope) {
      return this.accessToken
    }

    if (!opts?.clientId || !opts?.clientSecret) {
      if (typeof this._auth === "undefined") {
        throw new Error(
          "auth is not configured. See https://next-drupal.org/docs/client/auth"
        )
      }
    }

    if (
      !isClientIdSecretAuth(this._auth) ||
      (opts && !isClientIdSecretAuth(opts))
    ) {
      throw new Error(
        `'clientId' and 'clientSecret' required. See https://next-drupal.org/docs/client/auth`
      )
    }

    const clientId = opts?.clientId || this._auth.clientId
    const clientSecret = opts?.clientSecret || this._auth.clientSecret
    const url = this.buildUrl(opts?.url || this._auth.url || DEFAULT_AUTH_URL)

    if (
      this.accessTokenScope === opts?.scope &&
      this._token &&
      Date.now() < this.tokenExpiresOn
    ) {
      this._debug(`Using existing access token.`)
      return this._token
    }

    this._debug(`Fetching new access token.`)

    const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64")

    let body = `grant_type=client_credentials`

    if (opts?.scope) {
      body = `${body}&scope=${opts.scope}`

      this._debug(`Using scope: ${opts.scope}`)
    }

    const response = await this.fetch(url.toString(), {
      method: "POST",
      headers: {
        Authorization: `Basic ${basic}`,
        Accept: "application/json",
        "Content-Type": "application/x-www-form-urlencoded",
      },
      body,
    })

    if (!response?.ok) {
      await this.handleJsonApiErrors(response)
    }

    const result: AccessToken = await response.json()

    this._debug(result)

    this.token = result

    this.accessTokenScope = opts?.scope

    return result
  }

Seems like the simple_oauth module expects client_id in the body and that scope is required but next-drupal sends the client_id & client_secret as basic auth and I believe scope is optional (?).

composer.json (https://git.drupalcode.org/project/next/-/blob/1.0.x/composer.json?ref_type=heads#L17) has "drupal/simple_oauth": "^5.0 || ^6.0" which allows the module to be upgraded to 6.x version.

Expected behavior

client_id should be passed in the body and perhaps the basic auth isn't needed although I believe it's ok in the OAuth2 spec (?).

Steps to reproduce:

  1. Set up a next-drupal project with preview mode as per https://v1-6.next-drupal.org/learn/preview-mode with the oauth authentication using the simple_oauth module (https://v1-6.next-drupal.org/learn/preview-mode/create-oauth-client)
  2. Then create content in an entity type rendered by Next (https://v1-6.next-drupal.org/learn/preview-mode/configure-content-types)
  3. View the content and the preview iframe should show a 500 error and the Drupal logs contain simple_oauth warning messages

Additional context

Had this happen on 2 sites when upgrading from simple_oauth 5.x to 6.x, the workaround we used was to switch to basic_auth authentication instead since the simple_oauth upgrade contained database updates which made it harder to rollback.

I guess this can be "fixed" either by updating getAccessToken or in the shorter term changing "drupal/simple_oauth": "^5.0 || ^6.0" to "drupal/simple_oauth": "^5.0"

This was my first dip into the inner workings of the module & package so apologies if any of my assumptions are incorrect :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriageA new issue that needs triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions