Skip to content

Commit 71b69ab

Browse files
authored
Merge pull request #111 from mpastas/feature/108
Initial support to federated STS authentication
2 parents 1f4a9c5 + 2f7f590 commit 71b69ab

File tree

5 files changed

+244
-11
lines changed

5 files changed

+244
-11
lines changed

src/Runtime/Auth/SamlTokenProvider.php

Lines changed: 206 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ class SamlTokenProvider extends BaseTokenProvider
1616
*/
1717
private static $StsUrl = 'https://login.microsoftonline.com/extSTS.srf';
1818

19+
/**
20+
* RST2 URL
21+
* @var string
22+
*/
23+
private static $RST2Url = 'https://login.microsoftonline.com/rst2.srf';
24+
25+
/**
26+
* To Get the STS authentication url if $StsUrl request fails.
27+
* @var string
28+
*/
29+
private static $RealmUrlTemplate = 'https://login.microsoftonline.com/getuserrealm.srf?login={username}&xml=1';
30+
31+
1932
/**
2033
* Form Url to submit SAML token
2134
* @var string
@@ -24,6 +37,20 @@ class SamlTokenProvider extends BaseTokenProvider
2437

2538

2639
/**
40+
* Boolean to determine whether the system is using Federated STS or not.
41+
* @var
42+
*/
43+
protected $usingFederatedSTS;
44+
45+
/**
46+
* Form Url to submit SAML token if Federated STS is set.
47+
* @var string
48+
*/
49+
private static $IDCRLSVCPageUrl = '/_vti_bin/idcrl.svc/';
50+
51+
52+
53+
/**
2754
* @var string
2855
*/
2956
protected $authorityUrl;
@@ -41,15 +68,26 @@ class SamlTokenProvider extends BaseTokenProvider
4168
*/
4269
private $rtFa;
4370

71+
/**
72+
* Federated STS Auth Cookie
73+
* @var
74+
*/
75+
private $SPOIDCRL;
76+
4477

4578
public function __construct($authorityUrl)
4679
{
4780
$this->authorityUrl = $authorityUrl;
81+
$this->usingFederatedSTS = FALSE;
4882
}
4983

5084

5185
public function getAuthenticationCookie()
5286
{
87+
if ($this->usingFederatedSTS) {
88+
return 'SPOIDCRL=' . $this->SPOIDCRL;
89+
}
90+
5391
return 'FedAuth=' . $this->FedAuth . '; rtFa=' . $this->rtFa;
5492
}
5593

@@ -73,11 +111,27 @@ public function acquireToken($parameters)
73111
protected function acquireAuthenticationCookies($token)
74112
{
75113
$urlInfo = parse_url($this->authorityUrl);
114+
76115
$url = $urlInfo['scheme'] . '://' . $urlInfo['host'] . self::$SignInPageUrl;
77-
$response = Requests::post($url,null,$token,true);
78-
$cookies = Requests::parseCookies($response);
79-
$this->FedAuth = $cookies['FedAuth'];
80-
$this->rtFa = $cookies['rtFa'];
116+
if ($this->usingFederatedSTS) {
117+
$url = $urlInfo['scheme'] . '://' . $urlInfo['host'] . self::$IDCRLSVCPageUrl;
118+
119+
$headers = array();
120+
$headers['User-Agent'] = '';
121+
$headers['X-IDCRL_ACCEPTED'] = 't';
122+
$headers['Authorization'] = 'BPOSIDCRL ' . $token;
123+
$headers['Content-Type'] = 'application/x-www-form-urlencoded';
124+
125+
$response = Requests::getHead($url,$headers,$token,true);
126+
$cookies = Requests::parseCookies($response);
127+
$this->SPOIDCRL = $cookies['SPOIDCRL'];
128+
}
129+
else {
130+
$response = Requests::post($url,null,$token,true);
131+
$cookies = Requests::parseCookies($response);
132+
$this->FedAuth = $cookies['FedAuth'];
133+
$this->rtFa = $cookies['rtFa'];
134+
}
81135
}
82136

83137

@@ -93,31 +147,119 @@ protected function acquireSecurityToken($username, $password)
93147
{
94148
$data = $this->prepareSecurityTokenRequest($username, $password, $this->authorityUrl);
95149
$response = Requests::post(self::$StsUrl,null,$data);
150+
151+
try {
152+
$this->processSecurityTokenResponse($response);
153+
}
154+
catch (Exception $e) {
155+
// Try to get the token with a federated authentication.
156+
$response = $this->acquireSecurityTokenFromFederatedSTS($username, $password);
157+
158+
}
96159
return $this->processSecurityTokenResponse($response);
97160
}
98161

162+
/**
163+
* Acquire the service token from Federated STS
164+
*
165+
* @param string $username
166+
* @param string $password
167+
* @return string
168+
*/
169+
protected function acquireSecurityTokenFromFederatedSTS($username, $password) {
170+
171+
$response = Requests::get(str_replace('{username}', $username, self::$RealmUrlTemplate),null);
172+
$federatedStsUrl = $this->getFederatedAuthenticationInformation($response);
173+
174+
if ($federatedStsUrl) {
175+
$message_id = md5(uniqid($username . '-' . time() . '-' . rand() , true));
176+
$data = $this->prepareSecurityFederatedTokenRequest($username, $password, $message_id, $federatedStsUrl->textContent);
177+
178+
$headers = array();
179+
$headers['Content-Type'] = 'application/soap+xml';
180+
$response = Requests::post($federatedStsUrl->textContent, $headers, $data);
181+
182+
$samlAssertion = $this->getSamlAssertion($response);
183+
184+
if ($samlAssertion) {
185+
$samlAssertion_node = $samlAssertion->item(0);
186+
$data = $this->prepareRST2Request($samlAssertion_node);
187+
$response = Requests::post(self::$RST2Url, $headers, $data);
188+
$this->usingFederatedSTS = TRUE;
189+
190+
return $response;
191+
}
192+
}
193+
194+
return NULL;
195+
}
196+
197+
/**
198+
* Get SAML assertion Node so it can be used within the RST2 template
199+
* @param $response
200+
* @return \DOMNodeList|null
201+
*/
202+
protected function getSamlAssertion($response) {
203+
$xml = new \DOMDocument();
204+
$xml->loadXML($response);
205+
$xpath = new \DOMXPath($xml);
206+
207+
if ($xpath->query("//*[name()='saml:Assertion']")->length > 0) {
208+
$nodeToken = $xpath->query("//*[name()='saml:Assertion']");
209+
if (!empty($nodeToken)) {
210+
return $nodeToken;
211+
}
212+
}
213+
return NULL;
214+
}
215+
216+
/**
217+
* Retrieves the STS federated URL if any.
218+
* @param $response
219+
* @return string Federated STS Url
220+
*/
221+
protected function getFederatedAuthenticationInformation($response) {
222+
if ($response) {
223+
$xml = new \DOMDocument();
224+
$xml->loadXML($response);
225+
$xpath = new \DOMXPath($xml);
226+
if ($xpath->query("//STSAuthURL")->length > 0) {
227+
return $xpath->query("//STSAuthURL")->item(0);
228+
}
229+
}
230+
return '';
231+
}
99232

100233
/**
101234
* Verify and extract security token from the HTTP response
102235
* @param mixed $response
103-
* @return mixed
236+
* @return mixed BinarySecurityToken or Exception when an error is present
104237
* @throws Exception
105238
*/
106239
protected function processSecurityTokenResponse($response)
107240
{
108241
$xml = new \DOMDocument();
109242
$xml->loadXML($response);
110243
$xpath = new \DOMXPath($xml);
244+
if ($xpath->query("//wsse:BinarySecurityToken")->length > 0) {
245+
$nodeToken = $xpath->query("//wsse:BinarySecurityToken")->item(0);
246+
if (!empty($nodeToken)) {
247+
return $nodeToken->nodeValue;
248+
}
249+
}
250+
111251
if ($xpath->query("//S:Fault")->length > 0) {
112252
$nodeErr = $xpath->query("//S:Fault/S:Detail/psf:error/psf:internalerror/psf:text")->item(0);
113253
throw new \Exception($nodeErr->nodeValue);
114254
}
115-
$nodeToken = $xpath->query("//wsse:BinarySecurityToken")->item(0);
116-
if (empty($nodeToken)) {
117-
throw new \RuntimeException('Error trying to get a token, check your URL or credentials');
255+
256+
if ($xpath->query("//S:Fault")->length > 0) {
257+
$nodeErr = $xpath->query("//S:Fault/S:Detail/psf:error/psf:internalerror/psf:text")->item(0);
258+
throw new \Exception($nodeErr->nodeValue);
118259
}
119260

120-
return $nodeToken->nodeValue;
261+
throw new \RuntimeException('Error trying to get a token, check your URL or credentials');
262+
121263
}
122264

123265
/**
@@ -142,4 +284,59 @@ protected function prepareSecurityTokenRequest($username, $password, $address)
142284
$template = str_replace('{address}', $address, $template);
143285
return $template;
144286
}
287+
288+
/**
289+
* Construct the request body to acquire security token from Federated STS endpoint (sts.yourcompany.com)
290+
*
291+
* @param $username
292+
* @param $password
293+
* @param $message_uuid
294+
* @param $federated_sts_url
295+
* @return string
296+
* @throws Exception
297+
*/
298+
protected function prepareSecurityFederatedTokenRequest($username, $password, $message_uuid, $federated_sts_url)
299+
{
300+
$fileName = __DIR__ . '/xml/federatedSAML.xml';
301+
if (!file_exists($fileName)) {
302+
throw new \Exception("The file $fileName does not exist");
303+
}
304+
305+
$template = file_get_contents($fileName);
306+
$template = str_replace('{username}', $username, $template);
307+
$template = str_replace('{password}', $password, $template);
308+
$template = str_replace('{federated_sts_url}', $federated_sts_url, $template);
309+
$template = str_replace('{message_uuid}', $message_uuid, $template);
310+
return $template;
311+
}
312+
313+
/**
314+
* Prepare the request to be sent to RST2 endpoint with the saml assertion
315+
* @param $samlAssertion
316+
* @return bool|mixed|string
317+
* @throws \Exception
318+
*/
319+
protected function prepareRST2Request($samlAssertion)
320+
{
321+
322+
$fileName = __DIR__ . '/xml/RST2.xml';
323+
if (!file_exists($fileName)) {
324+
throw new \Exception("The file $fileName does not exist");
325+
}
326+
$template = file_get_contents($fileName);
327+
328+
$xml = new \DOMDocument();
329+
$xml->loadXML($template);
330+
$xpath = new \DOMXPath($xml);
331+
332+
$samlAssertion = $xml->importNode($samlAssertion, true);
333+
if ($xpath->query("//*[name()='wsse:Security']")->length > 0) {
334+
$parentNode = $xpath->query("//wsse:Security")->item(0);
335+
//append "saml assertion" node to <wsse:Security> node
336+
$parentNode->appendChild($samlAssertion);
337+
return $xml->saveXML();
338+
}
339+
340+
return NULL;
341+
}
145342
}

src/Runtime/Auth/xml/RST2.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<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">
3+
<s:Header>
4+
<wsa:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
5+
<wsa:To s:mustUnderstand="1">{federated_sts_url}</wsa:To>
6+
<wsa:MessageID>{message_uuid}</wsa:MessageID>
7+
<wsse:Security>
8+
<wsse:UsernameToken wsu:Id="user">
9+
<wsse:Username>{username}</wsse:Username>
10+
<wsse:Password>{password}</wsse:Password>
11+
</wsse:UsernameToken>
12+
</wsse:Security>
13+
</s:Header>
14+
<s:Body>
15+
<wst:RequestSecurityToken Id="RST0">
16+
<wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
17+
<wsp:AppliesTo>
18+
<wsa:EndpointReference>
19+
<wsa:Address>urn:federation:MicrosoftOnline</wsa:Address>
20+
</wsa:EndpointReference>
21+
</wsp:AppliesTo>
22+
<wst:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</wst:KeyType>
23+
</wst:RequestSecurityToken>
24+
</s:Body>
25+
</s:Envelope>

src/Runtime/Utilities/RequestOptions.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ public function toArray()
4949

5050
public function addCustomHeader($name, $value)
5151
{
52-
if (is_null($this->Headers))
52+
if (is_null($this->Headers)) {
5353
$this->Headers = array();
54-
if (!array_key_exists($name, $this->Headers))
54+
}
55+
if (!array_key_exists($name, $this->Headers)) {
5556
$this->Headers[$name] = $value;
57+
}
5658
}
5759

5860
public function getRawHeaders()

src/Runtime/Utilities/Requests.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ public static function get($url,$headers)
4949
return Requests::execute($options);
5050
}
5151

52+
public static function getHead($url,$headers)
53+
{
54+
$options = new RequestOptions($url);
55+
$options->Headers = $headers;
56+
$options->IncludeHeaders = $headers;
57+
return Requests::execute($options);
58+
}
59+
5260
public static function head($url,$headers)
5361
{
5462
$options = new RequestOptions($url);

0 commit comments

Comments
 (0)