From f1e1c8fd187cf8f22d06184b548ee1fb97ecf2ec Mon Sep 17 00:00:00 2001 From: LukasRos Date: Wed, 17 May 2023 18:01:12 +0200 Subject: [PATCH] Implementing OAuth2 client credential flow (WIP) --- CloudObjects/SDK/WebAPI/APIClientFactory.php | 49 ++++++-- .../WebAPI/Exceptions/OAuthFlowException.php | 14 +++ CloudObjects/SDK/WebAPI/OAuth2AuthServer.php | 105 ++++++++++++++++++ 3 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 CloudObjects/SDK/WebAPI/Exceptions/OAuthFlowException.php create mode 100644 CloudObjects/SDK/WebAPI/OAuth2AuthServer.php diff --git a/CloudObjects/SDK/WebAPI/APIClientFactory.php b/CloudObjects/SDK/WebAPI/APIClientFactory.php index 859b8f1..2c6408b 100644 --- a/CloudObjects/SDK/WebAPI/APIClientFactory.php +++ b/CloudObjects/SDK/WebAPI/APIClientFactory.php @@ -154,21 +154,48 @@ class APIClientFactory { 'timeout' => self::DEFAULT_TIMEOUT ]; - if ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism', - 'wa:APIKeyAuthentication')) + if ($this->reader->hasProperty($api, 'wa:hasAuthorizationServer')) { + // 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); - - 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); diff --git a/CloudObjects/SDK/WebAPI/Exceptions/OAuthFlowException.php b/CloudObjects/SDK/WebAPI/Exceptions/OAuthFlowException.php new file mode 100644 index 0000000..0a8d354 --- /dev/null +++ b/CloudObjects/SDK/WebAPI/Exceptions/OAuthFlowException.php @@ -0,0 +1,14 @@ +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']; + } +} \ No newline at end of file