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

feat: handle Client Credential OAuth2 authentication method #152

Merged
merged 1 commit into from
Sep 27, 2024
Merged
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
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
Loading