Implementing OAuth2 client credential flow (WIP)

This commit is contained in:
2023-05-17 18:01:12 +02:00
parent 8b1a5ca4a2
commit f1e1c8fd18
3 changed files with 157 additions and 11 deletions

View File

@@ -154,21 +154,48 @@ class APIClientFactory {
'timeout' => self::DEFAULT_TIMEOUT 'timeout' => self::DEFAULT_TIMEOUT
]; ];
if ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism', if ($this->reader->hasProperty($api, 'wa:hasAuthorizationServer')) {
'wa:APIKeyAuthentication')) // We have an authorization server for this endpoint/API
$authServerCoid = $this->reader->getFirstValueIRI($api, 'wa:hasAuthorizationServer');
$authServerObject = $this->objectRetriever->getObject($authServerCoid);
if (!isset($authServer))
throw new InvalidObjectConfigurationException("Authorization server object <"
. (string)$authServerCoid . "> not available.");
try {
$authServer = new OAuth2AuthServer($authServerObject);
} 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); $clientConfig = $this->configureAPIKeyAuthentication($api, $clientConfig);
} elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism', 'oauth2:FixedBearerTokenAuthentication')) {
'oauth2:FixedBearerTokenAuthentication')) // Fixed bearer token authentication
$clientConfig = $this->configureBearerTokenAuthentication($api, $clientConfig); $clientConfig = $this->configureBearerTokenAuthentication($api, $clientConfig);
} elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism', 'wa:HTTPBasicAuthentication')) {
'wa:HTTPBasicAuthentication')) // HTTP Basic authentication
$clientConfig = $this->configureBasicAuthentication($api, $clientConfig); $clientConfig = $this->configureBasicAuthentication($api, $clientConfig);
} elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism', 'wa:SharedSecretAuthenticationViaHTTPBasic')) {
'wa:SharedSecretAuthenticationViaHTTPBasic')) // HTTP Basic authentication using shared secrets in CloudObjects Core
$clientConfig = $this->configureSharedSecretBasicAuthentication($api, $clientConfig); $clientConfig = $this->configureSharedSecretBasicAuthentication($api, $clientConfig);
}
if ($specificClient == false) if ($specificClient == false)
return new Client($clientConfig); 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\OAuthFlowException;
/**
* An Exception that is thrown when an an OAuth flow failed.
*/
class OAuthFlowException extends \Exception {
}

View File

@@ -0,0 +1,105 @@
<?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;
use CloudObjects\SDK\NodeReader;
class OAuth2AuthServer {
private $reader;
private $authServer;
private $consumer;
private $grantType;
private $clientId;
private $clientSecret;
public function __construct(Node $authServer) {
$this->reader = new NodeReader([
'prefixes' => [
'oauth2' => 'coid://oauth2.co-n.net/'
]
]);
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($this->authServer, 'oauth2:usesClientIDFrom'),
"Authorization Server must define client ID property.");
Assert::true($this->reader->hasProperty($this->authServer, 'oauth2:usesClientSecretFrom'),
"Authorization Server must define client secret property.");
$this->authServer = $authServer;
}
private function assertClientCredentialPropertiesExist() : void {
}
public function configureConsumer(Node $consumer) : void {
$this->assertClientCredentialPropertiesExist();
$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");
if ($this->reader->hasPropertyValue($this->authServer,
'oauth2:supportsGrantType', 'oauth2:ClientCredentials')) {
// No additional conditions for "client_credentials" flow
$this->grantType = 'client_credentials';
} else {
throw new Exception("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() {
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.");
$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
default:
throw new Exception("No flow/grant_type found.");
}
$tokenResponse = json_decode($client->post($tokenEndpointUrl, [
'form_params' => $params
])->getBody(true));
Assert::keyExists($tokenResponse, 'access_token');
return $tokenResponse['access_token'];
}
}