OAuth2 client credentials flow with cache
This commit is contained in:
17
CloudObjects/SDK/CustomCacheAndLogInterface.php
Normal file
17
CloudObjects/SDK/CustomCacheAndLogInterface.php
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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,27 @@ 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 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->options['cache_prefix'].$id))
|
||||||
? $this->cache->fetch($this->options['cache_prefix'].$id) : null;
|
? $this->cache->fetch($this->options['cache_prefix'].$this->options['auth_ns'].'/'.$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->options['cache_prefix'].$this->options['auth_ns'].'/'.$id, $data, $ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFromCacheCustom($id) {
|
||||||
|
return $this->getFromCache('custom/'.$id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putIntoCacheCustom($id, $data, $ttl) {
|
||||||
|
$this->putIntoCache('custom/'.$id, $data, $ttl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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');
|
$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());
|
||||||
|
}
|
||||||
|
|
||||||
Assert::true($this->reader->hasProperty($consumer, $clientIDProperty),
|
|
||||||
"Namespace must have Client ID");
|
|
||||||
Assert::true($this->reader->hasProperty($consumer, $clientSecretProperty),
|
|
||||||
"Namespace must have Client Secret");
|
|
||||||
|
|
||||||
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));
|
||||||
|
|
||||||
|
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.", $ts);
|
||||||
|
$this->cacheAndLog->putIntoCacheCustom($grantCacheKey, json_encode($tokenResponse), $expiry);
|
||||||
|
}
|
||||||
|
|
||||||
return $tokenResponse['access_token'];
|
return $tokenResponse['access_token'];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user