Skip to content

Commit 91cdd80

Browse files
committed
feat: handle Client Credential OAuth2 authentication method
Signed-off-by: Adrien Barreau <[email protected]>
1 parent f417038 commit 91cdd80

File tree

5 files changed

+328
-35
lines changed

5 files changed

+328
-35
lines changed

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
],
1919
"require": {
2020
"php": ">=7.4",
21+
"ext-json": "*",
2122
"guzzlehttp/guzzle": "^6.0||^7.0",
22-
"ext-json": "*"
23+
"league/oauth2-client": "^2.7"
2324
},
2425
"require-dev": {
2526
"php-parallel-lint/php-parallel-lint": "^1.3.1",
2627
"phpdocumentor/shim": "^3",
27-
"phpunit/phpunit": "^9.5",
28+
"phpunit/phpunit": "^9.6",
2829
"squizlabs/php_codesniffer": "^3.6"
2930
},
3031
"autoload": {

src/Api.php

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
<?php
2-
# Copyright (c) 2013-2023, OVH SAS.
1+
<?php // phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
2+
# Copyright (c) 2013-2024, OVH SAS.
33
# All rights reserved.
44
#
55
# Redistribution and use in source and binary forms, with or without
@@ -37,6 +37,8 @@
3737
use GuzzleHttp\Psr7\Response;
3838
use Psr\Http\Message\ResponseInterface;
3939

40+
require_once('OAuth2.php');
41+
4042
/**
4143
* Wrapper to manage login and exchanges with simpliest Ovh API
4244
*
@@ -66,6 +68,12 @@ class Api
6668
'runabove-ca' => 'https://api.runabove.com/1.0',
6769
];
6870

71+
private static $OAUTH2_TOKEN_URLS = [
72+
"ovh-eu" => "https://www.ovh.com/auth/oauth2/token",
73+
"ovh-ca" => "https://ca.ovh.com/auth/oauth2/token",
74+
"ovh-us" => "https://us.ovhcloud.com/auth/oauth2/token",
75+
];
76+
6977
/**
7078
* Contain endpoint selected to choose API
7179
*
@@ -108,6 +116,13 @@ class Api
108116
*/
109117
private ?Client $http_client;
110118

119+
/**
120+
* OAuth2 wrapper if built with `withOAuth2`
121+
*
122+
* @var \Ovh\OAuth2
123+
*/
124+
private ?OAuth2 $oauth2;
125+
111126
/**
112127
* Construct a new wrapper instance
113128
*
@@ -154,6 +169,26 @@ public function __construct(
154169
$this->application_secret = $application_secret;
155170
$this->http_client = $http_client;
156171
$this->consumer_key = $consumer_key;
172+
$this->oauth2 = null;
173+
}
174+
175+
/**
176+
* Alternative constructor to build a client using OAuth2
177+
*
178+
* @throws Exceptions\InvalidParameterException if one parameter is missing or with bad value
179+
* @return Ovh\Api
180+
*/
181+
public static function withOAuth2($clientId, $clientSecret, $apiEndpoint)
182+
{
183+
if (!array_key_exists($apiEndpoint, self::$OAUTH2_TOKEN_URLS)) {
184+
throw new Exceptions\InvalidParameterException(
185+
"OAuth2 authentication is not compatible with endpoint $apiEndpoint (it can only be used with ovh-eu, ovh-ca and ovh-us)"
186+
);
187+
}
188+
189+
$instance = new self("", "", $apiEndpoint);
190+
$instance->oauth2 = new Oauth2($clientId, $clientSecret, self::$OAUTH2_TOKEN_URLS[$apiEndpoint]);
191+
return $instance;
157192
}
158193

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

301-
$headers['X-Ovh-Application'] = $this->application_key ?? '';
302336
if ($is_authenticated) {
303-
if (!isset($this->time_delta)) {
304-
$this->calculateTimeDelta();
305-
}
306-
$now = time() + $this->time_delta;
337+
if (!is_null($this->oauth2)) {
338+
$headers['Authorization'] = $this->oauth2->getAuthorizationHeader();
339+
} else {
340+
$headers['X-Ovh-Application'] = $this->application_key ?? '';
341+
342+
if (!isset($this->time_delta)) {
343+
$this->calculateTimeDelta();
344+
}
345+
$now = time() + $this->time_delta;
307346

308-
$headers['X-Ovh-Timestamp'] = $now;
347+
$headers['X-Ovh-Timestamp'] = $now;
309348

310-
if (isset($this->consumer_key)) {
311-
$toSign = $this->application_secret . '+' . $this->consumer_key . '+' . $method
312-
. '+' . $url . '+' . $body . '+' . $now;
313-
$signature = '$1$' . sha1($toSign);
314-
$headers['X-Ovh-Consumer'] = $this->consumer_key;
315-
$headers['X-Ovh-Signature'] = $signature;
349+
if (isset($this->consumer_key)) {
350+
$toSign = $this->application_secret . '+' . $this->consumer_key . '+' . $method
351+
. '+' . $url . '+' . $body . '+' . $now;
352+
$signature = '$1$' . sha1($toSign);
353+
$headers['X-Ovh-Consumer'] = $this->consumer_key;
354+
$headers['X-Ovh-Signature'] = $signature;
355+
}
316356
}
357+
} else {
358+
$headers['X-Ovh-Application'] = $this->application_key ?? '';
317359
}
318360

319361
/** @var Response $response */
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
# Copyright (c) 2013-2023, OVH SAS.
3+
# All rights reserved.
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are met:
7+
#
8+
# * Redistributions of source code must retain the above copyright
9+
# notice, this list of conditions and the following disclaimer.
10+
# * Redistributions in binary form must reproduce the above copyright
11+
# notice, this list of conditions and the following disclaimer in the
12+
# documentation and/or other materials provided with the distribution.
13+
# * Neither the name of OVH SAS nor the
14+
# names of its contributors may be used to endorse or promote products
15+
# derived from this software without specific prior written permission.
16+
#
17+
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
18+
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20+
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
21+
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27+
/**
28+
* This file contains code about \Ovh\Exceptions\InvalidParameterException class
29+
*/
30+
31+
namespace Ovh\Exceptions;
32+
33+
use Exception;
34+
35+
/**
36+
* InvalidParameterException exception is thrown when a request failed because of a bad client configuration
37+
*
38+
* InvalidParameterException appears when the request failed because of a bad parameter from
39+
* the client request.
40+
*
41+
* @package Ovh
42+
* @category Exceptions
43+
*/
44+
class OAuth2FailureException extends Exception
45+
{
46+
}

src/OAuth2.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
# Copyright (c) 2013-2024, OVH SAS.
3+
# All rights reserved.
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are met:
7+
#
8+
# * Redistributions of source code must retain the above copyright
9+
# notice, this list of conditions and the following disclaimer.
10+
# * Redistributions in binary form must reproduce the above copyright
11+
# notice, this list of conditions and the following disclaimer in the
12+
# documentation and/or other materials provided with the distribution.
13+
# * Neither the name of OVH SAS nor the
14+
# names of its contributors may be used to endorse or promote products
15+
# derived from this software without specific prior written permission.
16+
#
17+
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
18+
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20+
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
21+
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27+
28+
namespace Ovh;
29+
30+
use League\OAuth2\Client\OptionProvider\PostAuthOptionProvider;
31+
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
32+
use League\OAuth2\Client\Provider\GenericProvider;
33+
use UnexpectedValueException;
34+
35+
class OAuth2
36+
{
37+
private $provider;
38+
private $token;
39+
40+
public function __construct($clientId, $clientSecret, $tokenUrl)
41+
{
42+
43+
$this->provider = new \League\OAuth2\Client\Provider\GenericProvider([
44+
'clientId' => $clientId,
45+
'clientSecret' => $clientSecret,
46+
# Do not configure `scopes` here as this GenericProvider ignores it when using client credentials flow
47+
'urlAccessToken' => $tokenUrl,
48+
'urlAuthorize' => null, # GenericProvider wants it but OVHcloud doesn't provide it, as it's not needed for client credentials flow
49+
'urlResourceOwnerDetails' => null, # GenericProvider wants it but OVHcloud doesn't provide it, as it's not needed for client credentials flow
50+
]);
51+
}
52+
53+
public function getAuthorizationHeader()
54+
{
55+
if (is_null($this->token) ||
56+
$this->token->hasExpired() ||
57+
$this->token->getExpires() - 10 <= time()) {
58+
try {
59+
$this->token = $this->provider->getAccessToken('client_credentials', ['scope' => 'all']);
60+
} catch (UnexpectedValueException | IdentityProviderException $e) {
61+
throw new Exceptions\OAuth2FailureException('OAuth2 failure: ' . $e->getMessage(), $e->getCode(), $e);
62+
}
63+
}
64+
65+
return 'Bearer ' . $this->token->getToken();
66+
}
67+
}

0 commit comments

Comments
 (0)