Problem/Motivation

All DAM operations around the site started failing with a WSOD for a specific user but no other accounts. Found the root cause was an expired (see note below) unauthorized access token via the following error when attempting to click the "Remove Acquia DAM authorization" checkbox and save user profile:

The website encountered an unexpected error. Please try again later.

GuzzleHttp\Exception\ClientException: Client error: `POST redacted domain/api/rest/oauth/logout` resulted in a `404 Not Found` response: {"error":"Not Found","description":"Access token not found."} in GuzzleHttp\Exception\RequestException::create() (line 113 of /var/www/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php).
GuzzleHttp\Middleware::GuzzleHttp\{closure}(Object) (Line: 204)
GuzzleHttp\Promise\Promise::callHandler(1, Object, NULL) (Line: 153)
GuzzleHttp\Promise\Promise::GuzzleHttp\Promise\{closure}() (Line: 48)
GuzzleHttp\Promise\TaskQueue->run(1) (Line: 248)
GuzzleHttp\Promise\Promise->invokeWaitFn() (Line: 224)
GuzzleHttp\Promise\Promise->waitIfPending() (Line: 269)
GuzzleHttp\Promise\Promise->invokeWaitList() (Line: 226)
GuzzleHttp\Promise\Promise->waitIfPending() (Line: 62)
GuzzleHttp\Promise\Promise->wait() (Line: 182)
GuzzleHttp\Client->request('post', 'redacted domain/api/rest/oauth/logout', Array) (Line: 95)
GuzzleHttp\Client->__call('post', Array) (Line: 105)
Drupal\media_acquiadam\AcquiadamAuthService::cancel('redacted token') (Line: 85)
media_acquiadam_unauthorize(Array, Object)
call_user_func_array('media_acquiadam_unauthorize', Array) (Line: 114)
Drupal\Core\Form\FormSubmitter->executeSubmitHandlers(Array, Object) (Line: 52)
Drupal\Core\Form\FormSubmitter->doSubmitForm(Array, Object) (Line: 601)
Drupal\Core\Form\FormBuilder->processForm('user_form', Array, Object) (Line: 320)
Drupal\Core\Form\FormBuilder->buildForm(Object, Object) (Line: 73)
Drupal\Core\Controller\FormController->getContentResult(Object, Object) (Line: 39)
Drupal\layout_builder\Controller\LayoutBuilderHtmlEntityFormController->getContentResult(Object, Object)
call_user_func_array(Array, Array) (Line: 123)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 564)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 124)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array) (Line: 97)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 158)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 80)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 106)
Drupal\page_cache\StackMiddleware\PageCache->pass(Object, 1, 1) (Line: 85)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 265)
Drupal\shield\ShieldMiddleware->bypass(Object, 1, 1) (Line: 132)
Drupal\shield\ShieldMiddleware->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 708)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)

Steps to reproduce

  1. Authorize Acquia DAM for an account to obtain a token.
  2. Wait for token to expire* (see note below)
  3. Attempt to use "Remove Acquia DAM authorization" functionality

I'll note here that, since the expiration time for tokens could be a while, you could approximate this behavior for testing the handling of the response error by simply passing a garbage token instead of an expired one. The response from the API does not note the token is expired, just that it is "not found". So passing a bad token would get this same response and you could then test the error handling needed, as noted below.

Proposed resolution

So, tracing this out, this issue comes from the API returning a 401 if the given token is found to be invalid. Specifically, in media_acquiadam/media_acquiadam.module in the media_acquiadam_unauthorize() function.

Relevant code:

...
 // Cancel the user token on Acquia DAM.
    $cancelled = AcquiadamAuthService::cancel($acquiadam_account['acquiadam_token']);

    // Remove the Acquia DAM data (mainly the token) from the user account.
    if ($cancelled) {
      \Drupal::service('user.data')
        ->set('media_acquiadam', $user->id(), 'account', []);
    }
...

The issue is that, with an expired* token, the cancel() call receives a 401 Unauthorized and throws an error, causing a WSOD noting that the provided access token was not found.

My temporary workaround to unblock the user was to comment out the call to the cancel() function and also the if statement around the user.data service line. Leaving only the line that clears out the related Acquia account information.

To account for this edge case, that code should be updated to handle the 401 error and determine if the "access token not found" error was the cause. If it was, the code should continue into the if($cancelled) block to remove the bad token from the user.

It is worth noting that I did not create a patch for this because I am unaware of what other cases could cause an error here and will need to be handled. This cannot simply catch a PHP error and unlink the accounts without inspecting the error reported by the API because it may be unrelated to the token, e.g. if the service is temporarily unavailable.

Update: Created a patch for this and added it below. After some more thought, it seems that within this cancel() function, a 401 error would only be caused by a bad token being used.

Alternatively, this could also be handled a level deeper with some error handling around the post() call in Drupal\media_acquiadam\AcquiadamAuthService::cancel()

Update: Now that I have patched this, catching this error in the .module file is a more appropriate and cleaner place to address this.

A Note on the token being "Expired"

My initial assumption was that the token had expired and that was causing this issue but that may be incorrect. Where we are seeing this issue is in an active dev environment where the database is still being copied and replaced between environments when necessary. Given that, this may not be caused by a token expiration but may, instead, be a function of those databases being out of sync and having different tokens.

That point is a bit moot, however, as this bug where an invalid token creates a situation where the user cannot clear their authentication and reauthenticate to get a new token should be handled.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

amitchell-p2 created an issue. See original summary.

amitchell-p2’s picture

Version: 2.0.3 » 2.x-dev
amitchell-p2’s picture

StatusFileSize
new65 bytes

This patch sets $cancelled = TRUE when the cancel function receives a 401 Unauthorized response, which implies that the token we are trying to cancel was already removed or cancelled. This avoids the WSOD and then allows the subsequent code to remove the bad token from the database.

Without this patch, a 401 Unauthorized response causes a WSOD before the token is removed from the database producing this bug. In that case, there is no way to remove the token and the user is stuck being unable to reauthorize with the DAM to receive an updated token.

amitchell-p2’s picture

Issue summary: View changes
amitchell-p2’s picture

Issue summary: View changes
chakkche’s picture

Assigned: Unassigned » chakkche

I will review this.

chakkche’s picture

Assigned: chakkche » Unassigned
Status: Active » Reviewed & tested by the community

Able to replicate this issue.

Replicating steps:
Manually modified the token and tried to remove the authorization. Getting the mentioned error.

After applying the patch able to remove the authorization.

Balu Ertl made their first commit to this issue’s fork.

baluertl’s picture

Title: Cannot Remove Acquia DAM Authorization if Token Expires, Causing WSOD For All DAM Functionality » Cannot remove Acquia DAM authorization if token expires, causing WSOD for all DAM functionality
Status: Reviewed & tested by the community » Needs review
StatusFileSize
new65 bytes

I feel it worth to clarify that the issue is about the Drupal user's authentication token (marked with green) and not the site's one (marked with red):

However, by following the described steps I could not reproduce the WSOD issue as expected on the 2.x branch. My testing steps were in order:

  1. Install the module by Composer, then enable the parent and its Example Config sub-module by Drush
  2. Configure its site-wide settings on /admin/config/media/acquiadam with valid credentials
  3. Authorize Drupal user account on /user/{UID}/edit
  4. Test the proper way of working by creating a media item based on an Image type of DAM asset
  5. The media item should have a thumbnail and its name should be the asset title (usually a file name-like string)
  6. In the users_data table of the DB find your actual user account's row. Modify its value field by replacing the alphanumeric string under the serialized acquiadam_token key to something non-sense (like 0123456789abcdef0123456789abcdef). Ensure that the complete JSON value still starts with the term “wat_…” (acronym of Widen Access Token).
  7. Flush all caches.
  8. Test the erroneous way of working by creating another media item (eg. by sticking with the Image type of DAM asset).
  9. The media item is being created in Drupal but has no thumbnail (only a general placeholder image appears instead) and its name does not resemble any filename. (This is because no metadata was available due to the disconnection from the remote DAM system)
  10. Visit Watchdog and filter for the module name. Several log messages were emitted, some of them similar to these:

    Received a missing asset response when trying to load asset f2a9c03d-3664-477c-8013-e84504ed5adc. Was the asset deleted in Acquia DAM? DAM API client returned a 401 exception code with the following message: Client error: `GET https://api.widencollective.com/v2/assets/f2a9c03d-3664-477c-8013-e84504...` resulted in a `401 Unauthorized` response:

    {
      "error": true,
      "response_code": 401,
      "error_message": "Unauthorized",
      "stack_trace": null
    }
    


    Failed to fetch asset ids: Client error: `GET https://api.widencollective.com/v2/assets/search?limit=100&offset=0&quer...` resulted in a `401 NOT AUTHORIZED` response.


    Unable to register integration link for asset f2a9c03d-3664-477c-8013-e84504ed5adc. Exception message: Client error: `POST https://laser.widencollective.com/api/rest/integrationlink` resulted in a `401 Unauthorized` response:

    {
      "error":"invalid_request",
      "description":"No access token."
    }
    
  11. Switching back the valid “wat_…” string in your user account's record of the DB (and flushing caches again) everything should work normal.

Conclusion:

Because this module is being declared as “Minimally maintained & No further development” I would refrain from introducing changes on this scale in such cardinal logic as authentication is. Two factors, a two-year-old issue and an active development of an external system (the Widen software behind the Acquia DAM service) together can easily result that this issue is not present anymore. The decision is of course up to the module maintainers.

baluertl’s picture

Status: Needs review » Postponed (maintainer needs more info)
baluertl’s picture

baluertl’s picture

baluertl’s picture

Update: I just realised that my testing procedure described in #9 above is unrelated to this original issue starting from step #4. The unauthentication link needs to be clicked on the /user/{UID}/edit page instead, and then WSOD happens indeed. Sorry for the confuse.

baluertl’s picture

Status: Postponed (maintainer needs more info) » Needs review

japerry made their first commit to this issue’s fork.

  • japerry committed 8d98cc91 on 2.x authored by Balu Ertl
    Issue #3301392: Cannot remove Acquia DAM authorization if token expires...
japerry’s picture

Status: Needs review » Fixed

Status: Fixed » Closed (fixed)

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