Skip to content

Commit

Permalink
feat: handle Client Credential OAuth2 authentication method
Browse files Browse the repository at this point in the history
Signed-off-by: Adrien Barreau <[email protected]>
  • Loading branch information
deathiop committed Sep 25, 2024
1 parent f417038 commit e47594f
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 34 deletions.
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@
],
"require": {
"php": ">=7.4",
"ext-json": "*",
"guzzlehttp/guzzle": "^6.0||^7.0",
"ext-json": "*"
"league/oauth2-client": "^2.7"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.3.1",
"phpdocumentor/shim": "^3",
"phpunit/phpunit": "^9.5",
"phpunit/phpunit": "^9.6",
"squizlabs/php_codesniffer": "^3.6"
},
"autoload": {
Expand Down
68 changes: 55 additions & 13 deletions src/Api.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?php
# Copyright (c) 2013-2023, OVH SAS.
# Copyright (c) 2013-2024, OVH SAS.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -37,6 +37,8 @@
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ResponseInterface;

include_once('OAuth2.php');

/**
* Wrapper to manage login and exchanges with simpliest Ovh API
*
Expand Down Expand Up @@ -66,6 +68,12 @@ class Api
'runabove-ca' => 'https://api.runabove.com/1.0',
];

private static $OAUTH2_TOKEN_URLS = [
"ovh-eu" => "https://www.ovh.com/auth/oauth2/token",
"ovh-ca" => "https://ca.ovh.com/auth/oauth2/token",
"ovh-us" => "https://us.ovhcloud.com/auth/oauth2/token",
];

/**
* Contain endpoint selected to choose API
*
Expand Down Expand Up @@ -108,6 +116,13 @@ class Api
*/
private ?Client $http_client;

/**
* OAuth2 wrapper if built with `withOAuth2`
*
* @var \Ovh\OAuth2
*/
private ?OAuth2 $oauth2;

/**
* Construct a new wrapper instance
*
Expand Down Expand Up @@ -154,6 +169,26 @@ public function __construct(
$this->application_secret = $application_secret;
$this->http_client = $http_client;
$this->consumer_key = $consumer_key;
$this->oauth2 = null;
}

/**
* Alternative constructor to build a client using OAuth2
*
* @throws Exceptions\InvalidParameterException if one parameter is missing or with bad value
* @return Ovh\Api
*/
public static function withOAuth2($clientId, $clientSecret, $apiEndpoint)
{
if (!array_key_exists($apiEndpoint, self::$OAUTH2_TOKEN_URLS)) {
throw new Exceptions\InvalidParameterException(
"OAuth2 authentication is not compatible with endpoint $apiEndpoint (it can only be used with ovh-eu, ovh-ca and ovh-us)"
);
}

$instance = new self("", "", $apiEndpoint);
$instance->oauth2 = new Oauth2($clientId, $clientSecret, self::$OAUTH2_TOKEN_URLS[$apiEndpoint]);
return $instance;
}

/**
Expand Down Expand Up @@ -298,22 +333,29 @@ protected function rawCall($method, $path, $content = null, $is_authenticated =
}
$headers['Content-Type'] = 'application/json; charset=utf-8';

$headers['X-Ovh-Application'] = $this->application_key ?? '';
if ($is_authenticated) {
if (!isset($this->time_delta)) {
$this->calculateTimeDelta();
}
$now = time() + $this->time_delta;
if (!is_null($this->oauth2)) {
$headers['Authorization'] = $this->oauth2->getAuthorizationHeader();
} else {
$headers['X-Ovh-Application'] = $this->application_key ?? '';

if (!isset($this->time_delta)) {
$this->calculateTimeDelta();
}
$now = time() + $this->time_delta;

$headers['X-Ovh-Timestamp'] = $now;
$headers['X-Ovh-Timestamp'] = $now;

if (isset($this->consumer_key)) {
$toSign = $this->application_secret . '+' . $this->consumer_key . '+' . $method
. '+' . $url . '+' . $body . '+' . $now;
$signature = '$1$' . sha1($toSign);
$headers['X-Ovh-Consumer'] = $this->consumer_key;
$headers['X-Ovh-Signature'] = $signature;
if (isset($this->consumer_key)) {
$toSign = $this->application_secret . '+' . $this->consumer_key . '+' . $method
. '+' . $url . '+' . $body . '+' . $now;
$signature = '$1$' . sha1($toSign);
$headers['X-Ovh-Consumer'] = $this->consumer_key;
$headers['X-Ovh-Signature'] = $signature;
}
}
} else {
$headers['X-Ovh-Application'] = $this->application_key ?? '';
}

/** @var Response $response */
Expand Down
46 changes: 46 additions & 0 deletions src/Exceptions/OAuth2FailureException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
# Copyright (c) 2013-2023, OVH SAS.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of OVH SAS nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/**
* This file contains code about \Ovh\Exceptions\InvalidParameterException class
*/

namespace Ovh\Exceptions;

use Exception;

/**
* InvalidParameterException exception is thrown when a request failed because of a bad client configuration
*
* InvalidParameterException appears when the request failed because of a bad parameter from
* the client request.
*
* @package Ovh
* @category Exceptions
*/
class OAuth2FailureException extends Exception
{
}
67 changes: 67 additions & 0 deletions src/OAuth2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
# Copyright (c) 2013-2024, OVH SAS.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of OVH SAS nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

namespace Ovh;

use League\OAuth2\Client\OptionProvider\PostAuthOptionProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\GenericProvider;
use UnexpectedValueException;

class OAuth2
{
private $provider;
private $token;

public function __construct($clientId, $clientSecret, $tokenUrl)
{

$this->provider = new \League\OAuth2\Client\Provider\GenericProvider([
'clientId' => $clientId,
'clientSecret' => $clientSecret,
# Do not configure `scopes` here as this GenericProvider ignores it when using client credentials flow
'urlAccessToken' => $tokenUrl,
'urlAuthorize' => null, # GenericProvider wants it but OVHcloud doesn't provide it, as it's not needed for client credentials flow
'urlResourceOwnerDetails' => null, # GenericProvider wants it but OVHcloud doesn't provide it, as it's not needed for client credentials flow
]);
}

public function getAuthorizationHeader()
{
if (is_null($this->token) ||
$this->token->hasExpired() ||
$this->token->getExpires() - 10 <= time()) {
try {
$this->token = $this->provider->getAccessToken('client_credentials', ['scope' => 'all']);
} catch (UnexpectedValueException | IdentityProviderException $e) {
throw new Exceptions\OAuth2FailureException('OAuth2 failure: ' . $e->getMessage(), $e->getCode(), $e);
}
}

return 'Bearer ' . $this->token->getToken();
}
}
Loading

0 comments on commit e47594f

Please sign in to comment.