Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for client credentials grant #269

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?php

// Bootup the Composer autoloader
include __DIR__ . '/vendor/autoload.php';

use Mautic\Auth\ApiAuth;

session_start();

$publicKey = '';
$secretKey = '';
$callback = '';

// ApiAuth->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.

Expand Down
270 changes: 270 additions & 0 deletions lib/Auth/TwoLeggedOAuth2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
<?php

namespace Mautic\Auth;

use Mautic\Exception\IncorrectParametersReturnedException;
use Mautic\Exception\RequiredParameterMissingException;

/**
* OAuth Client modified from https://code.google.com/p/simple-php-oauth/.
*/
class TwoLeggedOAuth2 extends AbstractAuth
{
/**
* Access token URL.
*
* @var string
*/
protected $_access_token_url;

/**
* Access token returned by OAuth server.
*
* @var string
*/
protected $_access_token;

/**
* Consumer or client key.
*
* @var string
*/
protected $_client_id;

/**
* Consumer or client secret.
*
* @var string
*/
protected $_client_secret;

/**
* Unix timestamp for when token expires.
*
* @var string
*/
protected $_expires;

/**
* OAuth2 token type.
*
* @var string
*/
protected $_token_type = 'bearer';

/**
* Set to true if the access token was updated.
*
* @var bool
*/
protected $_access_token_updated = false;

/**
* @param string $baseUrl URL of the Mautic instance
* @param string $clientKey
* @param string $clientSecret
* @param string $accessToken
* @param string $accessTokenExpires
*/
public function setup(
$baseUrl = null,
$clientKey = null,
$clientSecret = null,
$accessToken = null,
$accessTokenExpires = null
) {
if (empty($clientKey) || empty($clientSecret)) {
// 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
$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 (!empty($this->_access_token) && !empty($this->_expires) && $this->_expires < (time() + 10)) {
$this->log('access token expired');

return false;
}

// 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
return false;
}

/**
* @param bool $isPost
* @param array $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 string $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);
}
}