Skip to content

Commit

Permalink
Merge pull request #111 from mpastas/feature/108
Browse files Browse the repository at this point in the history
Initial support to federated STS authentication
  • Loading branch information
vgrem authored Jul 17, 2018
2 parents 1f4a9c5 + 2f7f590 commit 71b69ab
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 11 deletions.
215 changes: 206 additions & 9 deletions src/Runtime/Auth/SamlTokenProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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;
}

Expand All @@ -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'];
}
}


Expand All @@ -93,31 +147,119 @@ 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)
{
$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');

}

/**
Expand All @@ -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 <wsse:Security> node
$parentNode->appendChild($samlAssertion);
return $xml->saveXML();
}

return NULL;
}
}
1 change: 1 addition & 0 deletions src/Runtime/Auth/xml/RST2.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><S:Envelope xmlns:S="http://www.w3.org/2003/05/soap-envelope" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust"><S:Header><wsa:Action S:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action><wsa:To S:mustUnderstand="1">https://login.microsoftonline.com/rst2.srf</wsa:To><ps:AuthInfo xmlns:ps="http://schemas.microsoft.com/LiveID/SoapServices/v1" Id="PPAuthInfo"><ps:BinaryVersion>5</ps:BinaryVersion><ps:HostingApp>Managed IDCRL</ps:HostingApp></ps:AuthInfo><wsse:Security><!--Replace the following node with the SAML Assertion--></wsse:Security></S:Header><S:Body><wst:RequestSecurityToken xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust" Id="RST0"><wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType><wsp:AppliesTo><wsa:EndpointReference><wsa:Address>sharepoint.com</wsa:Address></wsa:EndpointReference></wsp:AppliesTo><wsp:PolicyReference URI="MBI"></wsp:PolicyReference></wst:RequestSecurityToken></S:Body></S:Envelope>
25 changes: 25 additions & 0 deletions src/Runtime/Auth/xml/federatedSAML.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:wssc="http://schemas.xmlsoap.org/ws/2005/02/sc" xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust">
<s:Header>
<wsa:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
<wsa:To s:mustUnderstand="1">{federated_sts_url}</wsa:To>
<wsa:MessageID>{message_uuid}</wsa:MessageID>
<wsse:Security>
<wsse:UsernameToken wsu:Id="user">
<wsse:Username>{username}</wsse:Username>
<wsse:Password>{password}</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</s:Header>
<s:Body>
<wst:RequestSecurityToken Id="RST0">
<wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
<wsp:AppliesTo>
<wsa:EndpointReference>
<wsa:Address>urn:federation:MicrosoftOnline</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<wst:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</wst:KeyType>
</wst:RequestSecurityToken>
</s:Body>
</s:Envelope>
6 changes: 4 additions & 2 deletions src/Runtime/Utilities/RequestOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
8 changes: 8 additions & 0 deletions src/Runtime/Utilities/Requests.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 71b69ab

Please sign in to comment.