Skip to content

Commit

Permalink
Merge branch 'encryptCredentials-692' into 'main'
Browse files Browse the repository at this point in the history
Adds encryption of API credentials

See merge request softwares-pkp/plugins_ojs/pre-endorsement-plaudit!42
  • Loading branch information
YvesLepidus committed Aug 20, 2024
2 parents 0cd44f4 + 8f7bd00 commit 463817c
Show file tree
Hide file tree
Showing 16 changed files with 260 additions and 40 deletions.
10 changes: 9 additions & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,12 @@ include:
file:
- 'templates/groups/pkp_plugin.yml'
- 'templates/groups/ops/unit_tests.yml'
- 'templates/groups/ops/cypress_tests.yml'
- 'templates/groups/ops/cypress_tests.yml'

.unit_test_template:
before_script:
- sed -i 's/api_key_secret = ""/api_key_secret = "$API_KEY_SECRET"/' /var/www/ops/config.inc.php

.integration_tests_template:
before_script:
- sed -i 's/api_key_secret = ""/api_key_secret = "$API_KEY_SECRET"/' /var/www/ops/config.inc.php
30 changes: 17 additions & 13 deletions PlauditPreEndorsementSettingsForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use PKP\form\validation\FormValidatorPost;
use PKP\form\validation\FormValidatorCSRF;
use PKP\form\validation\FormValidatorCustom;
use APP\plugins\generic\plauditPreEndorsement\classes\api\APIKeyEncryption;
use APP\plugins\generic\plauditPreEndorsement\classes\OrcidCredentialsValidator;

class PlauditPreEndorsementSettingsForm extends Form
Expand All @@ -43,7 +44,8 @@ public function __construct($plugin, $contextId)
$this->plugin = $plugin;
$orcidValidator = new OrcidCredentialsValidator($plugin);
$this->validator = $orcidValidator;
parent::__construct($plugin->getTemplateResource('settingsForm.tpl'));
$template = APIKeyEncryption::secretConfigExists() ? 'settingsForm.tpl' : 'tokenError.tpl';
parent::__construct($plugin->getTemplateResource($template));
$this->addCheck(new FormValidatorPost($this));
$this->addCheck(new FormValidatorCSRF($this));

Expand All @@ -58,16 +60,6 @@ public function __construct($plugin, $contextId)
}
}

public function initData()
{
$contextId = $this->contextId;
$plugin = &$this->plugin;
$this->_data = array();
foreach (self::CONFIG_VARS as $configVar => $type) {
$this->_data[$configVar] = $plugin->getSetting($contextId, $configVar);
}
}

public function readInputData()
{
$this->readUserVars(array_keys(self::CONFIG_VARS));
Expand All @@ -79,6 +71,7 @@ public function fetch($request, $template = null, $display = false)
$templateMgr->assign('globallyConfigured', $this->orcidIsGloballyConfigured());
$templateMgr->assign('pluginName', $this->plugin->getName());
$templateMgr->assign('applicationName', Application::get()->getName());
$templateMgr->assign('hasCredentials', OrcidCredentialsValidator::hasCredentials($this->plugin, $this->contextId));
return parent::fetch($request, $template, $display);
}

Expand All @@ -88,9 +81,20 @@ public function execute(...$functionArgs)
$contextId = $this->contextId;
foreach (self::CONFIG_VARS as $configVar => $type) {
if ($configVar === 'orcidAPIPath') {
$plugin->updateSetting($contextId, $configVar, trim($this->getData($configVar), "\"\';"), $type);
$orcidAPIPath = trim($this->getData($configVar), "\"\';");
$plugin->updateSetting(
$contextId,
$configVar,
$orcidAPIPath,
$type
);
} else {
$plugin->updateSetting($contextId, $configVar, $this->getData($configVar), $type);
$plugin->updateSetting(
$contextId,
$configVar,
APIKeyEncryption::encryptString($this->getData($configVar)),
$type
);
}
}

Expand Down
7 changes: 5 additions & 2 deletions classes/EndorsementService.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use APP\plugins\generic\plauditPreEndorsement\classes\PlauditClient;
use APP\plugins\generic\plauditPreEndorsement\classes\CrossrefClient;
use APP\plugins\generic\plauditPreEndorsement\classes\OrcidClient;
use APP\plugins\generic\plauditPreEndorsement\classes\api\APIKeyEncryption;

class EndorsementService
{
Expand Down Expand Up @@ -55,7 +56,8 @@ public function sendEndorsement($endorsement, $needCheckMessageWasLoggedToday =
public function validateEndorsementSending($publication): string
{
$doi = $publication->getDoi();
$secretKey = $this->plugin->getSetting($this->contextId, 'plauditAPISecret');
$plauditApiKeySecretSetting = $this->plugin->getSetting($this->contextId, 'plauditAPISecret');
$secretKey = $plauditApiKeySecretSetting ? APIKeyEncryption::decryptString($plauditApiKeySecretSetting) : null;

if (empty($doi)) {
return 'plugins.generic.plauditPreEndorsement.log.failedEndorsementSending.emptyDoi';
Expand Down Expand Up @@ -87,7 +89,8 @@ public function sendEndorsementToPlaudit($endorsement, $publication)
$plauditClient = new PlauditClient();

try {
$secretKey = $this->plugin->getSetting($this->contextId, 'plauditAPISecret');
$plauditApiKeySecretSetting = $this->plugin->getSetting($this->contextId, 'plauditAPISecret');
$secretKey = APIKeyEncryption::decryptString($plauditApiKeySecretSetting);
$response = $plauditClient->requestEndorsementCreation($endorsement, $publication, $secretKey);
$newEndorsementStatus = $plauditClient->getEndorsementStatusByResponse($response, $publication, $endorsement);
} catch (ClientException $exception) {
Expand Down
11 changes: 6 additions & 5 deletions classes/OrcidClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace APP\plugins\generic\plauditPreEndorsement\classes;

use APP\core\Application;
use APP\plugins\generic\plauditPreEndorsement\classes\api\APIKeyEncryption;

class OrcidClient
{
Expand Down Expand Up @@ -31,8 +32,8 @@ public function getReadPublicAccessToken(): string
$tokenUrl = $this->plugin->getSetting($this->contextId, 'orcidAPIPath') . 'oauth/token';
$requestHeaders = ['Accept' => 'application/json'];
$requestData = [
'client_id' => $this->plugin->getSetting($this->contextId, 'orcidClientId'),
'client_secret' => $this->plugin->getSetting($this->contextId, 'orcidClientSecret'),
'client_id' => APIKeyEncryption::decryptString($this->plugin->getSetting($this->contextId, 'orcidClientId')),
'client_secret' => APIKeyEncryption::decryptString($this->plugin->getSetting($this->contextId, 'orcidClientSecret')),
'grant_type' => 'client_credentials',
'scope' => '/read-public'
];
Expand Down Expand Up @@ -76,8 +77,8 @@ public function requestOrcid(string $code)
$tokenUrl = $this->plugin->getSetting($this->contextId, 'orcidAPIPath') . 'oauth/token';
$requestHeaders = ['Accept' => 'application/json'];
$requestData = [
'client_id' => $this->plugin->getSetting($this->contextId, 'orcidClientId'),
'client_secret' => $this->plugin->getSetting($this->contextId, 'orcidClientSecret'),
'client_id' => APIKeyEncryption::decryptString($this->plugin->getSetting($this->contextId, 'orcidClientId')),
'client_secret' => APIKeyEncryption::decryptString($this->plugin->getSetting($this->contextId, 'orcidClientSecret')),
'grant_type' => 'authorization_code',
'code' => $code
];
Expand Down Expand Up @@ -125,7 +126,7 @@ public function buildOAuthUrl($redirectParams)

return $this->getOauthPath() . 'authorize?' . http_build_query(
array(
'client_id' => $this->plugin->getSetting($this->contextId, 'orcidClientId'),
'client_id' => APIKeyEncryption::decryptString($this->plugin->getSetting($this->contextId, 'orcidClientId')),
'response_type' => 'code',
'scope' => $scope,
'redirect_uri' => $redirectUrl)
Expand Down
7 changes: 7 additions & 0 deletions classes/OrcidCredentialsValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,11 @@ public function validateClientSecret($str): bool
}
return $valid;
}

public static function hasCredentials($plugin, $contextId): bool
{
$hasClientId = !empty($plugin->getSetting($contextId, 'orcidClientId'));
$hasClientSecret = !empty($plugin->getSetting($contextId, 'orcidClientSecret'));
return $hasClientId && $hasClientSecret;
}
}
52 changes: 52 additions & 0 deletions classes/api/APIKeyEncryption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace APP\plugins\generic\plauditPreEndorsement\classes\api;

use Firebase\JWT\JWT;
use PKP\config\Config;
use Exception;

class APIKeyEncryption
{
public static function secretConfigExists(): bool
{
try {
self::getSecretFromConfig();
} catch (Exception $e) {
return false;
}
return true;
}

private static function getSecretFromConfig(): string
{
$secret = Config::getVar('security', 'api_key_secret');
if ($secret === "") {
throw new Exception("A secret must be set in the config file ('api_key_secret') so that keys can be encrypted and decrypted");
}
return $secret;
}

public static function encryptString(string $plainText): string
{
$secret = self::getSecretFromConfig();
return JWT::encode($plainText, $secret, 'HS256');
}

public static function decryptString(string $encryptedText)
{
$secret = self::getSecretFromConfig();
try {
return JWT::decode($encryptedText, $secret, ['HS256']);
} catch (Exception $e) {
if ($e instanceof Firebase\JWT\SignatureInvalidException) {
throw new Exception(
'The `api_key_secret` configuration is not the same as the one used to encrypt the key.',
1
);
}

throw $e;
}
}
}
7 changes: 5 additions & 2 deletions classes/migration/addEndorsementsTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Support\Facades\Schema;
use PKP\install\DowngradeNotSupportedException;
use APP\plugins\generic\plauditPreEndorsement\classes\migration\upgrade\MoveLegacyEndorsementsToEndorsementsTable;
use APP\plugins\generic\plauditPreEndorsement\classes\migration\upgrade\EncryptLegacyCredentials;

class addEndorsementsTable extends Migration
{
Expand Down Expand Up @@ -40,8 +41,10 @@ public function up(): void
});
}

$upgradeMigration = new MoveLegacyEndorsementsToEndorsementsTable();
$upgradeMigration->up();
foreach ([MoveLegacyEndorsementsToEndorsementsTable::class, EncryptLegacyCredentials::class] as $class) {
$migration = new $class();
$migration->up();
}
}

public function down(): void
Expand Down
79 changes: 79 additions & 0 deletions classes/migration/upgrade/EncryptLegacyCredentials.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace APP\plugins\generic\plauditPreEndorsement\classes\migration\upgrade;

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use PKP\install\DowngradeNotSupportedException;
use APP\plugins\generic\plauditPreEndorsement\classes\api\APIKeyEncryption;
use Firebase\JWT\JWT;

class EncryptLegacyCredentials extends Migration
{
public function up(): void
{
$credentialSettings = $this->getCredentialSettings();

if (!empty($credentialSettings)) {
$credentials = $this->getCredentials($credentialSettings);

foreach ($credentials as $contextId => $setting) {
$orcidClientId = $setting['orcidClientId'];
$orcidClientSecret = $setting['orcidClientSecret'];
$plauditAPISecret = $setting['plauditAPISecret'];

try {
APIKeyEncryption::decryptString($orcidClientId);
} catch (\Exception $e) {
if ($e instanceof \UnexpectedValueException) {
$this->encryptCredentials($contextId, $orcidClientId, $orcidClientSecret, $plauditAPISecret);
}
}
}
}
}

public function down(): void
{
throw new DowngradeNotSupportedException();
}

private function getCredentialSettings()
{
return DB::table('plugin_settings')
->whereIn('setting_name', [
'orcidClientId',
'orcidClientSecret',
'plauditAPISecret'
])
->get();
}

private function getCredentials($credentialSettings)
{
$credentials = [];
foreach ($credentialSettings as $credentialSetting) {
$contextId = $credentialSetting->context_id;
$credentials[$contextId][$credentialSetting->setting_name] = $credentialSetting->setting_value;
}
return $credentials;
}

private function encryptCredentials($contextId, $orcidClientId, $orcidClientSecret, $plauditAPISecret)
{
$credentials = [
'orcidClientId' => $orcidClientId,
'orcidClientSecret' => $orcidClientSecret,
'plauditAPISecret' => $plauditAPISecret
];

foreach ($credentials as $settingName => $settingValue) {
$encryptedValue = APIKeyEncryption::encryptString($settingValue);

DB::table('plugin_settings')
->where('context_id', $contextId)
->where('setting_name', $settingName)
->update(['setting_value' => $encryptedValue]);
}
}
}
5 changes: 5 additions & 0 deletions cypress/tests/Test0_pluginSetup.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,10 @@ describe('Plaudit Pre-endorsement - Plugin setup', function () {

cy.get('#plauditPreEndorsementSettingsForm button:contains("OK")').click();
cy.contains('Please configure the ORCID API access for use in pulling ORCID profile information').should('not.exist');

cy.get('a[id^=' + pluginRowId + '-settings-button]').click();
cy.contains('Your credentials are already registered.');
cy.contains('For security reasons, the credentials are being encrypted. For the same reason, we will not display the already registered credentials in this form.');
cy.contains('I want to enter new credentials.');
});
});
12 changes: 12 additions & 0 deletions locale/en/locale.po
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ msgstr "This plugin allows authors to do the pre-endorsement of their submission
msgid "plugins.generic.plauditPreEndorsement.endorsement"
msgstr "Endorsement"

msgid "plugins.generic.plauditPreEndorsement.settings.apiKeyIsEmpty"
msgstr "Your site administrator must set a secret in the config file ('api_key_secret')."

msgid "plugins.generic.plauditPreEndorsement.settings.credentialsRegistered"
msgstr "Your credentials are already registered."

msgid "plugins.generic.plauditPreEndorsement.settings.clickHere"
msgstr "I want to enter new credentials"

msgid "plugins.generic.plauditPreEndorsement.settings.securityNotice"
msgstr "For security reasons, the credentials are being encrypted. For the same reason, we will not display the already registered credentials in this form."

msgid "plugins.generic.plauditPreEndorsement.endorsement.description"
msgstr "Do you have the endorsement of an experienced researcher in the field of knowledge of the manuscript? If yes, please provide the name and e-mail address of the endorsing researcher. Endorsements can significantly speed up the moderation process. The endorsement cannot be given by one of the authors of the manuscript."

Expand Down
12 changes: 12 additions & 0 deletions locale/es/locale.po
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ msgstr "Este módulo permite a los autores dar aprobación previa a sus envíos"
msgid "plugins.generic.plauditPreEndorsement.endorsement"
msgstr "Endoso"

msgid "plugins.generic.plauditPreEndorsement.settings.apiKeyIsEmpty"
msgstr "El administrador de su sitio debe establecer un secreto en el archivo de configuración ('api_key_secret')."

msgid "plugins.generic.plauditPreEndorsement.settings.credentialsRegistered"
msgstr "Sus credenciales ya están registradas."

msgid "plugins.generic.plauditPreEndorsement.settings.clickHere"
msgstr "Quiero ingresar nuevas credenciales"

msgid "plugins.generic.plauditPreEndorsement.settings.securityNotice"
msgstr "Por razones de seguridad, las credenciales están siendo encriptadas. Por el mismo motivo, no mostraremos las credenciales ya registradas en este formulario."

msgid "plugins.generic.plauditPreEndorsement.endorsement.description"
msgstr "¿Cuenta con el aval de un/a investigador/a experimentado/a en el campo de conocimiento del manuscrito? En caso afirmativo, indique el nombre y la dirección de correo electrónico del/a investigador/a que lo avala. Los avales pueden acelerar considerablemente el proceso de moderación. El aval no puede ser dado por uno de los autores del manuscrito."

Expand Down
12 changes: 12 additions & 0 deletions locale/pt_BR/locale.po
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ msgstr "Esse plugin permite aos autores realizar o pré-endosso das suas submiss
msgid "plugins.generic.plauditPreEndorsement.endorsement"
msgstr "Endosso"

msgid "plugins.generic.plauditPreEndorsement.settings.apiKeyIsEmpty"
msgstr "O administrador do seu site deve definir um segredo no arquivo de configuração ('api_key_secret')."

msgid "plugins.generic.plauditPreEndorsement.settings.credentialsRegistered"
msgstr "Suas credenciais já estão registradas."

msgid "plugins.generic.plauditPreEndorsement.settings.clickHere"
msgstr "Quero inserir novas credenciais"

msgid "plugins.generic.plauditPreEndorsement.settings.securityNotice"
msgstr "Por questões de segurança, as credenciais estão sendo criptografadas. Pela mesma razão, não exibiremos as credenciais já registradas neste formulário."

msgid "plugins.generic.plauditPreEndorsement.endorsement.description"
msgstr "Você possui o endosso de um(a) pesquisador(a) experiente na área de conhecimento do manuscrito? Em caso positivo, informe o nome e endereço e-mail do(a) pesquisador(a) endossador(a). Endossos podem acelerar significativamente o processo de moderação. O endosso não pode ser dado por um dos autores do manuscrito."

Expand Down
4 changes: 4 additions & 0 deletions styles/endorserSettingsForm.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.orcidAPIPath, .orcidClientId, .orcidClientSecret, .plauditAPISecret {
margin-top: 1rem;
}

#credentialsFields {
overflow: hidden;
}
Loading

0 comments on commit 463817c

Please sign in to comment.