Skip to content

Commit 57fe667

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

File tree

6 files changed

+359
-47
lines changed

6 files changed

+359
-47
lines changed

README.md

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# OVHcloud APIs lightweight PHP wrapper
22

3-
[![PHP Wrapper for OVH APIs](https://github.com/ovh/php-ovh/blob/master/img/logo.png)](https://packagist.org/packages/ovh/ovh)
3+
[![PHP Wrapper for OVHcloud APIs](https://github.com/ovh/php-ovh/blob/master/img/logo.png)](https://packagist.org/packages/ovh/ovh)
44

55
[![Source Code](https://img.shields.io/badge/source-ovh/php--ovh-blue.svg?style=flat-square)](https://github.com/ovh/php-ovh)
66
[![Build Status](https://img.shields.io/github/actions/workflow/status/ovh/php-ovh/ci.yaml?label=CI&logo=github&style=flat-square)](https://github.com/ovh/php-ovh/actions?query=workflow%3ACI)
@@ -38,7 +38,7 @@ echo 'Welcome '.$ovh->get('/me')['firstname'];
3838

3939
### Handle exceptions
4040

41-
Under the hood, ```php-ovh``` uses [Guzzle](http://docs.guzzlephp.org/en/latest/quickstart.html) by default to issue API requests.
41+
Under the hood, `php-ovh` uses [Guzzle](http://docs.guzzlephp.org/en/latest/quickstart.html) by default to issue API requests.
4242

4343
If everything goes well, it will return the response directly as shown in the examples above.
4444

@@ -89,7 +89,6 @@ After allowing access to his account, he will be redirected to your application.
8989

9090
See "OVHcloud API authentication" section below for more information about the authorization flow.
9191

92-
9392
```php
9493
use \Ovh\Api;
9594
session_start();
@@ -147,9 +146,29 @@ foreach ($servers as $server) {
147146

148147
### More code samples
149148

150-
Do you want to use OVH APIs? Maybe the script you want is already written in the [example part](examples/README.md) of this repository!
149+
Do you want to use OVHcloud APIs? Maybe the script you want is already written in the [example part](examples/README.md) of this repository!
150+
151+
## OAuth2 authentification
152+
153+
`php-ovh` supports two forms of authentication:
154+
155+
* OAuth2, using scopped service accounts, and compatible with OVHcloud IAM
156+
* application key & application secret & consumer key (covered in the next chapter)
157+
158+
For OAuth2, first, you need to generate a pair of valid `client_id` and `client_secret`: you can proceed by
159+
[following this documentation](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343).
160+
161+
Once you have retrieved your `client_id` and `client_secret`, you can instantiate an API client using:
162+
163+
```php
164+
use \Ovh\Api;
165+
166+
$ovh = Api::withOauth2($clientId, $clientSecret, $endpoint);
167+
```
168+
169+
Supported endpoints are only `ovh-eu`, `ovh-ca` and `ovh-us`.
151170

152-
## OVHcloud API authentication
171+
## Custom OVHcloud API authentication
153172

154173
To use the OVHcloud APIs you need three credentials:
155174

@@ -169,7 +188,7 @@ They can also be created together if your application is intended to use only yo
169188

170189
### OVHcloud Europe
171190

172-
* ```$endpoint = 'ovh-eu';```
191+
* `$endpoint = 'ovh-eu';`
173192
* Documentation: <https://eu.api.ovh.com/>
174193
* Console: <https://eu.api.ovh.com/console>
175194
* Create application credentials (generate only application credentials, your app will need to implement an authorization flow): <https://eu.api.ovh.com/createApp/>
@@ -178,15 +197,15 @@ They can also be created together if your application is intended to use only yo
178197

179198
### OVHcloud US
180199

181-
* ```$endpoint = 'ovh-us';```
200+
* `$endpoint = 'ovh-us';`
182201
* Documentation: <https://api.us.ovhcloud.com/>
183202
* Console: <https://api.us.ovhcloud.com/console>
184203
* Create application credentials (generate only application credentials, your app will need to implement an authorization flow): <https://api.us.ovhcloud.com/createApp/>
185204
* Create account credentials (all keys at once for your own account only): <https://api.us.ovhcloud.com/createToken/>
186205

187206
### OVHcloud North America / Canada
188207

189-
* ```$endpoint = 'ovh-ca';```
208+
* `$endpoint = 'ovh-ca';`
190209
* Documentation: <https://ca.api.ovh.com/>
191210
* Console: <https://ca.api.ovh.com/console>
192211
* Create application credentials (generate only application credentials, your app will need to implement an authorization flow): <https://ca.api.ovh.com/createApp/>
@@ -195,7 +214,7 @@ They can also be created together if your application is intended to use only yo
195214

196215
### So you Start Europe
197216

198-
* ```$endpoint = 'soyoustart-eu';```
217+
* `$endpoint = 'soyoustart-eu';`
199218
* Documentation: <https://eu.api.soyoustart.com/>
200219
* Console: <https://eu.api.soyoustart.com/console/>
201220
* Create application credentials (generate only application credentials, your app will need to implement an authorization flow): <https://eu.api.soyoustart.com/createApp/>
@@ -204,7 +223,7 @@ They can also be created together if your application is intended to use only yo
204223

205224
### So you Start North America
206225

207-
* ```$endpoint = 'soyoustart-ca';```
226+
* `$endpoint = 'soyoustart-ca';`
208227
* Documentation: <https://ca.api.soyoustart.com/>
209228
* Console: <https://ca.api.soyoustart.com/console/>
210229
* Create application credentials (generate only application credentials, your app will need to implement an authorization flow): <https://ca.api.soyoustart.com/createApp/>
@@ -213,7 +232,7 @@ They can also be created together if your application is intended to use only yo
213232

214233
### Kimsufi Europe
215234

216-
* ```$endpoint = 'kimsufi-eu';```
235+
* `$endpoint = 'kimsufi-eu';`
217236
* Documentation: <https://eu.api.kimsufi.com/>
218237
* Console: <https://eu.api.kimsufi.com/console/>
219238
* Create application credentials (generate only application credentials, your app will need to implement an authorization flow): <https://eu.api.kimsufi.com/createApp/>
@@ -222,7 +241,7 @@ They can also be created together if your application is intended to use only yo
222241

223242
### Kimsufi North America
224243

225-
* ```$endpoint = 'kimsufi-ca';```
244+
* `$endpoint = 'kimsufi-ca';`
226245
* Documentation: <https://ca.api.kimsufi.com/>
227246
* Console: <https://ca.api.kimsufi.com/console/>
228247
* Create application credentials (generate only application credentials, your app will need to implement an authorization flow): <https://ca.api.kimsufi.com/createApp/>

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)