4 Commits
0.8 ... 0.9

Author SHA1 Message Date
b127c3cba8 Ran composer update to update lockfile 2025-05-27 16:46:34 +02:00
3039ddc2ec Added expiry logging and fixed token caching 2024-11-11 00:04:20 +01:00
6d9ea6584d Fixed cache key issue 2024-11-11 00:02:02 +01:00
fde083f36f OAuth2 client credentials flow with cache 2024-11-10 23:32:27 +01:00
5 changed files with 394 additions and 506 deletions

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. * The ObjectRetriever provides access to objects on CloudObjects.
*/ */
class ObjectRetriever { class ObjectRetriever implements CustomCacheAndLogInterface {
use LoggerAwareTrait; use LoggerAwareTrait;
@@ -107,19 +107,31 @@ class ObjectRetriever {
$this->client = new Client($options); $this->client = new Client($options);
} }
private function logInfoWithTime($message, $ts) { public function logInfoWithTime($message, $ts) {
if (isset($this->logger)) if (isset($this->logger))
$this->logger->info($message, [ 'elapsed_ms' => round((microtime(true) - $ts) * 1000) ]); $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) { private function getFromCache($id) {
return (isset($this->cache) && $this->cache->contains($this->options['cache_prefix'].$id)) return (isset($this->cache) && $this->cache->contains($this->getCacheKey($id)))
? $this->cache->fetch($this->options['cache_prefix'].$id) : null; ? $this->cache->fetch($this->getCacheKey($id)) : null;
} }
private function putIntoCache($id, $data, $ttl) { private function putIntoCache($id, $data, $ttl) {
if (isset($this->cache)) 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,16 +154,19 @@ class APIClientFactory {
'timeout' => self::DEFAULT_TIMEOUT 'timeout' => self::DEFAULT_TIMEOUT
]; ];
if ($this->reader->hasProperty($api, 'wa:hasAuthorizationServer')) { if ($this->reader->hasProperty($api, 'oauth2:hasAuthorizationServer')) {
// We have an authorization server for this endpoint/API // We have an authorization server for this endpoint/API
$authServerCoid = $this->reader->getFirstValueIRI($api, 'wa:hasAuthorizationServer'); $authServerCoid = $this->reader->getFirstValueIRI($api, 'oauth2:hasAuthorizationServer');
$authServerObject = $this->objectRetriever->getObject($authServerCoid); $authServerObject = $this->objectRetriever->getObject($authServerCoid);
if (!isset($authServer)) if (!isset($authServerObject))
throw new InvalidObjectConfigurationException("Authorization server object <" throw new InvalidObjectConfigurationException("Authorization server object <"
. (string)$authServerCoid . "> not available."); . (string)$authServerCoid . "> not available.");
try { try {
$authServer = new OAuth2AuthServer($authServerObject); $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) { } catch (Exception $e) {
throw new InvalidObjectConfigurationException("Authorization server object <" throw new InvalidObjectConfigurationException("Authorization server object <"
. (string)$authServerCoid . "> could not be loaded. Its definition may be invalid."); . (string)$authServerCoid . "> could not be loaded. Its definition may be invalid.");

View File

@@ -9,63 +9,77 @@ namespace CloudObjects\SDK\WebAPI;
use Exception; use Exception;
use ML\JsonLD\Node; use ML\JsonLD\Node;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert,
use CloudObjects\SDK\NodeReader; Webmozart\Assert\InvalidArgumentException;
use CloudObjects\SDK\NodeReader,
CloudObjects\SDK\CustomCacheAndLogInterface;
use CloudObjects\SDK\Exceptions\InvalidObjectConfigurationException;
class OAuth2AuthServer { class OAuth2AuthServer {
private $reader; private $reader;
private $authServer; private $authServer;
private $consumer; private $consumer;
private $cacheAndLog;
private $grantType; private $grantType;
private $clientId; private $clientId;
private $clientSecret; private $clientSecret;
public function __construct(Node $authServer) { public function __construct(Node $authServer, CustomCacheAndLogInterface $cacheAndLog) {
$this->reader = new NodeReader([ $this->reader = new NodeReader([
'prefixes' => [ 'prefixes' => [
'oauth2' => 'coid://oauth2.co-n.net/' 'oauth2' => 'coid://oauth2.co-n.net/'
] ]
]); ]);
Assert::true($this->reader->hasProperty($authServer, 'oauth2:hasTokenEndpoint'), try {
"Authorization Server must have a token endpoint."); Assert::true($this->reader->hasProperty($authServer, 'oauth2:hasTokenEndpoint'),
Assert::startsWith($this->reader->getFirstValueString($authServer, 'oauth2:hasTokenEndpoint'), "Authorization Server must have a token endpoint.");
"https://", Assert::startsWith($this->reader->getFirstValueString($authServer, 'oauth2:hasTokenEndpoint'),
"Token endpoint must be an https:// URL."); "https://",
Assert::true($this->reader->hasProperty($authServer, 'oauth2:supportsGrantType'), "Token endpoint must be an https:// URL.");
"Authorization Server must support at least one grant type."); Assert::true($this->reader->hasProperty($authServer, 'oauth2:supportsGrantType'),
Assert::true($this->reader->hasProperty($this->authServer, 'oauth2:usesClientIDFrom'), "Authorization Server must support at least one grant type.");
"Authorization Server must define client ID property."); Assert::true($this->reader->hasProperty($authServer, 'oauth2:usesClientIDFrom'),
Assert::true($this->reader->hasProperty($this->authServer, 'oauth2:usesClientSecretFrom'), "Authorization Server must define client ID property.");
"Authorization Server must define client secret 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->authServer = $authServer;
} $this->cacheAndLog = $cacheAndLog;
private function assertClientCredentialPropertiesExist() : void {
} }
public function configureConsumer(Node $consumer) : void { public function configureConsumer(Node $consumer) : void {
$this->assertClientCredentialPropertiesExist(); try {
$clientIDProperty = $this->reader->getFirstValueString($this->authServer, Assert::notNull($this->authServer, "Object wasn't initialized correctly.");
'oauth2:usesClientIDFrom'); Assert::notNull($this->cacheAndLog, "Object wasn't initialized correctly.");
$clientSecretProperty = $this->reader->getFirstValueString($this->authServer,
'oauth2:usesClientSecretFrom');
Assert::true($this->reader->hasProperty($consumer, $clientIDProperty), $clientIDProperty = $this->reader->getFirstValueString($this->authServer,
"Namespace must have Client ID"); 'oauth2:usesClientIDFrom');
Assert::true($this->reader->hasProperty($consumer, $clientSecretProperty), $clientSecretProperty = $this->reader->getFirstValueString($this->authServer,
"Namespace must have Client Secret"); '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, if ($this->reader->hasPropertyValue($this->authServer,
'oauth2:supportsGrantType', 'oauth2:ClientCredentials')) { 'oauth2:supportsGrantType', 'oauth2:ClientCredentials'))
{
// No additional conditions for "client_credentials" flow // No additional conditions for "client_credentials" flow
$this->grantType = 'client_credentials'; $this->grantType = 'client_credentials';
} else { } else {
throw new Exception("No flow/grant_type found."); throw new InvalidObjectConfigurationException("No flow/grant_type found.");
} }
$this->consumer = $consumer; $this->consumer = $consumer;
@@ -74,10 +88,18 @@ class OAuth2AuthServer {
} }
public function getAccessToken() { public function getAccessToken() {
Assert::notNull($this->consumer, "Missing consumer."); try {
Assert::notNull($this->grantType, "Missing grant_type."); Assert::notNull($this->authServer, "Object wasn't initialized correctly.");
Assert::notNull($this->clientId, "Missing client_id."); Assert::notNull($this->cacheAndLog, "Object wasn't initialized correctly.");
Assert::notNull($this->clientSecret, "Missing client_secret.");
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; $client = new Client;
$tokenEndpointUrl = $this->reader->getFirstValueString($this->authServer, 'oauth2:hasTokenEndpoint'); $tokenEndpointUrl = $this->reader->getFirstValueString($this->authServer, 'oauth2:hasTokenEndpoint');
@@ -90,15 +112,31 @@ class OAuth2AuthServer {
switch ($this->grantType) { switch ($this->grantType) {
case "client_credentials": case "client_credentials":
// no additional params needed // no additional params needed
break;
default: default:
throw new Exception("No flow/grant_type found."); throw new Exception("No flow/grant_type found.");
} }
$tokenResponse = json_decode($client->post($tokenEndpointUrl, [ $grantCacheKey = sha1(json_encode($params));
'form_params' => $params
])->getBody(true));
Assert::keyExists($tokenResponse, 'access_token'); $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']; return $tokenResponse['access_token'];
} }

736
composer.lock generated

File diff suppressed because it is too large Load Diff