Skip to content
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
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ jobs:
strategy:
matrix:
os: [ windows-2022, windows-2025, ubuntu-22.04, ubuntu-24.04 ]
php-version: [8.0, 8.1, 8.2, 8.3]
php-version: [8.0, 8.1, 8.2, 8.3, 8.4]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand All @@ -31,7 +31,7 @@ jobs:
run: composer install

- name: Test
run: ./vendor/bin/phpunit tests
run: ./vendor/bin/phpunit tests --testdox -v

- uses: Bandwidth/build-notify-slack-action@v2
if: failure() && !github.event.pull_request.draft
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ selftests/
config.php
/bin
composer.lock
composer.phar
.idea/
.DS_Store
.phpunit.result.cache
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ PHP Client library for Bandwidth's Phone Number Dashboard (AKA: Dashboard, Iris)

## Supported PHP Versions

| Version | Support Level |
|:--------|:-------------------------|
| 8.0 | Supported |
| 8.1 | Supported |
| 8.2 | Supported |
| 8.3 | Supported |
| Version | Supported? |
|:--------|:-----------|
| 8.0 | Supported |
| 8.1 | Supported |
| 8.2 | Supported |
| 8.3 | Supported |
| 8.4 | Supported |

## Install

Expand All @@ -22,8 +23,15 @@ composer require bandwidth/iris
## Usage

```PHP
// Basic Auth
$client = new \Iris\Client($login, $password, ['url' => 'https://dashboard.bandwidth.com/api/']);

// Bearer Auth with Token
$client = new \Iris\Client(null, null, ['accessToken' => '<your_access_token>']);

// Bearer Auth using Client Credentials
$client = new \Iris\Client(null, null, ['clientId' => '<your_client_id>', 'clientSecret' => '<your_client_secret>']);

```

## Run tests
Expand Down
Binary file removed composer.phar
Binary file not shown.
76 changes: 63 additions & 13 deletions core/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,28 +135,48 @@ final class Client extends iClient
*/
protected $lastResponseBody = null;

protected $accessToken = null;

protected $accessTokenExpiration = null;

protected $clientId = null;

protected $clientSecret = null;

protected $clientOptions = [];

public function __construct($login, $password, $options = [])
{
if (empty($login) || empty($password))
{
throw new \Exception("Provide login, password");
foreach (['accessToken', 'accessTokenExpiration', 'clientId', 'clientSecret'] as $key) {
if (array_key_exists($key, $options)) {
$this->$key = $options[$key];
unset($options[$key]);
}
}

$options['auth'] = [$login, $password];
$options['base_uri'] = $options['url'] ?: 'https://dashboard.bandwidth.com/api';
if ($login !== null && $password !== null) {
$this->clientOptions['auth'] = [$login, $password];
}

$base = $options['base_uri'] ?? ($options['url'] ?? 'https://dashboard.bandwidth.com/api');
unset($options['url']);
$options['base_uri'] = rtrim($options['base_uri'], '/') . '/';
$this->clientOptions['base_uri'] = rtrim($base, '/') . '/';

$client_options = array();
if(isset($options['handler'])) {
$client_options['handler'] = $options['handler'];
if (isset($options['handler'])) {
$this->clientOptions['handler'] = $options['handler'];
unset($options['handler']);
} else {
$this->clientOptions['handler'] = \GuzzleHttp\HandlerStack::create();
}

$options['headers'] = array(
'User-Agent' => 'PHP-Bandwidth-Iris'
);
$headers = $options['headers'] ?? [];
$headers['User-Agent'] = 'PHP-Bandwidth-Iris';
unset($options['headers']);
$this->clientOptions['headers'] = $headers;

$this->clientOptions = array_replace($this->clientOptions, $options);

$this->client = new \GuzzleHttp\Client($options);
$this->client = new \GuzzleHttp\Client($this->clientOptions);
}

/**
Expand Down Expand Up @@ -289,6 +309,19 @@ public function request($method, $url, $options = [], $parse = true)
$this->lastResponseBody = null;
try
{
// Get token if we don't have one, or if it's expired, and we have client credentials
if ((empty($this->accessToken) || (!empty($this->accessTokenExpiration) && $this->accessTokenExpiration <= time() + 60))
&& !empty($this->clientId) && !empty($this->clientSecret)) {
$this->fetchAccessToken();
}

// If we have a valid access token, add it to the request headers
if (!empty($this->accessToken) && (empty($this->accessTokenExpiration) || $this->accessTokenExpiration > time() + 60)) {
$options['headers'] = $options['headers'] ?? [];
if (empty($options['headers']['Authorization'])) {
$options['headers']['Authorization'] = 'Bearer ' . $this->accessToken;
}
}
$response = $this->client->request($method, ltrim($url, '/'), $options);
if (!$parse)
{
Expand All @@ -302,6 +335,23 @@ public function request($method, $url, $options = [], $parse = true)
}
}

private function fetchAccessToken()
{
$tokenUrl = 'https://api.bandwidth.com/api/v1/oauth2/token';
$tokenOptions = [
'auth' => [$this->clientId, $this->clientSecret],
'form_params' => [
'grant_type' => 'client_credentials'
],
];
$oauthClient = new \GuzzleHttp\Client($this->clientOptions);

$tokenResponse = $oauthClient->request('post', $tokenUrl, $tokenOptions);
$tokenData = json_decode((string)$tokenResponse->getBody(), true);
$this->accessToken = $tokenData['access_token'] ?? null;
$this->accessTokenExpiration = isset($tokenData['expires_in']) ? (time() + (int)$tokenData['expires_in']) : null;
}

/**
* Returns the XML string received in the last response.
* @return string $lastResponseBody
Expand Down
96 changes: 96 additions & 0 deletions tests/OAuthTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Middleware;

use PHPUnit\Framework\TestCase;

class OAuthTest extends TestCase {
private const API_URL = 'https://api.test.com/v1.0';
private const INSERVICE_URL = self::API_URL . '/accounts/9500249/inserviceNumbers';
private const TOKEN_URL = 'https://api.bandwidth.com/api/v1/oauth2/token';

private const INSERVICE_RESPONSE = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><TNs><TotalCount>1</TotalCount><Links><first>link</first></Links><TelephoneNumbers><Count>1</Count><TelephoneNumber>8043024183</TelephoneNumber></TelephoneNumbers></TNs>";
private const TOKEN_RESPONSE = '{"access_token":"abcdef123456","expires_in":3600}';

private const TOKEN_REQUEST_AUTH_STRING = 'Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=';
private const BEARER_AUTH_STRING = 'Bearer abcdef123456';

private function makeAccount(array $responses, array $clientOptions, array &$container, ?string $login = null, ?string $password = null): Iris\Account {
$mock = new MockHandler($responses);
$handler = HandlerStack::create($mock);
$history = Middleware::history($container);
$handler->push($history);
$client = new Iris\Client($login, $password, array_merge(['url' => self::API_URL, 'handler' => $handler], $clientOptions));
return new Iris\Account(9500249, $client);
}

private function assertRequest(array $container, int $idx, string $method, string $uri, ?string $authHeader = null): void {
$request = $container[$idx]['request'];
$this->assertSame($uri, (string)$request->getUri());
$this->assertSame($method, $request->getMethod());
if ($authHeader !== null) {
$this->assertSame($authHeader, $request->getHeaderLine('Authorization'));
}
}

public function testBasicAuth(): void {
$container = [];
$account = $this->makeAccount([
new Response(200, [], self::INSERVICE_RESPONSE),
], [], $container, 'username', 'password');

$account->inserviceNumbers();
$this->assertRequest($container, 0, 'GET', self::INSERVICE_URL, 'Basic ' . base64_encode('username:password'));
}

public function testOAuth(): void {
$container = [];
$account = $this->makeAccount([
new Response(200, [], self::TOKEN_RESPONSE),
new Response(200, [], self::INSERVICE_RESPONSE),
new Response(200, [], self::INSERVICE_RESPONSE),
], [
'clientId' => 'client_id',
'clientSecret' => 'client_secret',
], $container);

$account->inserviceNumbers();
$account->inserviceNumbers();

$this->assertRequest($container, 0, 'POST', self::TOKEN_URL, self::TOKEN_REQUEST_AUTH_STRING);
$this->assertRequest($container, 1, 'GET', self::INSERVICE_URL, self::BEARER_AUTH_STRING);
$this->assertRequest($container, 2, 'GET', self::INSERVICE_URL, self::BEARER_AUTH_STRING);
}

public function testToken(): void {
$container = [];
$account = $this->makeAccount([
new Response(200, [], self::INSERVICE_RESPONSE),
], [
'accessToken' => 'access_token',
'accessTokenExpiration' => time() + 3600,
], $container);

$account->inserviceNumbers();
$this->assertRequest($container, 0, 'GET', self::INSERVICE_URL, 'Bearer access_token');
}

public function testExpiredToken(): void {
$container = [];
$account = $this->makeAccount([
new Response(200, [], self::TOKEN_RESPONSE),
new Response(200, [], self::INSERVICE_RESPONSE),
], [
'accessToken' => 'expired_token',
'accessTokenExpiration' => time() - 3600,
'clientId' => 'client_id',
'clientSecret' => 'client_secret',
], $container);

$account->inserviceNumbers();
$this->assertRequest($container, 0, 'POST', self::TOKEN_URL, self::TOKEN_REQUEST_AUTH_STRING);
$this->assertRequest($container, 1, 'GET', self::INSERVICE_URL, self::BEARER_AUTH_STRING);
}
}