Commit 9b3fefd9 authored by Bojan Bogdanovic's avatar Bojan Bogdanovic
Browse files

Enhanced/updated consumer entity

• Migrated settings to the consumer entity; so that there is more flexiblity.
• Removed the “use_implicit” setting; it’s no longer recommended by the OAuth2 spec.
• Added hook_updates for installing/updating/removing BaseFields on the “consumer” and “oauth2_token” entity.
• Disabled translation on BaseFields that should not be translatable on the “consumer” entity.
• Introduced custom field type for referencing to OAuth2 scopes; it can reference to static or dynamic scopes dependent on the active scope provider.
• Introduced custom validation constraint for the “Redirects” BaseField and using string as field type; the uri field type does not support custom URL schemes, this is the reason why string is used as field type. The validation constraint allows more than scoped in the issue, because local domains can differ alot.
• Added custom validation constraint for the “oauth2_scope_reference” field type; so that non-existing scopes can’t be referenced.
• The “third_party” BaseField is defined on in the consumers module; leaving it for now.
• Enforcing PKCE when client is public and Authorisation Code grant type is active.
• Vertical tabs don’t work properly yet with states, wrote todo to pickup it up when the following issue gets fixed: https://www.drupal.org/project/drupal/issues/1148950.
• Made “scope_provider” setting disabled when there are scopes referenced in consumers.
• Removed “ContainerFactoryPluginInterface” from the “Oauth2GrantBase”; not all plugins need dependency injection.
• Removed Oauth2Grant plugins (and associated tests) that are no longer recommended; this way they can’t be selected anymore from the consumer entity or dynamic/static scope, this related to issue: #3261247.
parent 4827a676
Loading
Loading
Loading
Loading
+0 −5
Original line number Diff line number Diff line
scope_provider: Drupal\simple_oauth\Entity\Oauth2ScopeEntityAdapter
access_token_expiration: 300
authorization_code_expiration: 300
refresh_token_expiration: 1209600
remember_clients: true
token_cron_batch_size: 0
use_implicit: false
disable_openid_connect: false
+0 −19
Original line number Diff line number Diff line
@@ -75,18 +75,6 @@ simple_oauth.settings:
    scope_provider:
      type: string
      label: 'Scope provider'
    access_token_expiration:
      type: integer
      label: 'Access Token Expiration Time'
      description: 'The default period in seconds while a access token is valid'
    authorization_code_expiration:
      type: integer
      label: 'Authorization Code Expiration Time'
      description: 'The default period in seconds while an authorization code is valid'
    refresh_token_expiration:
      type: integer
      label: 'Refresh Token Expiration Time'
      description: 'The default period in seconds while a refresh token is valid'
    token_cron_batch_size:
      type: integer
      label: 'Token batch size'
@@ -99,13 +87,6 @@ simple_oauth.settings:
      type: path
      label: 'Private Key'
      description: 'The path to the private file.'
    remember_clients:
      type: boolean
      label: 'Remember clients'
    use_implicit:
      type: boolean
      label: 'Enable the implicit grant?'
      description: 'Only use the implicit grant if you understand the security implications of using it.'
    disable_openid_connect:
      type: boolean
      label: 'Disable OpenID Connect?'
+120 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@

use Drupal\Core\Config\FileStorage;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\simple_oauth\Entity\Oauth2ScopeEntityAdapter;
use Drupal\simple_oauth\Plugin\Oauth2GrantManager;
@@ -152,3 +153,122 @@ function simple_oauth_update_8603() {
  $entity_type = $type_manager->getDefinition('oauth2_scope');
  \Drupal::entityDefinitionUpdateManager()->installEntityType($entity_type);
}

/**
 * Install/update/delete BaseFields for the consumer entity.
 */
function simple_oauth_update_8604() {
  $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
  $entity_type_id = 'consumer';
  $provider = 'simple_oauth';

  // Add new fields.
  $new_field_definitions['grant_types'] = BaseFieldDefinition::create('list_string')
    ->setLabel(new TranslatableMarkup('Grant types'))
    ->setDescription(new TranslatableMarkup('Enabled grant types.'))
    ->setRevisionable(TRUE)
    ->setTranslatable(FALSE)
    ->setRequired(TRUE)
    ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
  $new_field_definitions['scopes'] = BaseFieldDefinition::create('oauth2_scope_reference')
    ->setLabel(new TranslatableMarkup('Scopes'))
    ->setDescription(new TranslatableMarkup('Here you can select scopes that would be the default scopes when authorizing.'))
    ->setRevisionable(TRUE)
    ->setTranslatable(FALSE)
    ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
  $new_field_definitions['automatic_authorization'] = BaseFieldDefinition::create('boolean')
    ->setLabel(new TranslatableMarkup('Automatic authorization'))
    ->setDescription(new TranslatableMarkup('This will cause the authorization form to be skipped.'))
    ->setRevisionable(TRUE)
    ->setTranslatable(FALSE)
    ->setDefaultValue(FALSE);
  $new_field_definitions['remember_approval'] = BaseFieldDefinition::create('boolean')
    ->setLabel(new TranslatableMarkup('Remember previous approval'))
    ->setDescription(new TranslatableMarkup('When enabled, if previous authorization request with the same scopes got approved, authorization will be automatically accepted.'))
    ->setRevisionable(TRUE)
    ->setTranslatable(FALSE)
    ->setDefaultValue(TRUE);
  $new_field_definitions['access_token_expiration'] = BaseFieldDefinition::create('integer')
    ->setLabel(new TranslatableMarkup('Access token expiration time'))
    ->setDescription(new TranslatableMarkup('The number of seconds that the access token will be valid.'))
    ->setRevisionable(TRUE)
    ->setTranslatable(FALSE)
    ->setRequired(TRUE)
    ->setSetting('unsigned', TRUE)
    ->setDefaultValue(300);
  $new_field_definitions['refresh_token_expiration'] = BaseFieldDefinition::create('integer')
    ->setLabel(new TranslatableMarkup('Refresh token expiration time'))
    ->setDescription(new TranslatableMarkup('The number of seconds that the refresh token will be valid.'))
    ->setRevisionable(TRUE)
    ->setTranslatable(FALSE)
    ->setRequired(TRUE)
    ->setSetting('unsigned', TRUE)
    ->setDefaultValue(1209600);

  foreach ($new_field_definitions as $field_name => $field_definition) {
    $entity_definition_update_manager->installFieldStorageDefinition($field_name, $entity_type_id, $provider, $field_definition);
  }

  // Update fields.
  $update_field_definitions['confidential'] = $entity_definition_update_manager->getFieldStorageDefinition('confidential', $entity_type_id);
  $update_field_definitions['confidential']->setTranslatable(FALSE);
  $update_field_definitions['redirect'] = $entity_definition_update_manager->getFieldStorageDefinition('redirect', $entity_type_id);
  $update_field_definitions['redirect']
    ->setLabel(new TranslatableMarkup('Redirect URIs'))
    ->setDescription(new TranslatableMarkup('The absolute URIs to validate against.'))
    ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
    ->setRequired(TRUE)
    ->addConstraint('Oauth2RedirectUri');
  $update_field_definitions['pkce'] = $entity_definition_update_manager->getFieldStorageDefinition('pkce', $entity_type_id);
  $update_field_definitions['pkce']->setTranslatable(FALSE);

  foreach ($update_field_definitions as $field_definition) {
    $entity_definition_update_manager->updateFieldStorageDefinition($field_definition);
  }

  // Remove field.
  $roles_field_definition = $entity_definition_update_manager->getFieldStorageDefinition('roles', $entity_type_id);
  $entity_definition_update_manager->uninstallFieldStorageDefinition($roles_field_definition);
}

/**
 * Install/delete scopes BaseField for the oauth2_token entity.
 */
function simple_oauth_update_8605() {
  $field_name = 'scopes';
  $entity_type_id = 'oauth2_token';
  $provider = 'simple_oauth';

  $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
  $field_definition = $entity_definition_update_manager->getFieldStorageDefinition($field_name, $entity_type_id);
  // Remove existing scopes field.
  $entity_definition_update_manager->uninstallFieldStorageDefinition($field_definition);

  // Install new scopes field.
  $field_definition = BaseFieldDefinition::create('oauth2_scope_reference')
    ->setLabel(t('Scopes'))
    ->setDescription(t('The scopes for this Access Token.'))
    ->setRevisionable(TRUE)
    ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
    ->setTranslatable(FALSE);
  $entity_definition_update_manager->installFieldStorageDefinition($field_name, $entity_type_id, $provider, $field_definition);
}

/**
 * Migrate OAuth2 settings to the consumer entity.
 */
function simple_oauth_update_8606() {
  $config = \Drupal::configFactory()->get('simple_oauth.settings');
  $remember_clients = (bool) $config->get('remember_clients');
  $access_token_expiration = $config->get('access_token_expiration');
  $refresh_token_expiration = $config->get('refresh_token_expiration');
  $consumer_storage = \Drupal::entityTypeManager()->getStorage('consumer');

  foreach ($consumer_storage->loadMultiple() as $consumer) {
    $consumer->set('remember_approval', $remember_clients);
    $consumer->set('access_token_expiration', $access_token_expiration);
    $consumer->set('refresh_token_expiration', $refresh_token_expiration);
  }

  $consumer->save();
}
+174 −64
Original line number Diff line number Diff line
@@ -13,10 +13,6 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\consumers\Entity\Consumer;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\simple_oauth\Entity\Oauth2ScopeEntityAdapter;
use Drupal\user\RoleInterface;
use Drupal\Core\Link;

/**
 * Implements hook_cron().
@@ -72,60 +68,96 @@ function simple_oauth_entity_base_field_info(EntityTypeInterface $entity_type) {
      ->setDisplayOptions('view', [
        'label' => 'above',
        'type' => 'string',
        'weight' => -3,
        'weight' => 1,
      ])
      ->setDisplayOptions('form', [
        'type' => 'options_buttons',
        'weight' => -3,
        'weight' => 1,
      ]);

    $fields['confidential'] = BaseFieldDefinition::create('boolean')
      ->setLabel(new TranslatableMarkup('Is Confidential?'))
      ->setDescription(new TranslatableMarkup('A boolean indicating whether the client secret needs to be validated or not.'))
      ->setDescription(new TranslatableMarkup('A boolean indicating whether the client is confidential or public.'))
      ->setDisplayOptions('view', [
        'label' => 'inline',
        'type' => 'boolean',
        'weight' => 3,
        'weight' => 4,
      ])
      ->setDisplayOptions('form', [
        'weight' => 3,
        'weight' => 4,
      ])
      ->setRevisionable(TRUE)
      ->setTranslatable(TRUE)
      ->setTranslatable(FALSE)
      ->setDefaultValue(TRUE);

    $fields['roles'] = BaseFieldDefinition::create('entity_reference')
    $fields['scopes'] = BaseFieldDefinition::create('oauth2_scope_reference')
      ->setLabel(new TranslatableMarkup('Scopes'))
      ->setDescription(new TranslatableMarkup('The roles for this Consumer. OAuth2 scopes are implemented as Drupal roles.'))
      ->setRevisionable(TRUE)
      ->setSetting('target_type', 'user_role')
      ->setSetting('handler', 'default')
      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
      ->setTranslatable(FALSE)
      ->setDescription(new TranslatableMarkup('Here you can select scopes that would be the default scopes when authorizing.'))
      ->setDisplayOptions('view', [
        'label' => 'inline',
        'type' => 'entity_reference_label',
        'weight' => 5,
        'type' => 'oauth2_scope_reference_label',
        'weight' => 0,
      ])
      ->setDisplayOptions('form', [
        'type' => 'options_buttons',
        'weight' => 5,
      ]);
    $fields['redirect'] = BaseFieldDefinition::create('uri')
      ->setLabel(new TranslatableMarkup('Redirect URI'))
      ->setDescription(new TranslatableMarkup('The URI this client will redirect to when needed.'))
        'type' => 'oauth2_scope_reference',
        'weight' => 0,
      ])
      ->setRevisionable(TRUE)
      ->setTranslatable(FALSE)
      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);

    // Uri does not allow custom URL scheme, that's why we use string.
    $fields['redirect'] = BaseFieldDefinition::create('string')
      ->setLabel(new TranslatableMarkup('Redirect URIs'))
      ->setDescription(new TranslatableMarkup('The absolute URIs to validate against.'))
      ->setDisplayOptions('view', [
        'label' => 'inline',
        'weight' => 4,
        'weight' => 5,
      ])
      ->setDisplayOptions('form', [
        'weight' => 4,
        'weight' => 5,
      ])
      ->setDisplayConfigurable('view', TRUE)
      ->setTranslatable(TRUE)
      ->setRequired(TRUE)
      // todo: Cardinality should be set to: 10, while having the "Add another item" functionality.
      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
      // URIs are not length limited by RFC 2616, but we can only store 255
      // characters in our entity DB schema.
      ->setSetting('max_length', 255);
      ->setSetting('max_length', 255)
      ->addConstraint('Oauth2RedirectUri');

    $fields['access_token_expiration'] = BaseFieldDefinition::create('integer')
      ->setLabel(new TranslatableMarkup('Access token expiration time'))
      ->setDescription(new TranslatableMarkup('The number of seconds that the access token will be valid.'))
      ->setDisplayOptions('view', [
        'label' => 'inline',
        'weight' => 6,
      ])
      ->setDisplayOptions('form', [
        'weight' => 6,
      ])
      ->setRevisionable(TRUE)
      ->setTranslatable(FALSE)
      ->setRequired(TRUE)
      ->setSetting('unsigned', TRUE)
      ->setDefaultValue(300);

    $fields['refresh_token_expiration'] = BaseFieldDefinition::create('integer')
      ->setLabel(new TranslatableMarkup('Refresh token expiration time'))
      ->setDescription(new TranslatableMarkup('The number of seconds that the refresh token will be valid.'))
      ->setDisplayOptions('view', [
        'label' => 'inline',
        'weight' => 6,
      ])
      ->setDisplayOptions('form', [
        'weight' => 6,
      ])
      ->setRevisionable(TRUE)
      ->setTranslatable(FALSE)
      ->setRequired(TRUE)
      ->setSetting('unsigned', TRUE)
      ->setDefaultValue(1209600);

    $fields['user_id'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(new TranslatableMarkup('User'))
@@ -142,7 +174,7 @@ function simple_oauth_entity_base_field_info(EntityTypeInterface $entity_type) {
      ->setCardinality(1)
      ->setDisplayOptions('form', [
        'type' => 'entity_reference_autocomplete',
        'weight' => 0,
        'weight' => 2,
        'settings' => [
          'match_operator' => 'CONTAINS',
          'size' => '60',
@@ -157,12 +189,38 @@ function simple_oauth_entity_base_field_info(EntityTypeInterface $entity_type) {
      ->setDisplayOptions('view', [
        'label' => 'inline',
        'type' => 'boolean',
        'weight' => 3,
        'weight' => 4,
      ])
      ->setDisplayOptions('form', ['weight' => 3])
      ->setDisplayOptions('form', ['weight' => 4])
      ->setRevisionable(TRUE)
      ->setTranslatable(TRUE)
      ->setTranslatable(FALSE)
      ->setDefaultValue(FALSE);

    $fields['automatic_authorization'] = BaseFieldDefinition::create('boolean')
      ->setLabel(new TranslatableMarkup('Automatic authorization'))
      ->setDescription(new TranslatableMarkup('This will cause the authorization form to be skipped.'))
      ->setDisplayOptions('view', [
        'label' => 'inline',
        'type' => 'boolean',
        'weight' => 0,
      ])
      ->setDisplayOptions('form', ['weight' => 0])
      ->setRevisionable(TRUE)
      ->setTranslatable(FALSE)
      ->setDefaultValue(FALSE);

    $fields['remember_approval'] = BaseFieldDefinition::create('boolean')
      ->setLabel(new TranslatableMarkup('Remember previous approval'))
      ->setDescription(new TranslatableMarkup('When enabled, if previous authorization request with the same scopes got approved, authorization will be automatically accepted.'))
      ->setDisplayOptions('view', [
        'label' => 'inline',
        'type' => 'boolean',
        'weight' => 1,
      ])
      ->setDisplayOptions('form', ['weight' => 1])
      ->setRevisionable(TRUE)
      ->setTranslatable(FALSE)
      ->setDefaultValue(TRUE);
  }
  return $fields;
}
@@ -174,25 +232,73 @@ function simple_oauth_form_consumer_form_alter(array &$form, FormStateInterface
  // Add a custom submit behavior.
  $form['#entity_builders'][] = 'simple_oauth_form_consumer_form_submit';

  $entity_type_manager = \Drupal::service('entity_type.manager');
  // Remove automatic roles and administrator roles.
  unset($form['roles']['widget']['#options'][RoleInterface::ANONYMOUS_ID]);
  unset($form['roles']['widget']['#options'][RoleInterface::AUTHENTICATED_ID]);
  // Get the admin role.
  $admin_roles = $entity_type_manager->getStorage('user_role')->getQuery()
    ->condition('is_admin', TRUE)
    ->execute();
  $default_value = reset($admin_roles);
  unset($form['roles']['widget']['#options'][$default_value]);
  $recommendation_text = t(
    'Create a <a href=":url">role</a> for every logical group of permissions you want to make available to a consumer.',
    [':url' => Url::fromRoute('entity.user_role.collection')->toString()]
  // Hide PKCE when dealing with public client and authorization code grant type.
  // see: simple_oauth_entity_presave().
  $form['pkce']['#states']['invisible'] = [
    ':input[name="confidential[value]"]' => [
      'checked' => FALSE,
    ],
    'and',
    ':input[name="grant_types[authorization_code]"]' => [
      'checked' => TRUE,
    ],
  ];

  // @todo: Implement vertical tabs when this fix lands in core: https://www.drupal.org/project/drupal/issues/1148950.
  $form['authorization_code'] = [
    '#type' => 'details',
    '#title' => t('Authorization Code settings'),
    '#open' => TRUE,
    '#weight' => 2,
    '#states' => [
      'visible' => [
        ':input[name="grant_types[authorization_code]"]' => [
          'checked' => TRUE,
        ],
      ],
    ],
    'automatic_authorization' => $form['automatic_authorization'],
    'remember_approval' => $form['remember_approval'],
  ];

  $form['client_credentials'] = [
    '#type' => 'details',
    '#title' => t('Client Credentials settings'),
    '#open' => TRUE,
    '#weight' => 2,
    '#states' => [
      'visible' => [
        ':input[name="grant_types[client_credentials]"]' => [
          'checked' => TRUE,
        ],
      ],
    ],
    'user_id' => $form['user_id'],
    'scopes' => $form['scopes'],
  ];

  $form['refresh_token'] = [
    '#type' => 'details',
    '#title' => t('Refresh Token settings'),
    '#open' => TRUE,
    '#weight' => 3,
    '#states' => [
      'visible' => [
        ':input[name="grant_types[refresh_token]"]' => [
          'checked' => TRUE,
        ],
      ],
    ],
    'refresh_token_expiration' => $form['refresh_token_expiration'],
  ];

  unset(
    $form['user_id'],
    $form['scopes'],
    $form['automatic_authorization'],
    $form['remember_approval'],
    $form['refresh_token_expiration'],
  );
  $form['roles']['widget']['#description'] .= '<br>' . $recommendation_text;
  if (empty($form['roles']['widget']['#options'])) {
    \Drupal::service('messenger')->addMessage($recommendation_text, 'error');
    $form['actions']['#disabled'] = TRUE;
  }

  $description = t(
    'Use this field to create a hash of the secret key. This module will never store your consumer key, only a hash of it.'
@@ -202,11 +308,6 @@ function simple_oauth_form_consumer_form_alter(array &$form, FormStateInterface
    '#title' => t('New Secret'),
    '#description' => $description,
  ];
  $form['pkce']['#states']['visible'] = [
    ':input[name="confidential[value]"]' => [
      'checked' => FALSE,
    ],
  ];
}

/**
@@ -233,21 +334,30 @@ function simple_oauth_form_consumer_form_submit($entity_type_id, Consumer $entit
 */
function simple_oauth_consumers_list_alter(&$data, $context) {
  if ($context['type'] === 'header') {
    $data['redirect'] = t('Redirect');
    $data['scopes'] = t('Scopes');
    $data['redirect'] = t('Redirects');
  }
  elseif ($context['type'] === 'row') {
    $entity = $context['entity'];
    $data['redirect'] = NULL;
    if ($redirect_url = $context['entity']->get('redirect')->value) {
      $data['redirect'] = Link::fromTextAndUrl($redirect_url, Url::fromUri($redirect_url));
    }
    $data['scopes'] = [
    $data['redirect'] = [
      'data' => ['#theme' => 'item_list', '#items' => []],
    ];
    foreach ($entity->get('roles')->getValue() as $role) {
      $data['scopes']['data']['#items'][] = $role['target_id'];
    foreach ($entity->get('redirect')->getValue() as $redirect) {
      $data['redirect']['data']['#items'][] = $redirect['value'];
    }
  }
}

/**
 * Implements hook_entity_presave().
 *
 * Enforce PKCE for public clients with authorization code grant type.
 */
function simple_oauth_entity_presave(EntityInterface $entity) {
  if ($entity instanceof Consumer && !$entity->get('confidential')->value) {
    foreach ($entity->get('grant_types')->getValue() as $grant_type_item) {
      if ($grant_type_item['value'] === 'authorization_code') {
        $entity->set('pkce', TRUE);
      }
    }
  }
}
+4 −12
Original line number Diff line number Diff line
@@ -115,28 +115,20 @@ class Oauth2Token extends ContentEntityBase implements Oauth2TokenInterface {
        'weight' => 2,
      ]);

    $fields['scopes'] = BaseFieldDefinition::create('entity_reference')
    $fields['scopes'] = BaseFieldDefinition::create('oauth2_scope_reference')
      ->setLabel(t('Scopes'))
      ->setDescription(t('The scopes for this Access Token. OAuth2 scopes are implemented as Drupal roles.'))
      ->setDescription(t('The scopes for this Access Token.'))
      ->setRevisionable(TRUE)
      ->setSetting('target_type', 'user_role')
      ->setSetting('handler', 'default')
      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
      ->setTranslatable(FALSE)
      ->setDisplayOptions('view', [
        'label' => 'inline',
        'type' => 'entity_reference_label',
        'type' => 'oauth2_scope_reference_label',
        'weight' => 3,
      ])
      ->setDisplayOptions('form', [
        'type' => 'entity_reference_autocomplete',
        'type' => 'oauth2_scope_reference',
        'weight' => 3,
        'settings' => [
          'match_operator' => 'CONTAINS',
          'size' => '60',
          'autocomplete_type' => 'tags',
          'placeholder' => '',
        ],
      ]);

    $fields['value'] = BaseFieldDefinition::create('string')
Loading