While the module provides a JSON source plugin for remote content, it does not support JWT authentication, which is common on many systems.

Add a new authentication plugin which allows JWT transactions.

It should accept several values:

  • The URL to request authentication from.
  • The client ID value to pass.
  • The client secret value to pass.
  • The name of the return value that stores the token, e.g. "clientToken".
  • The name of the query argument to pass on all subsequent requests with the token, e.g. "x-api-token".

Disclaimer:
This is to work with a 3rd party API that requires an authentication process which it calls "JWT Authentication", but it doesn't match the JWT standard. As such I'm not sure if this should even be called "JWT"?

Comments

DamienMcKenna created an issue. See original summary.

damienmckenna’s picture

Title: Provide a source plugin that allows JWT authentication » Provide a JWT authentication
Issue summary: View changes
damienmckenna’s picture

Title: Provide a JWT authentication » Provide a JWT authentication plugin
damienmckenna’s picture

Issue summary: View changes
merauluka’s picture

StatusFileSize
new2.21 KB

@DamienMcKenna From my review of that endpoint, it appears to just be using a client id and client secret to generate a token that can be used in Authorization headers for future requests. That being said, it doesn't appear to be using JWT at all.

I would suggest making this a RESTAuth plugin instead so the implementation can be a bit more generic.

Attached is my first stab at it. This hasn't been tested and needs review.

damienmckenna’s picture

Status: Active » Needs review

That's pretty fantastic, thanks @merauluka!

Let's have the testbot give it a look-see.

I think one improvement would be to improve the class' docblock to provide an example of how to use it.

markie’s picture

+    catch (Exception $e) {
+      $error = TRUE;
+      watchdog_exception('error', $e);

Shouldn't this be `$e->getMessage()`? $e is a Class...

markie’s picture

StatusFileSize
new0 bytes

Updated patch to fix issue with Exception->getMessage, and added documentation. Looking at my specific use case, the resulting auth header is not 'Authorization: Bearer $token' so added the ability to configure the response_header and response_prefix. Still don't have access to the api that I want to use this on, so it's still pretty much a WIP.

markie’s picture

StatusFileSize
new3.49 KB

Adding file with actual changes.. Sorry kids.

merauluka’s picture

One thing I'm wondering @markie, is if the logger can be updated to use dependency injection instead of calling it statically.

damienmckenna’s picture

This is the code that ended up working:


namespace Drupal\mymodule\Plugin\migrate_plus\authentication;

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Site\Settings;
use Drupal\migrate\MigrateException;
use Drupal\migrate_plus\AuthenticationPluginBase;
use GuzzleHttp\Client;
use Exception;

/**
 * Provides authentication for the EMS resource.
 *
 * To set this migration up properly, a few configuration items are required.
 *
 * The "auth_host" will usually be the following:
 *   https://example.com/EmsPlatformServices/api/v1/clientauthentication
 *
 * The "urls" value for the feed will usually be something like the following:
 *   https://example.com/EmsPlatformServices/api/v1/buildings
 *   https://example.com/EmsPlatformServices/api/v1/rooms
 *
 * id: migrate_using_REST_Authentication
 * label: 'REST Authentication Plugin example'
 * migration_tags: null
 * migration_group: migrate_group
 * source:
 *   plugin: url
 *   # Specifies the http fetcher plugin.
 *   data_fetcher_plugin: http
 *   authentication:
 *     plugin: ems_auth
 *     auth_host: xxx
 *     client_id: xxx
 *     secret: xxx
 *   headers:
 *     Accept: 'application/json; charset=utf-8'
 *     Content-Type: 'application/json'
 *   # One or more URLs from which to fetch the source data.
 *   urls: https:/...some.source
 *   # Specifies the JSON parser plugin.
 *   data_parser_plugin: json
 *   <<<<CONTINUES>>>>
 *
 * The auth credentials may also be stored in a global setting in the site's
 * settings.php file as follows:
 *
 * $settings['ems_auth'] = [
 *   'client_id' => 'THE CLIENT ID',
 *   'secret' => 'THE SECRET',
 *   'auth_host' => 'https://example.com/.../clientauthentication',
 * ];
 *
 * @Authentication(
 *   id = "ems_auth",
 *   title = @Translation("EMS Authorization Token")
 * )
 */
class EmsAuth extends AuthenticationPluginBase implements ContainerFactoryPluginInterface {

  /**
   * {@inheritdoc}
   */
  public function getAuthenticationOptions() {
    // Try loading the auth configuration from a settings line.
    $settings = Settings::get('ems_auth', []);
    if (!empty($settings['client_id']) && !empty($settings['secret']) && !empty($settings['auth_host'])) {
      $client_id = $settings['client_id'];
      $secret = $settings['secret'];
      $auth_host = $settings['auth_host'];
    }
    // Try loading the auth configuration from the migration plugin.
    elseif (isset($this->configuration['client_id'], $this->configuration['secret'], $this->configuration['auth_host'])) {
      $client_id = $this->configuration['client_id'];
      $secret = $this->configuration['secret'];
      $auth_host = $this->configuration['auth_host'];
    }
    // If no auth conditions found fail.
    else {
      throw new MigrateException('The authentication "auth_host", "client_id" and "secret" values must be set for this connection.');
    }

    $client = new Client();
    $error = FALSE;
    try {
      $token_response = $client->request('POST', $auth_host, [
        'json' => [
          "clientId" => $client_id,
          "secret" => $secret,
        ],
      ]);
      if ($token_response->getStatusCode() == 200) {
        $token_response_array = json_decode($token_response->getBody(), TRUE);
      }
      else {
        $error = TRUE;
        $error_response = [
          'code' => $token_response->getStatusCode(),
          'reason' => $token_response->getReasonPhrase(),
          'payload' => json_decode($token_response->getBody(), TRUE),
        ];
        \Drupal::logger('migrate_plus')->error('Attempt to retrieve authorization token failed. Detail: <pre>@detail</pre>', [
          '@detail' => print_r($error_response),
        ]);
      }
    }
    catch (Exception $e) {
      $error = TRUE;
      watchdog_exception('error', $e, $e->getMessage());
    }

    if ($error || empty($token_response_array['clientToken'])) {
      throw new MigrateException('Error occurred while retrieving authorization token for client "' . $secret . '"');
    }

    return [
      'headers' => [
        'x-ems-api-token' => $token_response_array['clientToken'],
      ],
    ];
  }

}

I think it could be refactored to match the original request.