From 2f7f590b26f37a7d2f6487d9ee01a5945b9bc29e Mon Sep 17 00:00:00 2001 From: mpastas Date: Mon, 16 Jul 2018 16:52:28 -0500 Subject: [PATCH] Initial support to federated STS authentication --- src/Runtime/Auth/SamlTokenProvider.php | 215 ++++++++++++++++++++++- src/Runtime/Auth/xml/RST2.xml | 1 + src/Runtime/Auth/xml/federatedSAML.xml | 25 +++ src/Runtime/Utilities/RequestOptions.php | 6 +- src/Runtime/Utilities/Requests.php | 8 + 5 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 src/Runtime/Auth/xml/RST2.xml create mode 100644 src/Runtime/Auth/xml/federatedSAML.xml diff --git a/src/Runtime/Auth/SamlTokenProvider.php b/src/Runtime/Auth/SamlTokenProvider.php index 183db0f7..c50baf2b 100644 --- a/src/Runtime/Auth/SamlTokenProvider.php +++ b/src/Runtime/Auth/SamlTokenProvider.php @@ -16,6 +16,19 @@ class SamlTokenProvider extends BaseTokenProvider */ private static $StsUrl = 'https://login.microsoftonline.com/extSTS.srf'; + /** + * RST2 URL + * @var string + */ + private static $RST2Url = 'https://login.microsoftonline.com/rst2.srf'; + + /** + * To Get the STS authentication url if $StsUrl request fails. + * @var string + */ + private static $RealmUrlTemplate = 'https://login.microsoftonline.com/getuserrealm.srf?login={username}&xml=1'; + + /** * Form Url to submit SAML token * @var string @@ -24,6 +37,20 @@ class SamlTokenProvider extends BaseTokenProvider /** + * Boolean to determine whether the system is using Federated STS or not. + * @var + */ + protected $usingFederatedSTS; + + /** + * Form Url to submit SAML token if Federated STS is set. + * @var string + */ + private static $IDCRLSVCPageUrl = '/_vti_bin/idcrl.svc/'; + + + + /** * @var string */ protected $authorityUrl; @@ -41,15 +68,26 @@ class SamlTokenProvider extends BaseTokenProvider */ private $rtFa; + /** + * Federated STS Auth Cookie + * @var + */ + private $SPOIDCRL; + public function __construct($authorityUrl) { $this->authorityUrl = $authorityUrl; + $this->usingFederatedSTS = FALSE; } public function getAuthenticationCookie() { + if ($this->usingFederatedSTS) { + return 'SPOIDCRL=' . $this->SPOIDCRL; + } + return 'FedAuth=' . $this->FedAuth . '; rtFa=' . $this->rtFa; } @@ -73,11 +111,27 @@ public function acquireToken($parameters) protected function acquireAuthenticationCookies($token) { $urlInfo = parse_url($this->authorityUrl); + $url = $urlInfo['scheme'] . '://' . $urlInfo['host'] . self::$SignInPageUrl; - $response = Requests::post($url,null,$token,true); - $cookies = Requests::parseCookies($response); - $this->FedAuth = $cookies['FedAuth']; - $this->rtFa = $cookies['rtFa']; + if ($this->usingFederatedSTS) { + $url = $urlInfo['scheme'] . '://' . $urlInfo['host'] . self::$IDCRLSVCPageUrl; + + $headers = array(); + $headers['User-Agent'] = ''; + $headers['X-IDCRL_ACCEPTED'] = 't'; + $headers['Authorization'] = 'BPOSIDCRL ' . $token; + $headers['Content-Type'] = 'application/x-www-form-urlencoded'; + + $response = Requests::getHead($url,$headers,$token,true); + $cookies = Requests::parseCookies($response); + $this->SPOIDCRL = $cookies['SPOIDCRL']; + } + else { + $response = Requests::post($url,null,$token,true); + $cookies = Requests::parseCookies($response); + $this->FedAuth = $cookies['FedAuth']; + $this->rtFa = $cookies['rtFa']; + } } @@ -93,14 +147,93 @@ protected function acquireSecurityToken($username, $password) { $data = $this->prepareSecurityTokenRequest($username, $password, $this->authorityUrl); $response = Requests::post(self::$StsUrl,null,$data); + + try { + $this->processSecurityTokenResponse($response); + } + catch (Exception $e) { + // Try to get the token with a federated authentication. + $response = $this->acquireSecurityTokenFromFederatedSTS($username, $password); + + } return $this->processSecurityTokenResponse($response); } + /** + * Acquire the service token from Federated STS + * + * @param string $username + * @param string $password + * @return string + */ + protected function acquireSecurityTokenFromFederatedSTS($username, $password) { + + $response = Requests::get(str_replace('{username}', $username, self::$RealmUrlTemplate),null); + $federatedStsUrl = $this->getFederatedAuthenticationInformation($response); + + if ($federatedStsUrl) { + $message_id = md5(uniqid($username . '-' . time() . '-' . rand() , true)); + $data = $this->prepareSecurityFederatedTokenRequest($username, $password, $message_id, $federatedStsUrl->textContent); + + $headers = array(); + $headers['Content-Type'] = 'application/soap+xml'; + $response = Requests::post($federatedStsUrl->textContent, $headers, $data); + + $samlAssertion = $this->getSamlAssertion($response); + + if ($samlAssertion) { + $samlAssertion_node = $samlAssertion->item(0); + $data = $this->prepareRST2Request($samlAssertion_node); + $response = Requests::post(self::$RST2Url, $headers, $data); + $this->usingFederatedSTS = TRUE; + + return $response; + } + } + + return NULL; + } + + /** + * Get SAML assertion Node so it can be used within the RST2 template + * @param $response + * @return \DOMNodeList|null + */ + protected function getSamlAssertion($response) { + $xml = new \DOMDocument(); + $xml->loadXML($response); + $xpath = new \DOMXPath($xml); + + if ($xpath->query("//*[name()='saml:Assertion']")->length > 0) { + $nodeToken = $xpath->query("//*[name()='saml:Assertion']"); + if (!empty($nodeToken)) { + return $nodeToken; + } + } + return NULL; + } + + /** + * Retrieves the STS federated URL if any. + * @param $response + * @return string Federated STS Url + */ + protected function getFederatedAuthenticationInformation($response) { + if ($response) { + $xml = new \DOMDocument(); + $xml->loadXML($response); + $xpath = new \DOMXPath($xml); + if ($xpath->query("//STSAuthURL")->length > 0) { + return $xpath->query("//STSAuthURL")->item(0); + } + } + return ''; + } /** * Verify and extract security token from the HTTP response * @param mixed $response - * @return mixed + * @return mixed BinarySecurityToken or Exception when an error is present * @throws Exception */ protected function processSecurityTokenResponse($response) @@ -108,16 +241,25 @@ protected function processSecurityTokenResponse($response) $xml = new \DOMDocument(); $xml->loadXML($response); $xpath = new \DOMXPath($xml); + if ($xpath->query("//wsse:BinarySecurityToken")->length > 0) { + $nodeToken = $xpath->query("//wsse:BinarySecurityToken")->item(0); + if (!empty($nodeToken)) { + return $nodeToken->nodeValue; + } + } + if ($xpath->query("//S:Fault")->length > 0) { $nodeErr = $xpath->query("//S:Fault/S:Detail/psf:error/psf:internalerror/psf:text")->item(0); throw new \Exception($nodeErr->nodeValue); } - $nodeToken = $xpath->query("//wsse:BinarySecurityToken")->item(0); - if (empty($nodeToken)) { - throw new \RuntimeException('Error trying to get a token, check your URL or credentials'); + + if ($xpath->query("//S:Fault")->length > 0) { + $nodeErr = $xpath->query("//S:Fault/S:Detail/psf:error/psf:internalerror/psf:text")->item(0); + throw new \Exception($nodeErr->nodeValue); } - return $nodeToken->nodeValue; + throw new \RuntimeException('Error trying to get a token, check your URL or credentials'); + } /** @@ -142,4 +284,59 @@ protected function prepareSecurityTokenRequest($username, $password, $address) $template = str_replace('{address}', $address, $template); return $template; } + + /** + * Construct the request body to acquire security token from Federated STS endpoint (sts.yourcompany.com) + * + * @param $username + * @param $password + * @param $message_uuid + * @param $federated_sts_url + * @return string + * @throws Exception + */ + protected function prepareSecurityFederatedTokenRequest($username, $password, $message_uuid, $federated_sts_url) + { + $fileName = __DIR__ . '/xml/federatedSAML.xml'; + if (!file_exists($fileName)) { + throw new \Exception("The file $fileName does not exist"); + } + + $template = file_get_contents($fileName); + $template = str_replace('{username}', $username, $template); + $template = str_replace('{password}', $password, $template); + $template = str_replace('{federated_sts_url}', $federated_sts_url, $template); + $template = str_replace('{message_uuid}', $message_uuid, $template); + return $template; + } + + /** + * Prepare the request to be sent to RST2 endpoint with the saml assertion + * @param $samlAssertion + * @return bool|mixed|string + * @throws \Exception + */ + protected function prepareRST2Request($samlAssertion) + { + + $fileName = __DIR__ . '/xml/RST2.xml'; + if (!file_exists($fileName)) { + throw new \Exception("The file $fileName does not exist"); + } + $template = file_get_contents($fileName); + + $xml = new \DOMDocument(); + $xml->loadXML($template); + $xpath = new \DOMXPath($xml); + + $samlAssertion = $xml->importNode($samlAssertion, true); + if ($xpath->query("//*[name()='wsse:Security']")->length > 0) { + $parentNode = $xpath->query("//wsse:Security")->item(0); + //append "saml assertion" node to node + $parentNode->appendChild($samlAssertion); + return $xml->saveXML(); + } + + return NULL; + } } \ No newline at end of file diff --git a/src/Runtime/Auth/xml/RST2.xml b/src/Runtime/Auth/xml/RST2.xml new file mode 100644 index 00000000..54b59edb --- /dev/null +++ b/src/Runtime/Auth/xml/RST2.xml @@ -0,0 +1 @@ +http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issuehttps://login.microsoftonline.com/rst2.srf5Managed IDCRLhttp://schemas.xmlsoap.org/ws/2005/02/trust/Issuesharepoint.com \ No newline at end of file diff --git a/src/Runtime/Auth/xml/federatedSAML.xml b/src/Runtime/Auth/xml/federatedSAML.xml new file mode 100644 index 00000000..9b76406c --- /dev/null +++ b/src/Runtime/Auth/xml/federatedSAML.xml @@ -0,0 +1,25 @@ + + + + http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue + {federated_sts_url} + {message_uuid} + + + {username} + {password} + + + + + + http://schemas.xmlsoap.org/ws/2005/02/trust/Issue + + + urn:federation:MicrosoftOnline + + + http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey + + + \ No newline at end of file diff --git a/src/Runtime/Utilities/RequestOptions.php b/src/Runtime/Utilities/RequestOptions.php index 26fbe63e..9ee349d9 100644 --- a/src/Runtime/Utilities/RequestOptions.php +++ b/src/Runtime/Utilities/RequestOptions.php @@ -49,10 +49,12 @@ public function toArray() public function addCustomHeader($name, $value) { - if (is_null($this->Headers)) + if (is_null($this->Headers)) { $this->Headers = array(); - if (!array_key_exists($name, $this->Headers)) + } + if (!array_key_exists($name, $this->Headers)) { $this->Headers[$name] = $value; + } } public function getRawHeaders() diff --git a/src/Runtime/Utilities/Requests.php b/src/Runtime/Utilities/Requests.php index 60c093cc..61a47e09 100644 --- a/src/Runtime/Utilities/Requests.php +++ b/src/Runtime/Utilities/Requests.php @@ -49,6 +49,14 @@ public static function get($url,$headers) return Requests::execute($options); } + public static function getHead($url,$headers) + { + $options = new RequestOptions($url); + $options->Headers = $headers; + $options->IncludeHeaders = $headers; + return Requests::execute($options); + } + public static function head($url,$headers) { $options = new RequestOptions($url);