Compare commits

9 Commits

10 changed files with 931 additions and 867 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,7 @@
vendor
build
cache
.cache
.config
.local
*.phar

View File

@@ -9,7 +9,8 @@ namespace CloudObjects\SDK\AccountGateway;
use ML\IRI\IRI;
use ML\JsonLD\Document, ML\JsonLD\JsonLD, ML\JsonLD\Node;
use Symfony\Component\HttpFoundation\Request, Symfony\Component\HttpFoundation\Response;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use Nyholm\Psr7\Factory\Psr17Factory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use GuzzleHttp\Client, GuzzleHttp\HandlerStack, GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface, Psr\Http\Message\ResponseInterface;
@@ -110,8 +111,11 @@ class AccountContext {
new IRI('aauid:'.$request->headers->get('C-AAUID')),
$request->headers->get('C-Access-Token'));
$psr7Factory = new DiactorosFactory;
$context->parsePsrRequest($psr7Factory->createRequest($request));
// Convert HTTP Foundation to PSR17
// based on: https://symfony.com/doc/current/components/psr7.html#converting-from-httpfoundation-objects-to-psr-7
$psr17Factory = new Psr17Factory;
$psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
$context->parsePsrRequest($psrHttpFactory->createRequest($request));
return $context;
}

View File

@@ -0,0 +1,17 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
namespace CloudObjects\SDK;
interface CustomCacheAndLogInterface {
public function logInfoWithTime($message, $ts);
public function getFromCacheCustom($id);
public function putIntoCacheCustom($id, $data, $ttl);
}

View File

@@ -20,7 +20,7 @@ use CloudObjects\SDK\AccountGateway\AccountContext;
/**
* The ObjectRetriever provides access to objects on CloudObjects.
*/
class ObjectRetriever {
class ObjectRetriever implements CustomCacheAndLogInterface {
use LoggerAwareTrait;
@@ -107,19 +107,31 @@ class ObjectRetriever {
$this->client = new Client($options);
}
private function logInfoWithTime($message, $ts) {
public function logInfoWithTime($message, $ts) {
if (isset($this->logger))
$this->logger->info($message, [ 'elapsed_ms' => round((microtime(true) - $ts) * 1000) ]);
}
private function getCacheKey($id) {
return $this->options['cache_prefix'].$this->options['auth_ns'].'/'.$id;
}
private function getFromCache($id) {
return (isset($this->cache) && $this->cache->contains($this->options['cache_prefix'].$id))
? $this->cache->fetch($this->options['cache_prefix'].$id) : null;
return (isset($this->cache) && $this->cache->contains($this->getCacheKey($id)))
? $this->cache->fetch($this->getCacheKey($id)) : null;
}
private function putIntoCache($id, $data, $ttl) {
if (isset($this->cache))
$this->cache->save($this->options['cache_prefix'].$id, $data, $ttl);
$this->cache->save($this->getCacheKey($id), $data, $ttl);
}
public function getFromCacheCustom($id) {
return $this->getFromCache('custom/'.$id);
}
public function putIntoCacheCustom($id, $data, $ttl) {
$this->putIntoCache('custom/'.$id, $data, $ttl);
}
/**

View File

@@ -154,21 +154,51 @@ class APIClientFactory {
'timeout' => self::DEFAULT_TIMEOUT
];
if ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
'wa:APIKeyAuthentication'))
if ($this->reader->hasProperty($api, 'oauth2:hasAuthorizationServer')) {
// We have an authorization server for this endpoint/API
$authServerCoid = $this->reader->getFirstValueIRI($api, 'oauth2:hasAuthorizationServer');
$authServerObject = $this->objectRetriever->getObject($authServerCoid);
if (!isset($authServerObject))
throw new InvalidObjectConfigurationException("Authorization server object <"
. (string)$authServerCoid . "> not available.");
try {
$authServer = new OAuth2AuthServer($authServerObject, $this->objectRetriever);
} catch (InvalidObjectConfigurationException $e) {
throw new InvalidObjectConfigurationException("Authorization server object <"
. (string)$authServerCoid . "> could not be loaded; error: " . $e->getMessage());
} catch (Exception $e) {
throw new InvalidObjectConfigurationException("Authorization server object <"
. (string)$authServerCoid . "> could not be loaded. Its definition may be invalid.");
}
try {
$authServer->configureConsumer($this->namespace);
} catch (Exception $e) {
throw new InvalidObjectConfigurationException("The namespace <" . $this->namespace->getId()
. "> does not contain valid configuration to use the authorization server <"
. (string)$authServerCoid . ">.");
}
// Get access token through the auth server
$clientConfig['headers']['Authorization'] = 'Bearer ' . $authServer->getAccessToken();
} elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
'wa:APIKeyAuthentication')) {
// API key authentication
$clientConfig = $this->configureAPIKeyAuthentication($api, $clientConfig);
elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
'oauth2:FixedBearerTokenAuthentication'))
} elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
'oauth2:FixedBearerTokenAuthentication')) {
// Fixed bearer token authentication
$clientConfig = $this->configureBearerTokenAuthentication($api, $clientConfig);
elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
'wa:HTTPBasicAuthentication'))
} elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
'wa:HTTPBasicAuthentication')) {
// HTTP Basic authentication
$clientConfig = $this->configureBasicAuthentication($api, $clientConfig);
elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
'wa:SharedSecretAuthenticationViaHTTPBasic'))
} elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
'wa:SharedSecretAuthenticationViaHTTPBasic')) {
// HTTP Basic authentication using shared secrets in CloudObjects Core
$clientConfig = $this->configureSharedSecretBasicAuthentication($api, $clientConfig);
}
if ($specificClient == false)
return new Client($clientConfig);

View File

@@ -0,0 +1,14 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
namespace CloudObjects\SDK\WebAPI\Exceptions;
/**
* An Exception that is thrown when an an OAuth flow failed.
*/
class OAuthFlowException extends \Exception {
}

View File

@@ -0,0 +1,143 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
namespace CloudObjects\SDK\WebAPI;
use Exception;
use ML\JsonLD\Node;
use GuzzleHttp\Client;
use Webmozart\Assert\Assert,
Webmozart\Assert\InvalidArgumentException;
use CloudObjects\SDK\NodeReader,
CloudObjects\SDK\CustomCacheAndLogInterface;
use CloudObjects\SDK\Exceptions\InvalidObjectConfigurationException;
class OAuth2AuthServer {
private $reader;
private $authServer;
private $consumer;
private $cacheAndLog;
private $grantType;
private $clientId;
private $clientSecret;
public function __construct(Node $authServer, CustomCacheAndLogInterface $cacheAndLog) {
$this->reader = new NodeReader([
'prefixes' => [
'oauth2' => 'coid://oauth2.co-n.net/'
]
]);
try {
Assert::true($this->reader->hasProperty($authServer, 'oauth2:hasTokenEndpoint'),
"Authorization Server must have a token endpoint.");
Assert::startsWith($this->reader->getFirstValueString($authServer, 'oauth2:hasTokenEndpoint'),
"https://",
"Token endpoint must be an https:// URL.");
Assert::true($this->reader->hasProperty($authServer, 'oauth2:supportsGrantType'),
"Authorization Server must support at least one grant type.");
Assert::true($this->reader->hasProperty($authServer, 'oauth2:usesClientIDFrom'),
"Authorization Server must define client ID property.");
Assert::true($this->reader->hasProperty($authServer, 'oauth2:usesClientSecretFrom'),
"Authorization Server must define client secret property.");
} catch (InvalidArgumentException $e) {
throw new InvalidObjectConfigurationException($e->getMessage());
}
$this->authServer = $authServer;
$this->cacheAndLog = $cacheAndLog;
}
public function configureConsumer(Node $consumer) : void {
try {
Assert::notNull($this->authServer, "Object wasn't initialized correctly.");
Assert::notNull($this->cacheAndLog, "Object wasn't initialized correctly.");
$clientIDProperty = $this->reader->getFirstValueString($this->authServer,
'oauth2:usesClientIDFrom');
$clientSecretProperty = $this->reader->getFirstValueString($this->authServer,
'oauth2:usesClientSecretFrom');
Assert::true($this->reader->hasProperty($consumer, $clientIDProperty),
"Namespace must have Client ID");
Assert::true($this->reader->hasProperty($consumer, $clientSecretProperty),
"Namespace must have Client Secret");
} catch (InvalidArgumentException $e) {
throw new InvalidObjectConfigurationException($e->getMessage());
}
if ($this->reader->hasPropertyValue($this->authServer,
'oauth2:supportsGrantType', 'oauth2:ClientCredentials'))
{
// No additional conditions for "client_credentials" flow
$this->grantType = 'client_credentials';
} else {
throw new InvalidObjectConfigurationException("No flow/grant_type found.");
}
$this->consumer = $consumer;
$this->clientId = $this->reader->getFirstValueString($consumer, $clientIDProperty);
$this->clientSecret = $this->reader->getFirstValueString($consumer, $clientSecretProperty);
}
public function getAccessToken() {
try {
Assert::notNull($this->authServer, "Object wasn't initialized correctly.");
Assert::notNull($this->cacheAndLog, "Object wasn't initialized correctly.");
Assert::notNull($this->consumer, "Missing consumer.");
Assert::notNull($this->grantType, "Missing grant_type.");
Assert::notNull($this->clientId, "Missing client_id.");
Assert::notNull($this->clientSecret, "Missing client_secret.");
} catch (InvalidArgumentException $e) {
throw new InvalidObjectConfigurationException($e->getMessage());
}
$client = new Client;
$tokenEndpointUrl = $this->reader->getFirstValueString($this->authServer, 'oauth2:hasTokenEndpoint');
$params = [
'grant_type' => $this->grantType,
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret
];
switch ($this->grantType) {
case "client_credentials":
// no additional params needed
break;
default:
throw new Exception("No flow/grant_type found.");
}
$grantCacheKey = sha1(json_encode($params));
$ts = microtime(true);
$tokenResponse = json_decode($this->cacheAndLog->getFromCacheCustom($grantCacheKey), true);
if (isset($tokenResponse)) {
$this->cacheAndLog->logInfoWithTime("Reused access token for <".$this->authServer->getId()."> from cache.", $ts);
} else {
// Nothing cached, fetch from server
$tokenResponse = json_decode($client->post($tokenEndpointUrl, [
'form_params' => $params
])->getBody(true), true);
Assert::keyExists($tokenResponse, 'access_token');
$expiry = isset($tokenResponse['expires_in']) ? $tokenResponse['expires_in'] : 84600;
$this->cacheAndLog->logInfoWithTime("Retrieved access token for <".$this->authServer->getId()."> from token endpoint and will cache for ".$expiry." seconds.", $ts);
$this->cacheAndLog->putIntoCacheCustom($grantCacheKey, json_encode($tokenResponse), $expiry);
}
return $tokenResponse['access_token'];
}
}

View File

@@ -2,6 +2,8 @@
[![Latest Stable Version](https://poser.pugx.org/cloudobjects/sdk/v/stable)](https://packagist.org/packages/cloudobjects/sdk) [![Total Downloads](https://poser.pugx.org/cloudobjects/sdk/downloads)](https://packagist.org/packages/cloudobjects/sdk)
[![buddy branch](https://app.buddy.works/cloudobjects/php-sdk/repository/branch/main/badge.svg?token=52ae28bf71dbbd3dde018f3f3e7caafa04f95bdb451b3dfca547414ec7a01739 "buddy branch")](https://app.buddy.works/cloudobjects/php-sdk/repository/branch/main)
The CloudObjects PHP SDK provides simple access to [CloudObjects](https://cloudobjects.io/) from PHP-based applications. It wraps the [Object API](https://coid.link/cloudobjects.io/ObjectAPI/1.0) to fetch objects from the CloudObjects Core database and provides object-based access to their RDF description. A two-tiered caching mechanism (in-memory and Doctrine cache drivers) is included. The SDK also contains a helper class to validate COIDs.
## Installation

View File

@@ -9,7 +9,7 @@
"doctrine/common" : ">=2.6.1",
"doctrine/cache" : "1.*",
"guzzlehttp/guzzle" : ">=6.0",
"psr/log": "^1.1",
"psr/log": ">=1.1",
"kevinrob/guzzle-cache-middleware": "^3.2",
"webmozart/assert": "^1.6"
},
@@ -27,13 +27,13 @@
"phpunit/phpunit": ">=4.8.0,<5.0",
"symfony/http-foundation" : ">=4.0",
"symfony/psr-http-message-bridge" : ">=1.1.0",
"zendframework/zend-diactoros" : "~1.8.6",
"nyholm/psr7" : "~1.5.1",
"defuse/php-encryption" : "^2.2"
},
"suggest" : {
"symfony/http-foundation" : "Required to use parseSymfonyRequest() in AccountContext.",
"symfony/psr-http-message-bridge" : "Required to use parseSymfonyRequest() in AccountContext.",
"zendframework/zend-diactoros" : "Required to use parseSymfonyRequest() in AccountContext.",
"nyholm/psr7" : "Required to use parseSymfonyRequest() in AccountContext.",
"defuse/php-encryption": "Required to use CryptoHelper"
}
}

1529
composer.lock generated

File diff suppressed because it is too large Load Diff