Skip to content

Commit

Permalink
Merge pull request #152 from ovh/dev/abarreau/oauth2
Browse files Browse the repository at this point in the history
feat: handle Client Credential OAuth2 authentication method
  • Loading branch information
rbeuque74 authored Sep 27, 2024
2 parents f417038 + 57fe667 commit 3bebadd
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 47 deletions.
43 changes: 31 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OVHcloud APIs lightweight PHP wrapper

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

[![Source Code](https://img.shields.io/badge/source-ovh/php--ovh-blue.svg?style=flat-square)](https://github.com/ovh/php-ovh)
[![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)
Expand Down Expand Up @@ -38,7 +38,7 @@ echo 'Welcome '.$ovh->get('/me')['firstname'];

### Handle exceptions

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

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

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

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


```php
use \Ovh\Api;
session_start();
Expand Down Expand Up @@ -147,9 +146,29 @@ foreach ($servers as $server) {

### More code samples

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!
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!

## OAuth2 authentification

`php-ovh` supports two forms of authentication:

* OAuth2, using scopped service accounts, and compatible with OVHcloud IAM
* application key & application secret & consumer key (covered in the next chapter)

For OAuth2, first, you need to generate a pair of valid `client_id` and `client_secret`: you can proceed by
[following this documentation](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343).

Once you have retrieved your `client_id` and `client_secret`, you can instantiate an API client using:

```php
use \Ovh\Api;

$ovh = Api::withOauth2($clientId, $clientSecret, $endpoint);
```

Supported endpoints are only `ovh-eu`, `ovh-ca` and `ovh-us`.

## OVHcloud API authentication
## Custom OVHcloud API authentication

To use the OVHcloud APIs you need three credentials:

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

### OVHcloud Europe

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

### OVHcloud US

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

### OVHcloud North America / Canada

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

### So you Start Europe

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

### So you Start North America

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

### Kimsufi Europe

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

### Kimsufi North America

* ```$endpoint = 'kimsufi-ca';```
* `$endpoint = 'kimsufi-ca';`
* Documentation: <https://ca.api.kimsufi.com/>
* Console: <https://ca.api.kimsufi.com/console/>
* Create application credentials (generate only application credentials, your app will need to implement an authorization flow): <https://ca.api.kimsufi.com/createApp/>
Expand Down
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
70 changes: 56 additions & 14 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.
<?php // phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
# 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;

require_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 3bebadd

Please sign in to comment.