From cdcd2f7199e45fd187702897b7815e2fb44c826c Mon Sep 17 00:00:00 2001 From: Seamus Lee Date: Mon, 3 Apr 2023 15:08:00 +1000 Subject: [PATCH] [REF] Add in new JWT Entity with Create and Validate methods to permit creation and validation of JWTs via the API --- ext/authx/Civi/Api4/Action/JWT/Create.php | 69 ++++++++++++++ ext/authx/Civi/Api4/Action/JWT/Validate.php | 55 ++++++++++++ ext/authx/Civi/Api4/JWT.php | 60 +++++++++++++ ext/authx/authx.php | 1 + ext/authx/tests/phpunit/api/v4/JwtApiTest.php | 90 +++++++++++++++++++ 5 files changed, 275 insertions(+) create mode 100644 ext/authx/Civi/Api4/Action/JWT/Create.php create mode 100644 ext/authx/Civi/Api4/Action/JWT/Validate.php create mode 100644 ext/authx/Civi/Api4/JWT.php create mode 100644 ext/authx/tests/phpunit/api/v4/JwtApiTest.php diff --git a/ext/authx/Civi/Api4/Action/JWT/Create.php b/ext/authx/Civi/Api4/Action/JWT/Create.php new file mode 100644 index 0000000000..323dc8c894 --- /dev/null +++ b/ext/authx/Civi/Api4/Action/JWT/Create.php @@ -0,0 +1,69 @@ +ttl ?: 300; + $scope = $this->scope ?: 'authx'; + + $token = \Civi::service('crypto.jwt')->encode([ + 'exp' => time() + $ttl, + 'sub' => 'cid:' . $this->contactId, + 'scope' => $scope, + ]); + + $result[] = [ + 'token' => $token, + ]; + } + +} diff --git a/ext/authx/Civi/Api4/Action/JWT/Validate.php b/ext/authx/Civi/Api4/Action/JWT/Validate.php new file mode 100644 index 0000000000..4e08af7983 --- /dev/null +++ b/ext/authx/Civi/Api4/Action/JWT/Validate.php @@ -0,0 +1,55 @@ + 'script', + 'cred' => 'Bearer ' . $this->token, + 'siteKey' => NULL, + 'useSession' => FALSE, + ]); + $checkEvent = new CheckCredentialEvent($tgt->cred); + \Civi::dispatcher()->dispatch('civi.authx.checkCredential', $checkEvent); + + if ($checkEvent->getRejection()) { + throw new AuthxException($checkEvent->getRejection()); + } + + $result[] = $checkEvent->getPrincipal(); + } + +} diff --git a/ext/authx/Civi/Api4/JWT.php b/ext/authx/Civi/Api4/JWT.php new file mode 100644 index 0000000000..c666be2b0c --- /dev/null +++ b/ext/authx/Civi/Api4/JWT.php @@ -0,0 +1,60 @@ +setCheckPermissions($checkPermissions); + } + + /** + * @param bool $checkPermissions + * @return Action\JWT\validate + */ + public static function validate($checkPermissions = TRUE) { + return (new Action\JWT\Validate(__CLASS__, __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + + /** + * @param bool $checkPermissions + * @return Generic\BasicGetFieldsAction + */ + public static function getFields($checkPermissions = TRUE) { + return (new Generic\BasicGetFieldsAction(__CLASS__, __FUNCTION__, function() { + return []; + }))->setCheckPermissions($checkPermissions); + } + + public static function permissions() { + return [ + 'meta' => ['access CiviCRM'], + 'default' => ['administer CiviCRM'], + 'create' => ['generate JWT'], + 'validate' => [], + ]; + } + +} diff --git a/ext/authx/authx.php b/ext/authx/authx.php index 432afe1d89..e79ffcf634 100644 --- a/ext/authx/authx.php +++ b/ext/authx/authx.php @@ -124,6 +124,7 @@ function authx_civicrm_enable() { function authx_civicrm_permission(&$permissions) { $permissions['authenticate with password'] = E::ts('AuthX: Authenticate to services with password'); $permissions['authenticate with api key'] = E::ts('AuthX: Authenticate to services with API key'); + $permissions['generate JWT'] = E::ts('Authx: Permit the generation of JWTs via the API'); } // --- Functions below this ship commented out. Uncomment as required. --- diff --git a/ext/authx/tests/phpunit/api/v4/JwtApiTest.php b/ext/authx/tests/phpunit/api/v4/JwtApiTest.php new file mode 100644 index 0000000000..a818e354ae --- /dev/null +++ b/ext/authx/tests/phpunit/api/v4/JwtApiTest.php @@ -0,0 +1,90 @@ +installMe(__DIR__) + ->apply(); + } + + public function testJWTGenrateToken(): void { + $this->_apiversion = 4; + $contactRecord = $this->createTestRecord('Contact', ['contact_type' => 'Individual']); + $this->createLoggedInUser(); + $this->setPermissions([ + 'access CiviCRM', + ]); + try { + JWT::create()->setContactId($contactRecord['id'])->execute(); + $this->fail('JWT Should not be created as permission is not granted'); + } + catch (\Exception $e) { + } + $this->setPermissions([ + 'access CiviCRM', + 'generate JWT', + ]); + $jwt = JWT::create()->setContactId($contactRecord['id'])->execute(); + $this->assertNotEmpty($jwt[0]['token']); + } + + public function testJWTValidation(): void { + $this->_apiversion = 4; + $contactRecord = $this->createTestRecord('Contact', ['contact_type' => 'Individual']); + $this->createLoggedInUser(); + $this->setPermissions([ + 'access CiviCRM', + 'generate JWT', + ]); + $jwt = JWT::create()->setContactId($contactRecord['id'])->execute(); + $validate = JWT::validate()->setToken($jwt[0]['token'])->execute(); + $this->assertEquals('jwt', $validate[0]['credType']); + $this->assertEquals($contactRecord['id'], $validate[0]['contactId']); + $this->assertEquals('cid:' . $contactRecord['id'], $validate[0]['jwt']['sub']); + } + + /** + * Test that the JWT does not validate if expired + */ + public function testExpiredJWTValidation(): void { + $this->expectException(\Civi\Authx\AuthxException::class); + $this->expectExceptionMessage('Expired token'); + $this->_apiversion = 4; + $contactRecord = $this->createTestRecord('Contact', ['contact_type' => 'Individual']); + $this->createLoggedInUser(); + $this->setPermissions([ + 'access CiviCRM', + 'generate JWT', + ]); + $jwt = JWT::create()->setContactId($contactRecord['id'])->setTtl(5)->execute(); + sleep(10); + $validate = JWT::validate()->setToken($jwt[0]['token'])->execute(); + } + + /** + * Set ACL permissions, overwriting any existing ones. + * + * @param array $permissions + * Array of permissions e.g ['access CiviCRM','access CiviContribute'], + */ + protected function setPermissions(array $permissions): void { + \CRM_Core_Config::singleton()->userPermissionClass->permissions = $permissions; + } + +} -- 2.25.1