Skip to content

Commit

Permalink
pkp#10318 Revoke ORCID tokens when deleting authenticated ORCIDs
Browse files Browse the repository at this point in the history
  • Loading branch information
taslangraham committed Aug 21, 2024
1 parent e1cec28 commit 98df0e5
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 18 deletions.
4 changes: 4 additions & 0 deletions api/v1/orcid/OrcidController.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Route;
use PKP\core\PKPBaseController;
use PKP\jobs\orcid\RevokeOrcidToken;
use PKP\jobs\orcid\SendAuthorMail;
use PKP\orcid\OrcidManager;
use PKP\security\Role;
Expand Down Expand Up @@ -168,9 +169,12 @@ function (Role $role) {
}
}

$clonedAuthor = clone $author;
$author->setOrcid(null);
$author->setOrcidVerified(false);

OrcidManager::removeOrcidAccessToken($author);
dispatch(new RevokeOrcidToken($context, $clonedAuthor));
Repo::author()->edit($author, []);

return response()->json([], Response::HTTP_OK);
Expand Down
3 changes: 2 additions & 1 deletion classes/author/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ public function validate($author, $props, Submission $submission, Context $conte
* @copydoc \PKP\services\entityProperties\EntityWriteInterface::add()
*
* @hook Author::add [[$author]]
* @hook Author::add::before [[$author]]
*/
public function add(Author $author): int
{
Expand All @@ -173,7 +174,7 @@ public function add(Author $author): int
*
* @hook Author::edit [[$newAuthor, $author, $params]]
*/
public function edit(Author $author, array $params)
public function edit(Author $author, array $params = [])
{
$newAuthor = Repo::author()->newDataObject(array_merge($author->_data, $params));

Expand Down
55 changes: 44 additions & 11 deletions classes/orcid/OrcidManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use PKP\config\Config;
use PKP\context\Context;
use PKP\core\Core;
use PKP\user\User;

class OrcidManager
{
Expand All @@ -30,6 +31,8 @@ class OrcidManager
public const ORCID_API_URL_MEMBER = 'https://api.orcid.org/';
public const ORCID_API_URL_MEMBER_SANDBOX = 'https://api.sandbox.orcid.org/';
public const ORCID_API_VERSION_URL = 'v3.0/';
public const ORCID_TOKEN_REVOCATION_URL = 'https://orcid.org/oauth/revoke';
public const ORCID_TOKEN_REVOCATION_URL_SANDBOX = 'https://sandbox.orcid.org/oauth/revoke';

public const OAUTH_TOKEN_URL = 'oauth/token';

Expand Down Expand Up @@ -315,20 +318,24 @@ public static function getClientSecret(?Context $context = null): string

/**
* Remove all data fields, which belong to an ORCID access token from the
* given Author object. Also updates fields in the db.
* given Author or User object. Also updates fields in the db.
*
* @param bool $updateAuthor If true, update the author fields in the database.
* Use only if not called from a function, which will already update the author.
* @param bool $updateDb If true, update the underlying fields in the database.
* Use only if not called from a function, which will already update the object.
*/
public static function removeOrcidAccessToken(Author $author, bool $updateAuthor = false): void
public static function removeOrcidAccessToken(Author|User $identity, bool $updateDb = false): void
{
$author->setData('orcidAccessToken', null);
$author->setData('orcidAccessScope', null);
$author->setData('orcidRefreshToken', null);
$author->setData('orcidAccessExpiresOn', null);

if ($updateAuthor) {
Repo::author()->dao->update($author);
$identity->setData('orcidAccessToken', null);
$identity->setData('orcidAccessScope', null);
$identity->setData('orcidRefreshToken', null);
$identity->setData('orcidAccessExpiresOn', null);

if ($updateDb) {
if ($identity instanceof User) {
Repo::user()->edit($identity);
} else {
Repo::author()->edit($identity);
}
}
}

Expand Down Expand Up @@ -363,4 +370,30 @@ private static function writeLog(string $message, string $level): void
$logFilePath = Config::getVar('files', 'files_dir') . '/orcid.log';
error_log("{$fineStamp} {$level} {$message}\n", 3, $logFilePath);
}

/**
*/
public static function getTokenRevocationUrl(?Context $context = null): string
{
return match (self::getApiType($context)) {
self::API_MEMBER_SANDBOX,
self::API_PUBLIC_SANDBOX => self::ORCID_TOKEN_REVOCATION_URL_SANDBOX,
default => self::ORCID_TOKEN_REVOCATION_URL,
};
}

private static function getApiType(?Context $context = null)
{
$apiType = '';
if (self::isGloballyConfigured()) {
$apiType = Application::get()->getRequest()->getSite()->getData(self::API_TYPE);
} else {
if ($context === null) {
$context = Application::get()->getRequest()->getContext();
}
$apiType = $context->getData(self::API_TYPE);
}

return $apiType;
}
}
20 changes: 16 additions & 4 deletions classes/user/form/IdentityForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

use APP\core\Application;
use APP\template\TemplateManager;
use pkp\jobs\orcid\RevokeOrcidToken;
use PKP\orcid\OrcidManager;
use PKP\user\User;

Expand Down Expand Up @@ -109,7 +110,7 @@ public function readInputData()
parent::readInputData();

$this->readUserVars([
'givenName', 'familyName', 'preferredPublicName', 'orcid',
'givenName', 'familyName', 'preferredPublicName', 'orcid','removeOrcidId'
]);
}

Expand All @@ -121,9 +122,20 @@ public function execute(...$functionArgs)
$request = Application::get()->getRequest();
$user = $request->getUser();

$user->setGivenName($this->getData('givenName'), null);
$user->setFamilyName($this->getData('familyName'), null);
$user->setPreferredPublicName($this->getData('preferredPublicName'), null);

// Request to delete ORCID token is handle separately from other form field updates
if($this->getData('removeOrcidId') === 'true') {
$clonedUser = clone $user;
$user->setOrcid(null);
$user->setOrcidVerified(false);

OrcidManager::removeOrcidAccessToken($user);
dispatch(new RevokeOrcidToken($request->getContext(), $clonedUser));
} else {
$user->setGivenName($this->getData('givenName'), null);
$user->setFamilyName($this->getData('familyName'), null);
$user->setPreferredPublicName($this->getData('preferredPublicName'), null);
}

parent::execute(...$functionArgs);
}
Expand Down
68 changes: 68 additions & 0 deletions jobs/orcid/RevokeOrcidToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

/**
* @file jobs/orcid/RevokeOrcidToken.php
*
* Copyright (c) 2014-2024 Simon Fraser University
* Copyright (c) 2000-2024 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DepositOrcidSubmission
*
* @ingroup jobs
*
* @brief Job to revoke a user's ORCID access token for the application.
*/

namespace pkp\jobs\orcid;

use APP\core\Application;
use GuzzleHttp\Exception\ClientException;
use PKP\context\Context;
use PKP\identity\Identity;
use PKP\jobs\BaseJob;
use PKP\orcid\OrcidManager;

class RevokeOrcidToken extends BaseJob
{
public function __construct(
private readonly Context $context,
private readonly Identity $user
) {
parent::__construct();
}

/**
* @inheritDoc
*/
public function handle()
{
$token = $this->user->getData('orcidAccessToken');
$httpClient = Application::get()->getHttpClient();
$headers = ['Accept' => 'application/json'];

$postData = [
'token' => $token,
'client_id' => OrcidManager::getClientId($this->context),
'client_secret' => OrcidManager::getClientSecret($this->context)
];

try {
$httpClient->request(
'POST',
OrcidManager::getTokenRevocationUrl(),
[
'headers' => $headers,
'form_params' => $postData,
],
);

OrcidManager::logInfo('Token revoked for user, with ID: ' . $this->user->getId());
} catch (ClientException $exception) {
$this->fail($exception);
$httpStatus = $exception->getCode();
$error = json_decode($exception->getResponse()->getBody(), true);
OrcidManager::logError("ORCID token revocation failed with status {$httpStatus}. Error: " . $error['error_description']);
}
}
}
12 changes: 11 additions & 1 deletion templates/form/orcidProfile.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@

{capture name=orcidLink assign=orcidLink}
{if $orcidAuthenticated}
<a href="{$orcid}" target="_blank">{$orcidIcon}{$orcid}</a>
<a href="{$orcid}" target="_blank" id='orcid-link' >{$orcidIcon}{$orcid}</a>
<style>
#orcid-link {
display: flex;
gap: 0.5rem;
}
#orcid-link svg {
width: 2rem;
height: 1.5rem;
}
</style>
{else}
<a href="{$orcid}" target="_blank">{$orcid}</a>&nbsp;{$orcidButton}
{/if}
Expand Down
17 changes: 16 additions & 1 deletion templates/user/identityForm.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
$(function() {ldelim}
// Attach the form handler.
$('#identityForm').pkpHandler('$.pkp.controllers.form.AjaxFormHandler');
$('#deleteOrcidButton').on('click', function(e) {
const isModalConfirmTrigger = !e.originalEvent;
// Only execute logic when button was clicked via ButtonConfirmationModalHandler
if(isModalConfirmTrigger){
$('#identityForm').append('<input type="checkbox" id="removeOrcidId" name="removeOrcidId" checked value="true"/>');
$('#identityForm').submit();
$('#removeOrcidId').remove();
}
});
{rdelim});
</script>

Expand Down Expand Up @@ -44,8 +54,13 @@
{* FIXME: The form element is still required for "connect ORCID" functionality to work. *}
{fbvFormSection}
{fbvElement type="text" label="user.orcid" name="orcid" id="orcid" value=$orcid maxlength="46"}

{include file="form/orcidProfile.tpl"}
{include file="linkAction/buttonConfirmationLinkAction.tpl" titleIcon="modal_delete" buttonSelector="#deleteOrcidButton" dialogText="orcid.field.deleteOrcidModal.message"}
{if $orcidAuthenticated}
{fbvElement type="button" id="deleteOrcidButton" label="common.delete" class="pkp_button pkp_button_offset"}
{/if}
{/fbvFormSection}
{include file="form/orcidProfile.tpl"}
{/if}

<p>
Expand Down

0 comments on commit 98df0e5

Please sign in to comment.