From d101d404efe8eea12fabc74a8d906563888de1e8 Mon Sep 17 00:00:00 2001 From: Nick Vanpraet Date: Thu, 7 Apr 2022 16:26:05 +0200 Subject: [PATCH 1/6] Add support for client credentials grant --- README.md | 50 +++++++ lib/Auth/TwoLeggedOAuth2.php | 270 +++++++++++++++++++++++++++++++++++ 2 files changed, 320 insertions(+) create mode 100755 lib/Auth/TwoLeggedOAuth2.php diff --git a/README.md b/README.md index 6eabaa1b..c07bbd7c 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,56 @@ try { } ``` +### Using Two-Legged Authentication (Oauth2 Client Credentials) Instead +The above method uses authorization code flow for Oauth2. Client Credentials is the preferred method of +authentication when the use-case is application to application, where any actions +are triggered by the application itself and not a user taking an action (e.g. cleanup during cron). + +```php +newAuth() will accept an array of Auth settings +$settings = [ + 'AuthMethod' => 'TwoLeggedOAuth2', + 'clientKey' => '', + 'clientSecret' => '', + 'baseUrl' => '', +]; + +/* +// If you already have the access token, et al, pass them in as well to prevent the need for reauthorization +$settings['accessToken'] = $accessToken; +$settings['accessTokenExpires'] = $accessTokenExpires; //UNIX timestamp +*/ + +// Initiate the auth object +$initAuth = new ApiAuth(); +$auth = $initAuth->newAuth($settings, $settings['AuthMethod']); + +if (!$auth->isAuthorized()) { + $auth->requestAccessToken(); + // $accessTokenData will have the following keys: + // access_token, expires, token_type + $accessTokenData = $auth->getAccessTokenData(); + + //store access token data however you want +} + +// Nothing else to do ... It's ready to use. +// Just pass the auth object to the API context you are creating. +``` + ### Using Basic Authentication Instead Instead of messing around with OAuth, you may simply elect to use BasicAuth instead. diff --git a/lib/Auth/TwoLeggedOAuth2.php b/lib/Auth/TwoLeggedOAuth2.php new file mode 100755 index 00000000..f571380c --- /dev/null +++ b/lib/Auth/TwoLeggedOAuth2.php @@ -0,0 +1,270 @@ +log('parameters did not include clientkey and/or clientSecret'); + throw new RequiredParameterMissingException('One or more required parameters was not supplied. Both clientKey and clientSecret required!'); + } + + if (empty($baseUrl)) { + //Throw exception if the required parameters were not found + $this->log('parameters did not include baseUrl'); + throw new RequiredParameterMissingException('One or more required parameters was not supplied. baseUrl required!'); + } + + $this->_client_id = $clientKey; + $this->_client_secret = $clientSecret; + $this->_access_token = $accessToken; + $this->_access_token_url = $baseUrl.'/oauth/v2/token'; + + if (!empty($accessToken)) { + $this->setAccessTokenDetails([ + 'access_token' => $accessToken, + 'expires' => $accessTokenExpires, + ]); + } + } + + /** + * Check to see if the access token was updated. + * + * @return bool + */ + public function accessTokenUpdated() + { + return $this->_access_token_updated; + } + + /** + * Returns access token data. + * + * @return array + */ + public function getAccessTokenData() + { + return [ + 'access_token' => $this->_access_token, + 'expires' => $this->_expires, + 'token_type' => $this->_token_type, + ]; + } + + /** + * {@inheritdoc} + */ + public function isAuthorized() + { + $this->log('isAuthorized()'); + + return $this->validateAccessToken(); + } + + /** + * Set an existing/already retrieved access token. + * + * @return $this + */ + public function setAccessTokenDetails(array $accessTokenDetails) + { + $this->_access_token = $accessTokenDetails['access_token'] ?? null; + $this->_expires = $accessTokenDetails['expires'] ?? null; + + return $this; + } + + /** + * Validate existing access token. + * + * @return bool + */ + public function validateAccessToken() + { + $this->log('validateAccessToken()'); + + //Check to see if token in session has expired + if (strlen($this->_access_token) > 0 && !empty($this->_expires) && $this->_expires < (time() + 10)) { + $this->log('access token expired'); + + return false; + } + + //Check for existing access token + if (strlen($this->_access_token) > 0) { + $this->log('has valid access token'); + + return true; + } + + //If there is no existing access token, it can't be valid + return false; + } + + /** + * @param $isPost + * @param $parameters + * + * @return array + */ + protected function getQueryParameters($isPost, $parameters) + { + $query = parent::getQueryParameters($isPost, $parameters); + + if (isset($parameters['file'])) { + //Mautic's OAuth2 server does not recognize multipart forms so we have to append the access token as part of the URL + $query['access_token'] = $parameters['access_token']; + } + + return $query; + } + + /** + * @param $url + * @param array $method + * + * @return array + */ + protected function prepareRequest($url, array $headers, array $parameters, $method, array $settings) + { + if ($this->isAuthorized()) { + $headers = array_merge($headers, ['Authorization: Bearer '.$this->_access_token]); + } + + return [$headers, $parameters]; + } + + /** + * Request access token. + * + * @return bool + * + * @throws IncorrectParametersReturnedException|\Mautic\Exception\UnexpectedResponseFormatException + */ + public function requestAccessToken() + { + $this->log('requestAccessToken()'); + + $parameters = [ + 'client_id' => $this->_client_id, + 'client_secret' => $this->_client_secret, + 'grant_type' => 'client_credentials', + ]; + + //Make the request + $params = $this->makeRequest($this->_access_token_url, $parameters, 'POST'); + + //Add the token to session + if (is_array($params)) { + if (isset($params['access_token']) && isset($params['expires_in'])) { + $this->log('access token set as '.$params['access_token']); + + $this->_access_token = $params['access_token']; + $this->_expires = time() + $params['expires_in']; + $this->_token_type = (isset($params['token_type'])) ? $params['token_type'] : null; + $this->_access_token_updated = true; + + if ($this->_debug) { + $_SESSION['oauth']['debug']['tokens']['access_token'] = $params['access_token']; + $_SESSION['oauth']['debug']['tokens']['expires_in'] = $params['expires_in']; + $_SESSION['oauth']['debug']['tokens']['token_type'] = $params['token_type']; + } + + return true; + } + } + + $this->log('response did not have an access token'); + + if ($this->_debug) { + $_SESSION['oauth']['debug']['response'] = $params; + } + + if (is_array($params)) { + if (isset($params['errors'])) { + $errors = []; + foreach ($params['errors'] as $error) { + $errors[] = $error['message']; + } + $response = implode('; ', $errors); + } else { + $response = print_r($params, true); + } + } else { + $response = $params; + } + + throw new IncorrectParametersReturnedException('Incorrect access token parameters returned: '.$response); + } +} From 39176cbca21bb07c3bbd0265db285383dafcef24 Mon Sep 17 00:00:00 2001 From: Nick Vanpraet Date: Tue, 7 Jan 2025 11:25:50 +0100 Subject: [PATCH 2/6] Fix notice, phpcs changes --- lib/Auth/TwoLeggedOAuth2.php | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/Auth/TwoLeggedOAuth2.php b/lib/Auth/TwoLeggedOAuth2.php index f571380c..c9a06f07 100755 --- a/lib/Auth/TwoLeggedOAuth2.php +++ b/lib/Auth/TwoLeggedOAuth2.php @@ -74,13 +74,13 @@ public function setup( $accessTokenExpires = null ) { if (empty($clientKey) || empty($clientSecret)) { - //Throw exception if the required parameters were not found + // Throw exception if the required parameters were not found $this->log('parameters did not include clientkey and/or clientSecret'); throw new RequiredParameterMissingException('One or more required parameters was not supplied. Both clientKey and clientSecret required!'); } if (empty($baseUrl)) { - //Throw exception if the required parameters were not found + // Throw exception if the required parameters were not found $this->log('parameters did not include baseUrl'); throw new RequiredParameterMissingException('One or more required parameters was not supplied. baseUrl required!'); } @@ -154,27 +154,27 @@ public function validateAccessToken() { $this->log('validateAccessToken()'); - //Check to see if token in session has expired - if (strlen($this->_access_token) > 0 && !empty($this->_expires) && $this->_expires < (time() + 10)) { + // Check to see if token in session has expired + if (!empty($this->_access_token) && !empty($this->_expires) && $this->_expires < (time() + 10)) { $this->log('access token expired'); return false; } - //Check for existing access token - if (strlen($this->_access_token) > 0) { + // Check for existing access token + if (!empty($this->_access_token)) { $this->log('has valid access token'); return true; } - //If there is no existing access token, it can't be valid + // If there is no existing access token, it can't be valid return false; } /** - * @param $isPost - * @param $parameters + * @param bool $isPost + * @param array $parameters * * @return array */ @@ -183,7 +183,7 @@ protected function getQueryParameters($isPost, $parameters) $query = parent::getQueryParameters($isPost, $parameters); if (isset($parameters['file'])) { - //Mautic's OAuth2 server does not recognize multipart forms so we have to append the access token as part of the URL + // Mautic's OAuth2 server does not recognize multipart forms so we have to append the access token as part of the URL $query['access_token'] = $parameters['access_token']; } @@ -191,8 +191,8 @@ protected function getQueryParameters($isPost, $parameters) } /** - * @param $url - * @param array $method + * @param string $url + * @param array $method * * @return array */ @@ -222,10 +222,10 @@ public function requestAccessToken() 'grant_type' => 'client_credentials', ]; - //Make the request + // Make the request $params = $this->makeRequest($this->_access_token_url, $parameters, 'POST'); - //Add the token to session + // Add the token to session if (is_array($params)) { if (isset($params['access_token']) && isset($params['expires_in'])) { $this->log('access token set as '.$params['access_token']); From 7610eeecd0c9eff088a10d175173b87f2b0420af Mon Sep 17 00:00:00 2001 From: Nick Vanpraet Date: Thu, 19 Jun 2025 12:57:39 +0200 Subject: [PATCH 3/6] Add typehinting where possible Some would require changes to the parent, out of scope for this PR --- lib/Auth/TwoLeggedOAuth2.php | 95 +++++++++++++----------------------- 1 file changed, 33 insertions(+), 62 deletions(-) diff --git a/lib/Auth/TwoLeggedOAuth2.php b/lib/Auth/TwoLeggedOAuth2.php index c9a06f07..e891037f 100755 --- a/lib/Auth/TwoLeggedOAuth2.php +++ b/lib/Auth/TwoLeggedOAuth2.php @@ -1,5 +1,7 @@ log('parameters did not include clientkey and/or clientSecret'); @@ -92,8 +76,8 @@ public function setup( if (!empty($accessToken)) { $this->setAccessTokenDetails([ - 'access_token' => $accessToken, - 'expires' => $accessTokenExpires, + 'access_token' => $accessToken, + 'expires' => $accessTokenExpires, ]); } } @@ -110,22 +94,17 @@ public function accessTokenUpdated() /** * Returns access token data. - * - * @return array */ - public function getAccessTokenData() + public function getAccessTokenData(): array { return [ - 'access_token' => $this->_access_token, - 'expires' => $this->_expires, - 'token_type' => $this->_token_type, + 'access_token' => $this->_access_token, + 'expires' => $this->_expires, + 'token_type' => $this->_token_type, ]; } - /** - * {@inheritdoc} - */ - public function isAuthorized() + public function isAuthorized(): bool { $this->log('isAuthorized()'); @@ -137,7 +116,7 @@ public function isAuthorized() * * @return $this */ - public function setAccessTokenDetails(array $accessTokenDetails) + public function setAccessTokenDetails(array $accessTokenDetails): static { $this->_access_token = $accessTokenDetails['access_token'] ?? null; $this->_expires = $accessTokenDetails['expires'] ?? null; @@ -147,14 +126,12 @@ public function setAccessTokenDetails(array $accessTokenDetails) /** * Validate existing access token. - * - * @return bool */ - public function validateAccessToken() + public function validateAccessToken(): bool { $this->log('validateAccessToken()'); - // Check to see if token in session has expired + // Check to see if token in session has expired (or will in a few seconds) if (!empty($this->_access_token) && !empty($this->_expires) && $this->_expires < (time() + 10)) { $this->log('access token expired'); @@ -175,10 +152,8 @@ public function validateAccessToken() /** * @param bool $isPost * @param array $parameters - * - * @return array */ - protected function getQueryParameters($isPost, $parameters) + protected function getQueryParameters($isPost, $parameters): array { $query = parent::getQueryParameters($isPost, $parameters); @@ -193,10 +168,8 @@ protected function getQueryParameters($isPost, $parameters) /** * @param string $url * @param array $method - * - * @return array */ - protected function prepareRequest($url, array $headers, array $parameters, $method, array $settings) + protected function prepareRequest($url, array $headers, array $parameters, $method, array $settings): array { if ($this->isAuthorized()) { $headers = array_merge($headers, ['Authorization: Bearer '.$this->_access_token]); @@ -208,18 +181,16 @@ protected function prepareRequest($url, array $headers, array $parameters, $meth /** * Request access token. * - * @return bool - * * @throws IncorrectParametersReturnedException|\Mautic\Exception\UnexpectedResponseFormatException */ - public function requestAccessToken() + public function requestAccessToken(): bool { $this->log('requestAccessToken()'); $parameters = [ - 'client_id' => $this->_client_id, - 'client_secret' => $this->_client_secret, - 'grant_type' => 'client_credentials', + 'client_id' => $this->_client_id, + 'client_secret' => $this->_client_secret, + 'grant_type' => 'client_credentials', ]; // Make the request @@ -231,7 +202,7 @@ public function requestAccessToken() $this->log('access token set as '.$params['access_token']); $this->_access_token = $params['access_token']; - $this->_expires = time() + $params['expires_in']; + $this->_expires = time() + (int) $params['expires_in']; $this->_token_type = (isset($params['token_type'])) ? $params['token_type'] : null; $this->_access_token_updated = true; From 628c31fc80fb190435d644141a3804b6d3f41a53 Mon Sep 17 00:00:00 2001 From: Nick Vanpraet Date: Thu, 19 Jun 2025 16:08:05 +0200 Subject: [PATCH 4/6] Add @internal annotation --- lib/Auth/TwoLeggedOAuth2.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Auth/TwoLeggedOAuth2.php b/lib/Auth/TwoLeggedOAuth2.php index e891037f..84abd432 100755 --- a/lib/Auth/TwoLeggedOAuth2.php +++ b/lib/Auth/TwoLeggedOAuth2.php @@ -8,7 +8,7 @@ use Mautic\Exception\RequiredParameterMissingException; /** - * OAuth Client modified from https://code.google.com/p/simple-php-oauth/. + * @internal OAuth Client modified from https://code.google.com/p/simple-php-oauth/. */ class TwoLeggedOAuth2 extends AbstractAuth { From b46496a7f6116cad6a46dc5c544e59c35769743d Mon Sep 17 00:00:00 2001 From: Nick Vanpraet Date: Tue, 15 Jul 2025 11:43:23 +0200 Subject: [PATCH 5/6] Correct typehinting --- lib/Auth/TwoLeggedOAuth2.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Auth/TwoLeggedOAuth2.php b/lib/Auth/TwoLeggedOAuth2.php index 84abd432..aa714902 100755 --- a/lib/Auth/TwoLeggedOAuth2.php +++ b/lib/Auth/TwoLeggedOAuth2.php @@ -55,7 +55,7 @@ public function setup( ?string $clientKey = null, ?string $clientSecret = null, ?string $accessToken = null, - ?string $accessTokenExpires = null, + ?int $accessTokenExpires = null, ): void { if (empty($clientKey) || empty($clientSecret)) { // Throw exception if the required parameters were not found @@ -119,7 +119,7 @@ public function isAuthorized(): bool public function setAccessTokenDetails(array $accessTokenDetails): static { $this->_access_token = $accessTokenDetails['access_token'] ?? null; - $this->_expires = $accessTokenDetails['expires'] ?? null; + $this->_expires = isset($accessTokenDetails['expires']) ? (int) $accessTokenDetails['expires'] : null; return $this; } From 9ebb3367288dbc38eb9eb2f10ab3160c79d4f791 Mon Sep 17 00:00:00 2001 From: Nick Vanpraet Date: Tue, 15 Jul 2025 11:57:03 +0200 Subject: [PATCH 6/6] phpcs --- lib/Auth/TwoLeggedOAuth2.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Auth/TwoLeggedOAuth2.php b/lib/Auth/TwoLeggedOAuth2.php index aa714902..8b1a398a 100755 --- a/lib/Auth/TwoLeggedOAuth2.php +++ b/lib/Auth/TwoLeggedOAuth2.php @@ -55,7 +55,7 @@ public function setup( ?string $clientKey = null, ?string $clientSecret = null, ?string $accessToken = null, - ?int $accessTokenExpires = null, + ?int $accessTokenExpires = null, ): void { if (empty($clientKey) || empty($clientSecret)) { // Throw exception if the required parameters were not found @@ -119,7 +119,7 @@ public function isAuthorized(): bool public function setAccessTokenDetails(array $accessTokenDetails): static { $this->_access_token = $accessTokenDetails['access_token'] ?? null; - $this->_expires = isset($accessTokenDetails['expires']) ? (int) $accessTokenDetails['expires'] : null; + $this->_expires = isset($accessTokenDetails['expires']) ? (int) $accessTokenDetails['expires'] : null; return $this; }