diff --git a/composer.json b/composer.json index 3a17860c5888464a6ffe3736fe6716212864e3a5..a76637dfa2a34811d4f4242c4a2ea1d480f927c9 100644 --- a/composer.json +++ b/composer.json @@ -14,5 +14,11 @@ "php": ">=8.1.0", "ext-json": "*", "drupal/externalauth": "^2.0" + }, + "require-dev": { + "drupal/key": "1.x-dev" + }, + "suggest": { + "drupal/key": "Allows better key management" } } diff --git a/config/schema/openid_connect.schema.yml b/config/schema/openid_connect.schema.yml index 607549408b6532e402b7eab1b083f23b6d86e546..0d340c421c76c8ec25d602813e3eaa1e11a21fd1 100644 --- a/config/schema/openid_connect.schema.yml +++ b/config/schema/openid_connect.schema.yml @@ -59,12 +59,18 @@ openid_connect.client.plugin.*: type: mapping label: 'OpenID Connect plugin base settings' mapping: &base + credentials_provider: + type: string + label: 'Defines mechanism to provide credentials' client_id: type: string label: 'Client ID' client_secret: type: string label: 'Client secret' + credentials_key: + type: string + label: 'Reference to key providing credentials' iss_allowed_domains: type: string label: 'Domains that are allowed to initiate SSO using ISS' diff --git a/openid_connect.install b/openid_connect.install index fa82053b1fde9e84d2df97539f7d98bd877980c6..b72b2fe5dce05ec46db363665494587bb65239fa 100644 --- a/openid_connect.install +++ b/openid_connect.install @@ -388,3 +388,19 @@ function openid_connect_update_30004() { } } } + +/** + * Add extra credentials configuration to plugins. + */ +function openid_connect_update_30005() { + $storage = \Drupal::entityTypeManager()->getStorage('openid_connect_client'); + $clients = $storage->loadMultiple(); + foreach ($clients as $client) { + $config = $client->getPlugin()->getConfiguration(); + if (!array_key_exists('credentials_provider', $config)) { + $client->getPlugin()->setConfiguration(['credentials_provider' => 'config']); + $client->getPlugin()->setConfiguration(['credentials_key' => '']); + $client->save(); + } + } +} diff --git a/src/Plugin/OpenIDConnectClientBase.php b/src/Plugin/OpenIDConnectClientBase.php index 28167024d49748918086723610066e000c733e7b..b4db01d9b63b5ffafbb77de0f28529287dc7139c 100644 --- a/src/Plugin/OpenIDConnectClientBase.php +++ b/src/Plugin/OpenIDConnectClientBase.php @@ -16,6 +16,7 @@ use Drupal\Core\Plugin\PluginWithFormsTrait; use Drupal\Core\Routing\TrustedRedirectResponse; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; +use Drupal\key\KeyRepository; use Drupal\openid_connect\OpenIDConnectAutoDiscover; use Drupal\openid_connect\OpenIDConnectStateTokenInterface; use GuzzleHttp\ClientInterface; @@ -94,6 +95,13 @@ abstract class OpenIDConnectClientBase extends PluginBase implements OpenIDConne */ protected $parentEntityId; + /** + * The key repository service. + * + * @var \Drupal\key\KeyRepository|null + */ + protected ?KeyRepository $keyRepository = NULL; + /** * The constructor. * @@ -119,8 +127,10 @@ abstract class OpenIDConnectClientBase extends PluginBase implements OpenIDConne * The OpenID state token service. * @param \Drupal\openid_connect\OpenIDConnectAutoDiscover $auto_discover * The OpenID well-known discovery service. + * @param \Drupal\key\KeyRepository|null $key_repository + * The key repository service. */ - public function __construct(array $configuration, string $plugin_id, $plugin_definition, RequestStack $request_stack, ClientInterface $http_client, LoggerChannelFactoryInterface $logger_factory, TimeInterface $datetime_time, KillSwitch $page_cache_kill_switch, LanguageManagerInterface $language_manager, OpenIDConnectStateTokenInterface $state_token, OpenIDConnectAutoDiscover $auto_discover) { + public function __construct(array $configuration, string $plugin_id, $plugin_definition, RequestStack $request_stack, ClientInterface $http_client, LoggerChannelFactoryInterface $logger_factory, TimeInterface $datetime_time, KillSwitch $page_cache_kill_switch, LanguageManagerInterface $language_manager, OpenIDConnectStateTokenInterface $state_token, OpenIDConnectAutoDiscover $auto_discover, ?KeyRepository $key_repository = NULL) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->requestStack = $request_stack; @@ -132,6 +142,7 @@ abstract class OpenIDConnectClientBase extends PluginBase implements OpenIDConne $this->stateToken = $state_token; $this->autoDiscover = $auto_discover; $this->parentEntityId = ''; + $this->keyRepository = $key_repository; $this->setConfiguration($configuration); } @@ -139,6 +150,13 @@ abstract class OpenIDConnectClientBase extends PluginBase implements OpenIDConne * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + /** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */ + $module_handler = $container->get('module_handler'); + $keyRepository = NULL; + if ($module_handler->moduleExists('key')) { + $keyRepository = $container->get('key.repository'); + } + return new static( $configuration, $plugin_id, @@ -150,7 +168,8 @@ abstract class OpenIDConnectClientBase extends PluginBase implements OpenIDConne $container->get('page_cache_kill_switch'), $container->get('language_manager'), $container->get('openid_connect.state_token'), - $container->get('openid_connect.autodiscover') + $container->get('openid_connect.autodiscover'), + $keyRepository ); } @@ -196,8 +215,10 @@ abstract class OpenIDConnectClientBase extends PluginBase implements OpenIDConne */ public function defaultConfiguration(): array { return [ + 'credentials_provider' => 'config', 'client_id' => '', 'client_secret' => '', + 'credentials_key' => '', 'iss_allowed_domains' => '', ]; } @@ -227,18 +248,68 @@ abstract class OpenIDConnectClientBase extends PluginBase implements OpenIDConne * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state): array { + $form['credentials_provider'] = [ + '#type' => 'select', + '#title' => $this->t('Credentials provider'), + '#options' => [ + 'config' => $this->t('Local configuration'), + ], + '#default_value' => $this->configuration['credentials_provider'] ?? 'config', + ]; + // Enable key as credentials provider if key module is installed. + if ($this->keyRepository !== NULL) { + $form['credentials_provider']['#options']['key'] = $this->t('Key'); + } + $form['client_id'] = [ '#title' => $this->t('Client ID'), '#type' => 'textfield', '#default_value' => $this->configuration['client_id'], '#required' => TRUE, + '#states' => [ + 'visible' => [ + ':input[name="settings[credentials_provider]"]' => ['value' => 'config'], + ], + 'required' => [ + ':input[name="settings[credentials_provider]"]' => ['value' => 'config'], + ], + ], ]; + $form['client_secret'] = [ '#title' => $this->t('Client secret'), '#type' => 'textarea', '#default_value' => $this->configuration['client_secret'], '#required' => TRUE, + '#states' => [ + 'visible' => [ + ':input[name="settings[credentials_provider]"]' => ['value' => 'config'], + ], + 'required' => [ + ':input[name="settings[credentials_provider]"]' => ['value' => 'config'], + ], + ], ]; + + if ($this->keyRepository !== NULL) { + $form['credentials_key'] = [ + '#type' => 'key_select', + '#title' => $this->t('Key'), + '#description' => $this->t('The key should contain the OAuth credentials. Choose key type "Authentication (Multivalue)", and add the properties "client_id" and "client_secret" to the key value.') . $this->t('<br />Example key data: <code>{"client_id": "47012495342576573474", "client_secret": "51106350561134283737"}</code>'), + '#default_value' => $this->configuration['credentials_key'] ?? '', + '#empty_option' => $this->t('- Please select -'), + '#key_filters' => ['type' => 'authentication_multivalue'], + '#states' => [ + 'visible' => [ + ':input[name="settings[credentials_provider]"]' => ['value' => 'key'], + ], + 'required' => [ + ':input[name="settings[credentials_provider]"]' => ['value' => 'key'], + ], + ], + ]; + } + $form['iss_allowed_domains'] = [ '#title' => $this->t('Allowed domains'), '#description' => $this->t('Enter one domain per line that are allowed to initiate SSO using ISS.'), @@ -266,7 +337,22 @@ abstract class OpenIDConnectClientBase extends PluginBase implements OpenIDConne * {@inheritdoc} */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { - // Empty function. Can be overridden by derived classes if required. + // Manipulate client credentials storage. + // Method can be overridden by derived classes if required, to perform + // additional configuration actions. + $configuration = $form_state->getValues(); + // Only save the relevant client credentials. + $credentials_provider = $configuration['credentials_provider']; + if ($credentials_provider === 'config') { + // Make sure no key reference is saved, if we are using config to store + // credentials. + $this->setConfiguration(['credentials_key' => '']); + } + elseif ($credentials_provider === 'key') { + // Make sure no client ID / secret is saved, if we are using keys to store + // credentials. + $this->setConfiguration(['client_id' => '', 'client_secret' => '']); + } } /** @@ -295,6 +381,32 @@ abstract class OpenIDConnectClientBase extends PluginBase implements OpenIDConne return $response; } + /** + * Helper function to retrieve client ID / secret credentials. + * + * @return array + * Array with 'client_id' and 'client_secret' keys. + */ + protected function getClientCredentials(): array { + $credentials_provider = $this->configuration['credentials_provider']; + $client_id = $client_secret = ''; + if ($credentials_provider === 'config') { + $client_id = $this->configuration['client_id']; + $client_secret = $this->configuration['client_secret']; + } + elseif ($this->keyRepository && $credentials_provider === 'key') { + $key_credentials = $this->keyRepository->getKey($this->configuration['credentials_key'])?->getKeyValues(); + if (!empty($key_credentials)) { + $client_id = $key_credentials['client_id'] ?? ''; + $client_secret = $key_credentials['client_secret'] ?? ''; + } + } + return [ + 'client_id' => $client_id, + 'client_secret' => $client_secret, + ]; + } + /** * Helper function for URL options. * @@ -309,7 +421,7 @@ abstract class OpenIDConnectClientBase extends PluginBase implements OpenIDConne protected function getUrlOptions(string $scope, GeneratedUrl $redirect_uri): array { return [ 'query' => [ - 'client_id' => $this->configuration['client_id'], + 'client_id' => $this->getClientCredentials()['client_id'], 'response_type' => 'code', 'scope' => $scope, 'redirect_uri' => $redirect_uri->getGeneratedUrl(), @@ -333,8 +445,8 @@ abstract class OpenIDConnectClientBase extends PluginBase implements OpenIDConne return [ 'form_params' => [ 'code' => $authorization_code, - 'client_id' => $this->configuration['client_id'], - 'client_secret' => $this->configuration['client_secret'], + 'client_id' => $this->getClientCredentials()['client_id'], + 'client_secret' => $this->getClientCredentials()['client_secret'], 'redirect_uri' => $redirect_uri, 'grant_type' => 'authorization_code', ], diff --git a/tests/src/Functional/AutoLoginTest.php b/tests/src/Functional/AutoLoginTest.php index dced8c1851c81b86592f713823e36af44511f7a3..dbe46294f37590d7c98eefe9aa99358768330fd2 100644 --- a/tests/src/Functional/AutoLoginTest.php +++ b/tests/src/Functional/AutoLoginTest.php @@ -41,6 +41,7 @@ class AutoLoginTest extends OpenIdConnectTestBase { [ 'label' => self::OIDC_LABEL, 'id' => self::OIDC_ID, + 'settings[credentials_provider]' => 'config', 'settings[client_id]' => $this->randomString(8), 'settings[client_secret]' => $this->randomString(8), ], diff --git a/tests/src/Functional/Plugin/OpenIDConnectClient/GenericClientTest.php b/tests/src/Functional/Plugin/OpenIDConnectClient/GenericClientTest.php index 5bb9793ad152082307111e50f039a59f9fc46f48..d867c578b315104e0b8a0499700dec2e86aa7353 100644 --- a/tests/src/Functional/Plugin/OpenIDConnectClient/GenericClientTest.php +++ b/tests/src/Functional/Plugin/OpenIDConnectClient/GenericClientTest.php @@ -68,6 +68,7 @@ class GenericClientTest extends BrowserTestBase { $edit = [ 'label' => self::LABEL, 'id' => self::ID, + 'settings[credentials_provider]' => 'config', 'settings[client_id]' => $this->randomString(6), 'settings[client_secret]' => $this->randomString(6), 'settings[iss_allowed_domains]' => '', diff --git a/tests/src/Unit/Entity/OpenIDConnectClientEntityTest.php b/tests/src/Unit/Entity/OpenIDConnectClientEntityTest.php index c5e1589f34c2af1f1445803d986c58354ac5ac07..f4b793e9e911fe0c06e5d4e94c1cb86156757cb6 100644 --- a/tests/src/Unit/Entity/OpenIDConnectClientEntityTest.php +++ b/tests/src/Unit/Entity/OpenIDConnectClientEntityTest.php @@ -23,6 +23,8 @@ use Drupal\Tests\UnitTestCase; * @group openid_connect */ class OpenIDConnectClientEntityTest extends UnitTestCase { + + const CREDENTIALS_PROVIDER = 'config'; const CLIENT_ID = 'test_client_id'; const CLIENT_SECRET = 'test_client_secret'; const PLUGIN_ID = 'generic'; @@ -31,6 +33,7 @@ class OpenIDConnectClientEntityTest extends UnitTestCase { 'label' => 'Test Plugin', 'plugin' => self::PLUGIN_ID, 'settings' => [ + 'credentials_provider' => self::CREDENTIALS_PROVIDER, 'client_id' => self::CLIENT_ID, 'client_secret' => self::CLIENT_SECRET, 'issuer_url' => '',