## Apache Zeta Components Manual :: File Source for openid_filter.php

## Source for file openid_filter.php

Documentation is available at openid_filter.php

1. <?php
2. /**
3. * File containing the ezcAuthenticationOpenidFilter class.
4. *
5. * Licensed to the Apache Software Foundation (ASF) under one
6. * or more contributor license agreements. See the NOTICE file
7. * distributed with this work for additional information
8. * regarding copyright ownership. The ASF licenses this file
9. * to you under the Apache License, Version 2.0 (the
10. * "License"); you may not use this file except in compliance
11. * with the License. You may obtain a copy of the License at
12. *
13. * http://www.apache.org/licenses/LICENSE-2.0
14. *
15. * Unless required by applicable law or agreed to in writing,
16. * software distributed under the License is distributed on an
17. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18. * KIND, either express or implied. See the License for the
19. * specific language governing permissions and limitations
20. * under the License.
21. *
22. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
23. * @filesource
24. * @package Authentication
25. * @version //autogen//
26. */
28. /**
29. * Filter to authenticate against OpenID. Currently supporting OpenID 1.0, 1.1
30. * and 2.0.
31. *
32. * The filter takes an identifier (URL) as credentials, and performs these steps
33. * (by default, with redirection of the user agent to the OpenID provider):
34. * 1. Normalize the identifier
35. * 2. Discover the provider and delegate by requesting the URL
36. * - first using the Yadis discovery protocol
37. * - if Yadis fails then discover by parsing the HTML page source at URL
38. * 3. (Optional) OpenID associate request - for the so-called 'smart' (stateful) mode.
39. * 4. OpenID checkid_setup request. This step redirects the browser to the OpenID
40. * provider discovered in step 2. After user enters his OpenID username and
41. * password at this page and accepts the originating site, the browser is
42. * redirected back to the originating site. The return URL can be changed
43. * with the OpenID option returnUrl (see {@link ezcAuthenticationOpenidOptions}).
44. * 5. OpenID check_authentication request. After the redirection from the provider
45. * to the originating site, the values provided by the provider must be checked
46. * in an extra request against the provider. The provider responds with is_valid
47. * true or false.
48. *
49. * The OpenID request checkid_immediate is supported, which allows for user
50. * authentication in a pop-up window or iframe (or similar techniques). The steps
51. * of the authentication process are the same as above, but step 4 changes as
52. * follows:
53. * 4. OpenID checkid_immediate request. This step asks the OpenID provider if the
54. * user can be authenticated on the spot, with no redirection. If the user
55. * cannot be authenticated, the provider sends back a setup URL, which the
56. * application can use in a pop-up window or iframe to display to the user
57. * so that he can authenticate himself to the OpenID provider. After user
58. * enters his OpenID username and password at this page and accepts the
59. * originating site, the pop-up window or iframe is redirected to the
60. * return URL value (which should be a different page than the page which
61. * opens the pop-up window). The return URL page will then inform the
62. * main page of success or failure through JavaScript, and the main page
63. * can do the action that it needs to perform based on the outcome in the
64. * pop-up page. The checkid_immediate mode is enabled by setting the
65. * option immediate to true.
66. *
67. * For example, this is one simple way of implementing checkid_immediate:
68. * - the main page contains the OpenID login form (where the user types in his
69. * OpenID identifier). This page contains also a hidded form value which
70. * specifies to which page to return to in the pop-up window. The Enter key
71. * and the submit button should be disabled on the form. When user clicks on
72. * the Login button, the main page should employ AJAX to request the return
73. * URL. When the return URL finishes loading, the main page will read from the
74. * return URL page the setup URL and it will open it in a pop-up/iframe.
75. * - the return URL page enables the option immediate to the OpenID filter, and
76. * runs the filter. It gets back the setup URL and it echoes it to be picked-up
77. * by the main page once the return URL page will finish loading. The setup URL
78. * should be the only thing that the return URL page is echoing, to not interfere
79. * with the main page.
80. * - in the pop-up/iframe the setup URL will load, which basically depends on
81. * the OpenID provider how it is handled by the user. After the user enters
82. * his credentials on the setup URL page, he will be redirected to the return URL,
83. * which should detect this, and which should inform the main page that the
84. * user was authenticated to the OpenID provider.
85. *
86. * As this mode required advanced JavaScript techniques and AJAX, no example
87. * source code will be provided here as it is out of the scope of this
88. * documentation. A rudimentary example is provided in the tutorial.
89. *
90. * Specifications:
91. * - OpenID 1.0: {@link http://openid.net/specs/openid-simple-registration-extension-1_0.html}
92. * - OpenID 1.1: {@link http://openid.net/specs/openid-authentication-1_1.html}
93. * - OpenID 2.0: {@link http://openid.net/specs/openid-authentication-2_0.html}
94. * - Yadis 1.0: {@link http://yadis.org}
95. * - XRDS discovery: {@link http://docs.oasis-open.org/xri/2.0/specs/cd02/xri-resolution-V2.0-cd-02.html}
96. *
97. * To specify which OpenID version to authenticate against, use the
98. * openidVersion option (default is '1.1'):
99. *
100. * <code>
101. * $options = new ezcAuthenticationOpenidOptions();
102. * $options->version = ezcAuthenticationOpenidFilter::VERSION_2_0;
103. * $filter = new ezcAuthenticationOpenidFilter( $options );
104. * </code>
105. *
106. * OpenID 2.0 will use XRDS discovery by default instead of Yadis. If XRDS
107. * discovery fails (or if the specified version is not '2.0') then Yadis
108. * discovery will be performed, and if Yadis fails then HTML discovery
109. * will be performed.
110. *
111. * Example of use (authentication code + login form + logout support) - stateless ('dumb'):
112. * <code>
113. * // no headers should be sent before calling $session->start()
114. * $session = new ezcAuthenticationSession();
115. * $session->start();
116. *
117. * $url = isset( $_GET['openid_identifier'] ) ? $_GET['openid_identifier'] : $session->load();
118. * $action = isset( $_GET['action'] ) ? strtolower( $_GET['action'] ) : null;
119. *
120. * $credentials = new ezcAuthenticationIdCredentials( $url );
121. * $authentication = new ezcAuthentication( $credentials );
122. * $authentication->session = $session;
123. *
124. * if ( $action === 'logout' )
125. * {
126. * $session->destroy();
127. * }
128. * else
129. * {
130. * $filter = new ezcAuthenticationOpenidFilter();
131. * $authentication->addFilter( $filter );
132. * }
133. *
134. * if ( !$authentication->run() )
135. * {
136. * // authentication did not succeed, so inform the user
137. * $status = $authentication->getStatus();
138. * $err = array(
139. * 'ezcAuthenticationOpenidFilter' => array(
140. * ezcAuthenticationOpenidFilter::STATUS_SIGNATURE_INCORRECT => 'OpenID said the provided identifier was incorrect',
141. * ezcAuthenticationOpenidFilter::STATUS_CANCELLED => 'The OpenID authentication was cancelled',
142. * ezcAuthenticationOpenidFilter::STATUS_URL_INCORRECT => 'The identifier you provided is invalid'
143. * ),
144. * 'ezcAuthenticationSession' => array(
145. * ezcAuthenticationSession::STATUS_EMPTY => '',
146. * ezcAuthenticationSession::STATUS_EXPIRED => 'Session expired'
147. * )
148. * );
149. * foreach ( $status as $line )
150. * {
151. * list( $key, $value ) = each( $line );
152. * echo $err[$key][$value] . "\n";
153. * }
154. * ?>
155. * Please login with your OpenID identifier (an URL, eg. www.example.com or http://www.example.com):
156. * <form method="GET" action="">
157. * <input type="hidden" name="action" value="login" />
158. * <img src="http://openid.net/login-bg.gif" /> <input type="text" name="openid_identifier" />
159. * <input type="submit" value="Login" />
160. * </form>
161. *
162. * <?php
163. * }
164. * else
165. * {
166. * ?>
167. *
168. * You are logged-in as <b><?php echo $url; ?></b> | <a href="?action=logout">Logout</a>
169. *
170. * <?php
171. * }
172. * ?>
173. * </code>
174. *
175. * To use stateful ('smart') mode, the only changes to the above example are in
176. * the else branch of the "if ( $action === 'logout' )":
177. * <code>
178. * // ...
179. * if ( $action === 'logout' )
180. * {
181. * $session->destroy();
182. * }
183. * else
184. * {
185. * $options = new ezcAuthenticationOpenidOptions();
186. * $options->mode = ezcAuthenticationOpenidFilter::MODE_SMART;
187. * $options->store = new ezcAuthenticationOpenidFileStore( '/tmp/store' );
188. * $filter = new ezcAuthenticationOpenidFilter( $options );
189. * $authentication->addFilter( $filter );
190. * }
191. * // ...
192. * </code>
193. *
194. * Extra data can be fetched from the OpenID provider during the authentication
195. * process, by registering the data to be fetched before calling run(). Example:
196. * <code>
197. * // $filter is an ezcAuthenticationOpenidFilter object
198. * $filter->registerFetchData( array( 'fullname', 'gender', 'country', 'language' ) );
199. *
200. * // after run()
201. * $data = $filter->fetchData();
202. * </code>
203. *
204. * The $data array will be something like:
205. * <code>
206. * array( 'fullname' => array( 'John Doe' ),
207. * 'gender' => array( 'M' ),
208. * 'country' => array( 'US' ),
209. * 'language' => array( 'FR' )
210. * );
211. * </code>
212. *
213. * The extra data which is possible to be fetched during the authentication
214. * process is:
215. * - nickname - the user's nickname (short name, alias)
216. * - email - the user's email address
217. * - fullname - the user's full name
218. * - dob - the user's date of birth as YYYY-MM-DD. Any component value whose
219. * representation uses fewer than the specified number of digits should
220. * be zero-padded (eg. 02 for February). If the user does not want to
221. * reveal any particular component of this value, it should be zero
222. * (eg. "1980-00-00" if the user is born in 1980 but does not want to
223. * specify his month and day of birth)
224. * - gender - the user's gender, "M" for male, "F" for female
225. * - postcode - the user's postal code
226. * - country - the user's country as an ISO3166 string, (eg. "US")
227. * - language - the user's preferred language as an ISO639 string (eg. "FR")
228. * - timezone - the user's timezone, for example "Europe/Paris"
229. *
230. * Note: if using the checkid_immediate mode (by setting the option immediate to
231. * true), then retrieval of extra data is not possible.
232. *
233. * @todo add support for fetching extra data as in OpenID attribute exchange
234. * {@link http://issues.ez.no/14847}
235. * @todo add support for multiple URLs in each category at discovery
236. * @todo add support for XRI identifiers and discovery
237. * @todo check if the nonce handling is correct (openid.response_nonce?)
238. * @todo check return_to in the response
239. *
240. * @package Authentication
241. * @version //autogen//
242. * @mainclass
243. */
244. class ezcAuthenticationOpenidFilter extends ezcAuthenticationFilter implements ezcAuthenticationDataFetch
245. {
246. /**
247. * The OpenID provider did not authorize the provided URL.
248. */
249. const STATUS_SIGNATURE_INCORRECT = 1;
251. /**
252. * The OpenID provider did not return a valid nonce in the response.
253. */
254. const STATUS_NONCE_INCORRECT = 2;
256. /**
257. * User cancelled the OpenID authentication.
258. */
259. const STATUS_CANCELLED = 3;
261. /**
262. * The URL provided by user was empty, or the required information could
263. * not be discovered from it.
264. *
265. * @todo remove and return STATUS_SIGNATURE_INCORRECT instead?
266. */
267. const STATUS_URL_INCORRECT = 4;
269. /**
270. * The OpenID server returned a setup URL after a checkid_immediate request,
271. * which is available by calling the getSetupUrl() method.
272. */
273. const STATUS_SETUP_URL = 5;
275. /**
276. * OpenID authentication mode where the OpenID provider generates a secret
277. * for every request.
278. *
279. * The server (consumer) is stateless.
280. * An extra check_authentication request to the provider is needed.
281. * This is the default mode.
282. */
283. const MODE_DUMB = 1;
285. /**
286. * OpenID authentication mode where the server generates a secret which will
287. * be shared with the OpenID provider.
288. *
289. * The server (consumer) is keeping state.
290. * The extra check_authentication request is not needed.
291. * The shared secret must be established once in a while (defined by the
292. * option secretValidity, default 1 day = 86400 seconds).
293. */
294. const MODE_SMART = 2;
296. /**
297. * OpenID version 1.0.
298. */
299. const VERSION_1_0 = '1.0';
301. /**
302. * OpenID version 1.1.
303. */
304. const VERSION_1_1 = '1.1';
306. /**
307. * OpenID version 2.0.
308. */
309. const VERSION_2_0 = '2.0';
311. /**
312. * The default value for p used in the Diffie-Hellman exchange.
313. *
314. * It is a confirmed prime number.
315. *
316. * @ignore
317. */
318. const DEFAULT_P = '155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443';
320. /**
321. * The default value for q used in the Diffie-Hellman exchange.
322. *
323. * @ignore
324. */
325. const DEFAULT_Q = '2';
327. /**
328. * Holds the setup URL retrieved during the checkid_immediate OpenID request.
329. *
330. * This URL can be used by the application to authenticate the user in a
331. * pop-up window or iframe (or similar techniques).
332. *
333. * @var string
334. */
335. protected $setupUrl = false;
337. /**
338. * Holds the attributes which will be requested during the authentication
339. * process.
340. *
341. * Usually it has this structure:
342. * <code>
343. * array( 'fullname', 'gender', 'country', 'language' );
344. * </code>
345. *
346. * @var array(string)
347. */
348. protected $requestedData = array();
350. /**
351. * Holds the extra data fetched during the authentication process.
352. *
353. * Usually it has this structure:
354. * <code>
355. * array( 'fullname' => array( 'John Doe' ),
356. * 'gender' => array( 'M' ),
357. * 'country' => array( 'NO' ),
358. * 'language' => array( 'FR' )
359. * );
360. * </code>
361. *
362. * @var array(string=>mixed)
363. */
364. protected $data = array();
366. /**
367. * Creates a new object of this class.
368. *
369. * @param ezcAuthenticationOpenidOptions $options Options for this class
370. */
371. public function __construct ( ezcAuthenticationOpenidOptions $options = null )
372. {
373. $this-> options = ( $options === null ) ? new ezcAuthenticationOpenidOptions () : $options;
374. }
376. /**
377. * Runs the filter and returns a status code when finished.
378. *
379. * @throws ezcAuthenticationOpenidModeNotSupportedException
380. * if trying to authenticate with an unsupported OpenID mode
381. *
382. * @param ezcAuthenticationIdCredentials $credentials Authentication credentials
383. * @return int
384. */
385. public function run ( $credentials )
386. {
387. $source = $this-> options ->requestSource;
388. $mode = isset( $source['openid_mode'] ) ? strtolower ( $source['openid_mode'] ) : null;
389. switch ( $mode )
390. {
391. case null:
392. if ( empty( $credentials->id ) )
393. {
394. return self::STATUS_URL_INCORRECT;
395. }
397. $providers = $this-> discover ( $credentials->id );
399. // @todo add support for multiple URLs in each category
400. if ( !$provider = $this-> getProvider ( $providers ) )
401. {
402. return self::STATUS_URL_INCORRECT;
403. }
405. $params = $this-> createCheckidRequest ( $credentials->id, $providers );
407. if ( $this-> options ->immediate === true )
408. {
409. $params['openid.mode'] = 'checkid_immediate';
410. $response = $this-> checkImmediate ( $provider, $params );
412. if ( $response !== false )
413. {
414. $this-> setupUrl = $response;
415. return self::STATUS_SETUP_URL;
416. }
417. else
418. {
419. return self::STATUS_URL_INCORRECT;
420. }
421. }
422. else
423. {
424. $params['openid.mode'] = 'checkid_setup';
425. $this-> redirectToOpenidProvider ( $provider, $params );
426. }
427. break;
429. case 'id_res':
430. $assocHandle = isset( $source['openid_assoc_handle'] ) ? $source['openid_assoc_handle'] : null;
431. $identity = isset( $source['openid_identity'] ) ? $source['openid_identity'] : null;
432. $sig = isset( $source['openid_sig'] ) ? $source['openid_sig'] : null;
433. $signed = isset( $source['openid_signed'] ) ? $source['openid_signed'] : null;
434. $returnTo = isset( $source['openid_return_to'] ) ? $source['openid_return_to'] : null;
435. $claimedId = isset( $source['openid_claimed_id'] ) ? $source['openid_claimed_id'] : null;
437. // @todo add verification of openid_identity and openid_claimed_id to the initial openid_identifier
439. if ( $this-> options ->store !== null )
440. {
441. $nonce = ezcAuthenticationUrl :: fetchQuery ( $returnTo, $this-> options ->nonceKey );
442. if ( $nonce !== null )
443. {
444. $nonceTimestamp = $this-> options ->store->useNonce( $nonce );
445. if ( $nonceTimestamp === false || time () - $nonceTimestamp > $this-> options ->nonceValidity )
446. {
447. return self::STATUS_NONCE_INCORRECT;
448. }
449. }
450. }
452. $params = array(
453. 'openid.assoc_handle' => $assocHandle,
454. 'openid.signed' => $signed,
455. 'openid.sig' => $sig,
456. 'openid.mode' => 'id_res'
457. );
459. $signed = explode ( ',', $signed );
460. for ( $i = 0; $i < count ( $signed ); $i++ )
461. {
462. $s = str_replace ( '.', '_', $signed[$i] );
463. $c = $source['openid_' . $s];
464. $params['openid.' . $signed[$i]] = isset( $params['openid.' . $s] ) ? $params['openid.' . $s] : $c;
465. if ( strpos ( $s, 'sreg_' ) !== false )
466. {
467. $this-> data [ str_replace ( 'sreg_', '', $s )] = array( $c );
468. }
469. }
471. if ( isset( $source['openid_op_endpoint'] ) )
472. {
473. // if the endpoint is available then use it, otherwise discover it
474. $provider = $source['openid_op_endpoint'];
476. // @todo check how to detect OpenID version from id_res response
477. }
478. else
479. {
480. // @todo cache this somewhere (in the request URL for example)
481. $providers = $this-> discover ( $credentials->id );
483. // @todo add support for multiple URLs in each category
484. if ( !$provider = $this-> getProvider ( $providers ) )
485. {
486. return self::STATUS_URL_INCORRECT;
487. }
488. }
490. if ( $this-> options ->mode === self::MODE_SMART )
491. {
492. $store = $this-> options ->store;
493. if ( $store !== null )
494. {
495. $association = $store->getAssociation( $provider );
496. if ( $association !== false &&
497. time () - $association->issued <= $association->validity
498. )
499. {
500. if ( $this-> checkSignatureSmart ( $association, $params ) )
501. {
502. return self::STATUS_OK;
503. }
504. else
505. {
506. return self::STATUS_SIGNATURE_INCORRECT;
507. }
508. }
509. }
510. }
512. // if smart mode didn't succeed continue with the dumb mode as usual
513. $params['openid.mode'] = 'check_authentication';
515. if ( $this-> options ->openidVersion === self::VERSION_2_0 )
516. {
517. $params['openid.ns'] = 'http://specs.openid.net/auth/2.0';
518. }
520. foreach ( $params as $key => $value )
521. {
522. $params[$key] = urlencode ( $value );
523. }
525. if ( $this-> checkSignature ( $provider, $params ) )
526. {
527. return self::STATUS_OK;
528. }
529. break;
531. case 'checkid_setup':
532. return self::STATUS_CANCELLED;
534. case 'cancel':
535. return self::STATUS_CANCELLED;
537. default:
538. throw new ezcAuthenticationOpenidModeNotSupportedException ( $mode );
539. }
540. return self::STATUS_SIGNATURE_INCORRECT;
541. }
543. /**
544. * Returns an array of parameters for use in an OpenID check_id request.
545. *
546. * @param string $id The OpenID identifier from the user
547. * @param array(string) $providers OpenID providers retrieved during discovery
548. * @return array(string=>array)
549. */
550. public function createCheckidRequest ( $id, array $providers )
551. {
552. $provider = $providers['openid.server'][0];
554. // if a delegate is found, it is used instead of the credentials
555. $identity = isset( $providers['openid.delegate'][0] ) ? $providers['openid.delegate'][0] :
556. $id;
558. $host = isset( $_SERVER["HTTP_HOST"] ) ? $_SERVER["HTTP_HOST"] : null;
559. $path = isset( $_SERVER["REQUEST_URI"] ) ? $_SERVER["REQUEST_URI"] : null;
561. if ( $this-> options ->mode === self:: MODE_SMART )
562. {
563. $store = $this-> options ->store;
564. if ( $store !== null )
565. {
566. $association = $store->getAssociation( $provider );
567. if ( $association === false ||
568. time() - $association->issued > $association->validity
569. )
570. {
571. $params = $this-> createAssociateRequest ();
572. $result = $this-> associate ( $provider, $params );
574. $secret = isset( $result['enc_mac_key'] ) ? $result['enc_mac_key'] : $result['mac_key'];
575. $association = new ezcAuthenticationOpenidAssociation ( $result['assoc_handle'],
576. $secret,
577. time(),
578. $result['expires_in'],
579. $result['assoc_type'] );
580. $store->storeAssociation( $provider, $association );
581. }
582. }
583. }
585. $returnUrl = $this-> options ->returnUrl;
586. if ( $returnUrl === null )
587. {
588. $returnUrl = "http://{$host}{$path}";
589. }
591. $nonce = $this-> generateNonce ( $this-> options ->nonceLength );
592. $returnTo = ezcAuthenticationUrl :: appendQuery ( $returnUrl, $this-> options ->nonceKey, $nonce );
593. $trustRoot = "http://{$host}";
595. if ( $this-> options ->mode === self::MODE_SMART && $store !== null )
596. {
597. $store->storeNonce( $nonce );
598. }
600. $params = array(
601. 'openid.return_to' => urlencode ( $returnTo ),
602. 'openid.trust_root' => urlencode ( $trustRoot ),
603. 'openid.identity' => urlencode ( $id ),
604. );
606. if ( $this-> options ->openidVersion === self::VERSION_2_0 )
607. {
608. $params['openid.identity' ] = urlencode ( 'http://specs.openid.net/auth/2.0/identifier_select' );
609. $params['openid.claimed_id' ] = urlencode ( 'http://specs.openid.net/auth/2.0/identifier_select' );
610. $params['openid.realm'] = urlencode ( $trustRoot );
611. $params['openid.ns'] = urlencode ( 'http://specs.openid.net/auth/2.0' );
612. }
614. if ( count ( $this-> requestedData ) > 0 )
615. {
616. $params['openid.sreg.optional'] = implode ( ',', $this-> requestedData );
617. $params['openid.ns.sreg'] = urlencode ( 'http://openid.net/sreg/1.0' );
618. }
620. if ( $this-> options ->mode === self::MODE_SMART && $store !== null )
621. {
622. $association = $store->getAssociation( $provider );
623. if ( $association !== false &&
624. time () - $association->issued <= $association->validity
625. )
626. {
627. $params['openid.assoc_handle'] = urlencode ( $association->handle );
628. }
629. }
631. return $params;
632. }
634. /**
635. * Returns an array of parameters for use in an OpenID associate request.
636. *
637. * @return array(string=>array)
638. */
639. protected function createAssociateRequest ()
640. {
641. $lib = ezcAuthenticationMath::createBignumLibrary();
642. $p = self::DEFAULT_P;
643. $q = self::DEFAULT_Q;
645. $private = $lib-> rand ( $p );
646. $public = $lib->powmod( $q, $private, $p );
647. $params = array(
648. 'openid.mode' => 'associate',
649. 'openid.assoc_type' => 'HMAC-SHA1',
651. // @todo add support for DH-SHA1 (is it needed if the connection is SSL?)
652. // 'openid.session_type' => 'DH-SHA1', // not supported yet
653. 'openid.dh_modulus' => urlencode ( base64_encode ( $lib->btwoc( $p ) ) ),
654. 'openid.dh_gen' => 2, urlencode ( base64_encode ( $lib->btwoc( $q ) ) ),
655. 'openid.dh_consumer_public' => urlencode ( base64_encode ( $lib->btwoc( $public ) ) )
656. );
658. return $params;
659. }
661. /**
662. * Discovers the OpenID server information from the provided URL.
663. *
664. * First the XRDS discovery is tried, if the openidVersion option is
665. * "2.0". If XRDS discovery fails, or if the openidVersion option is
666. * not "2.0", Yadis discovery is tried. If it doesn't succeed, then the
667. * HTML discovery is tried.
668. *
669. * The format of the returned array is (example):
670. * <code>
671. * array( 'openid.server' => array( 0 => 'http://www.example-provider.com' ),
672. * 'openid.delegate' => array( 0 => 'http://www.example-delegate.com' )
673. * );
674. * </code>
675. *
676. * @throws ezcAuthenticationOpenidConnectionException
677. * if connection to the URL could not be opened
678. * @param string $url URL to connect to and discover the OpenID information
679. * @return array(string=>array)
680. */
681. protected function discover ( $url )
682. {
683. $providers = array();
685. if ( $this-> options ->openidVersion === self::VERSION_2_0 )
686. {
687. $providers = $this-> discoverXrds ( $url );
688. }
690. if ( count ( $providers ) === 0 )
691. {
692. $providers = $this-> discoverYadis ( $url );
693. if ( count ( $providers ) === 0 )
694. {
695. $providers = $this-> discoverHtml ( $url );
696. }
697. }
699. foreach ( $providers as $key => $provider )
700. {
701. // normalize array keys
702. if ( $key === 'openid2.provider' )
703. {
704. unset( $providers[$key] );
705. $providers['openid.server'] = $provider;
706. }
708. if ( $key === 'openid2.local_id' )
709. {
710. unset( $providers[$key] );
711. $providers['openid.delegate'] = $provider;
712. }
713. }
714. return $providers;
715. }
717. /**
718. * Discovers the OpenID server information from the provided URL using XRDS.
719. *
720. * The format of the returned array is (example):
721. * <code>
722. * array( 'openid.server' => array( 0 => 'http://www.example-provider.com' ),
723. * 'openid.delegate' => array( 0 => 'http://www.example-delegate.com' )
724. * );
725. * </code>
726. *
727. * @throws ezcAuthenticationOpenidConnectionException
728. * if connection to the URL could not be opened
729. * @param string $url URL to connect to and discover the OpenID information
730. * @return array(string=>array)
731. */
732. protected function discoverXrds ( $url )
733. {
734. $url = ezcAuthenticationUrl :: normalize ( $url );
736. $src = ezcAuthenticationUrl :: getUrl ( $url, 'HEAD', 'application/xrds+xml' );
738. preg_match ( '@X-XRDS-Location:\s(.*)@', $src, $matches );
739. if ( isset( $matches[1] ) )
740. {
741. // get the XRDS document from the X-XRDS-Location URL
742. $url = $matches[1];
743. $src = ezcAuthenticationUrl :: getUrl ( $url, 'GET', 'application/xrds+xml' );
744. }
745. else
746. {
747. // get the XRDS document from the original location (provided $url)
748. $src = ezcAuthenticationUrl :: getUrl ( $url, 'GET', 'application/xrds+xml' );
749. }
750. $result = array();
752. // @todo check the regexp in this function, maybe they should be rewritten
754. // get the OpenID servers
755. $pattern = "#<URI[^>]*>(.*?)</URI>#si";
756. preg_match_all ( $pattern, $src, $matches );
757. $count = count ( $matches[0] );
758. for ( $i = 0; $i < $count; $i++ )
759. {
760. // force OpenID 2.0
761. $result['openid2.provider'][] = $matches[1][$i];
762. }
763. return $result;
764. }
766. /**
767. * Discovers the OpenID server information from the provided URL using Yadis.
768. *
769. * The format of the returned array is (example):
770. * <code>
771. * array( 'openid.server' => array( 0 => 'http://www.example-provider.com' ),
772. * 'openid.delegate' => array( 0 => 'http://www.example-delegate.com' )
773. * );
774. * </code>
775. *
776. * @throws ezcAuthenticationOpenidConnectionException
777. * if connection to the URL could not be opened
778. * @param string $url URL to connect to and discover the OpenID information
779. * @return array(string=>array)
780. */
781. protected function discoverYadis ( $url )
782. {
783. $url = ezcAuthenticationUrl :: normalize ( $url );
785. $src = ezcAuthenticationUrl :: getUrl ( $url, 'GET', 'application/xrds+xml' );
786. $result = array();
788. // @todo check the regexp in this function, maybe they should be rewritten
790. // get the OpenID servers
791. $pattern = "#<URI[^>]*>(.*?)</URI>#si";
792. preg_match_all ( $pattern, $src, $matches );
793. $count = count ( $matches[0] );
794. for ( $i = 0; $i < $count; $i++ )
795. {
796. $result['openid.server'][] = $matches[1][$i];
797. }
799. // get the OpenID delegates
800. // @todo Add support for OpenID 2.0 <openid:LocalID> tags
801. $pattern = "#<openid:Delegate[^>]*>(.*?)</openid:Delegate>#si";
802. preg_match_all ( $pattern, $src, $matches );
803. $count = count ( $matches[0] );
804. for ( $i = 0; $i < $count; $i++ )
805. {
806. $result['openid.delegate'][] = $matches[1][$i];
807. }
808. return $result;
809. }
811. /**
812. * Discovers the OpenID server information from the provided URL by parsing
813. * the HTML page source.
814. *
815. * The format of the returned array is (example):
816. * <code>
817. * array( 'openid.server' => array( 0 => 'http://www.example-provider.com' ),
818. * 'openid.delegate' => array( 0 => 'http://www.example-delegate.com' )
819. * );
820. * </code>
821. *
822. * @throws ezcAuthenticationOpenidConnectionException
823. * if connection to the URL could not be opened
824. * @param string $url URL to connect to and discover the OpenID information
825. * @return array(string=>array)
826. */
827. protected function discoverHtml ( $url )
828. {
829. $url = ezcAuthenticationUrl :: normalize ( $url );
831. $src = ezcAuthenticationUrl :: getUrl ( $url, 'GET', 'text/html' );
832. $result = array();
834. $pattern = "%<\w.*rel\=[\s\"'`]*([\w:?=@&\/#._;-]+)[\s\"'`]*[^>]*>%i";
835. preg_match_all ( $pattern, $src, $matches );
836. $count = count ( $matches[0] );
838. for ( $i = 0; $i < $count; $i++ )
839. {
840. if ( stristr ( $matches[1][$i], 'openid' ) !== false )
841. {
842. $pattern = "%.*href\=[\s\"'`]*([\w:?=@&\/#._;-]+)[\s\"'`]*%i";
843. preg_match ( $pattern, $matches[0][$i], $href );
844. $result[ strtolower ( $matches[1][$i] )][] = $href[1];
845. }
846. }
847. return $result;
848. }
850. /**
851. * Performs a redirection to $provider with the specified parameters $params
852. * (checkid_setup OpenID request).
853. *
854. * The format of the checkid_setup $params array is:
855. * <code>
856. * array(
857. * 'openid.return_to' => urlencode( URL ),
858. * 'openid.trust_root' => urlencode( URL ),
859. * 'openid.identity' => urlencode( URL ),
860. * 'openid.mode' => 'checkid_setup'
861. * );
862. * </code>
863. *
864. * @throws ezcAuthenticationOpenidRedirectException
865. * if redirection could not be performed
866. * @param string $provider The OpenID provider (discovered in HTML or Yadis)
867. * @param array(string=>string) $params OpenID parameters for checkid_setup
868. */
869. protected function redirectToOpenidProvider ( $provider, array $params )
870. {
871. $redirect = $provider . "?" . urldecode( http_build_query( $params ) );
873. if ( PHP_SAPI !== 'cli' )
874. {
875. if ( headers_sent () )
876. {
877. echo "<script language='JavaScript'>window.location='{$redirect}';</script>";
878. }
879. else
880. {
881. header ( 'Location: ' . $redirect );
882. }
883. }
885. // Normally the user should not see the following error because he was redirected
886. throw new ezcAuthenticationOpenidRedirectException ( $redirect );
887. }
889. /**
890. * Returns the first provider in the $providers array if exists, otherwise
891. * returns false.
892. *
893. * @param array(string=>array) $providers OpenID providers discovered during OpenID discovery
894. * @return bool|string
895. */
896. protected function getProvider ( array $providers )
897. {
898. if ( isset( $providers['openid.server'][0] ) )
899. {
900. return $providers['openid.server'][0];
901. }
903. return false;
904. }
906. /**
907. * Connects to $provider (checkid_immediate OpenID request) and returns an
908. * URL (setup URL) which can be used by the application in a pop-up window.
909. *
910. * The format of the check_authentication $params array is:
911. * <code>
912. * array(
913. * 'openid.return_to' => urlencode( URL ),
914. * 'openid.trust_root' => urlencode( URL ),
915. * 'openid.identity' => urlencode( URL ),
916. * 'openid.mode' => 'checkid_immediate'
917. * );
918. * </code>
919. *
920. * @throws ezcAuthenticationOpenidException
921. * if connection to the OpenID provider could not be opened
922. * @param string $provider The OpenID provider (discovered in HTML or Yadis)
923. * @param array(string=>string) $params OpenID parameters for checkid_immediate mode
924. * @param string $method The method to connect to the provider (default GET)
925. * @return bool
926. */
927. protected function checkImmediate ( $provider, array $params, $method = 'GET' )
928. {
929. $parts = parse_url( $provider );
930. $path = isset( $parts['path'] ) ? $parts['path'] : '/';
931. $host = isset( $parts['host'] ) ? $parts['host'] : null;
932. $port = 80;
934. // suppress warnings caused by fsockopen() if $host is not a valid domain
935. $connection = @ fsockopen ( $host, $port, $errno, $errstr, $this->options->timeoutOpen );
936. if ( !$connection )
937. {
938. throw new ezcAuthenticationOpenidException ( "Could not connect to host {$host}:{$port}: {$errstr}." );
939. }
940. else
941. {
942. stream_set_timeout ( $connection, $this-> options ->timeout );
943. $url = $path . '?' . urldecode ( http_build_query ( $params ) );
944. $headers = array( "{$method} {$url} HTTP/1.0

", "Host: {$host}", "Connection: close" );
- fputs ( $connection, implode ( "\r\n", $headers ) . "\r\n\r\n" );
- $src = stream_get_contents ( $connection );
- fclose ( $connection );
- $pattern = "/Location:\s(.*)/";
- if ( preg_match ( $pattern, $src, $matches ) > 0 )
- {
- $returnUrl = trim ( $matches[1] );
- // get the query parameters from the response URL
- $query = parse_url ( $returnUrl, PHP_URL_QUERY );
- $vars = ezcAuthenticationUrl :: parseQueryString ( $query );
- // get the openid.user_setup_url value from the response URL
- $setupUrl = isset( $vars['openid.user_setup_url'] ) ? $vars['openid.user_setup_url'] : false;
- if ( $setupUrl !== false )
- {
- // the next call to OpenID will be check_authentication
- $vars['openid.mode'] = 'check_authentication';
- // get the query parameters from the openid.user_setup_url in $setupParams
- // and the other parts of the URL in $parts
- $parts = parse_url ( $setupUrl );
- $query = isset( $parts['query'] ) ? $parts['query'] : false;
- $setupParams = ezcAuthenticationUrl :: parseQueryString ( $query );
- // merge the setup_url query parameters with all the other query parameters
- $params = array_merge ( $vars, $setupParams );
- // return the setup URL combined with the rest of the query parameters
- $parts['query'] = $params;
- $setupUrl = ezcAuthenticationUrl :: buildUrl ( $parts );
- }
- return $setupUrl;
- }
- }
- // the response from the OpenID server did not contain setup_url
- return false;
- }
- /**
- * Opens a connection with the OpenID provider and checks if $params are
- * correct (check_authentication OpenID request).
- *
- * The format of the check_authentication $params array is:
- * <code>
- * array(
- * 'openid.assoc_handle' => urlencode( HANDLE ),
- * 'openid.signed' => urlencode( SIGNED ),
- * 'openid.sig' => urlencode( SIG ),
- * 'openid.mode' => 'check_authentication'
- * );
- * </code>
- * where HANDLE, SIGNED and SIG are parameters returned from the provider in
- * the id_res step of OpenID authentication. In addition, the $params array
- * must contain the values present in SIG.
- *
- * @throws ezcAuthenticationOpenidException
- * if connection to the OpenID provider could not be opened
- * @param string $provider The OpenID provider (discovered in HTML or Yadis)
- * @param array(string=>string) $params OpenID parameters for check_authentication mode
- * @param string $method The method to connect to the provider (default GET)
- * @return bool
- */
- protected function checkSignature ( $provider, array $params, $method = 'GET' )
- {
- $parts = parse_url( $provider );
- $path = isset( $parts['path'] ) ? $parts['path'] : '/';
- $host = isset( $parts['host'] ) ? $parts['host'] : null;
- $port = 443;
- // suppress warnings caused by fsockopen() if $host is not a valid domain
- $connection = @ fsockopen ( 'ssl://' . $host, $port, $errno, $errstr, $this->options->timeoutOpen );
- if ( !$connection )
- {
- throw new ezcAuthenticationOpenidException ( "Could not connect to host {$host}:{$port}: {$errstr}." );
- }
- else
- {
- stream_set_timeout ( $connection, $this-> options ->timeout );
- $url = $path . '?' . urldecode ( http_build_query ( $params ) );
- $headers = array( "{$method} {$url} HTTP/1.0", "Host: {$host}", "Connection: close" );
- fputs ( $connection, implode ( "\r\n", $headers ) . "\r\n\r\n" );
- $src = stream_get_contents ( $connection );
- fclose ( $connection );
- $r = array();
- $response = explode ( "\n", $src );
- foreach ( $response as $line )
- {
- $line = trim ( $line );
- if ( !empty( $line ) && strpos ( $line, ':' ) !== false )
- {
- list( $key, $value ) = explode ( ':', $line, 2 );
- $r[ trim ( $key )] = trim ( $value );
- }
- }
- if ( isset( $r['is_valid'] ) )
- {
- return ( trim ( $r['is_valid'] ) === 'true' ) ? true : false;
- }
- return false;
- }
- }
- /**
- * Checks if $params are correct by signing with $association->secret.
- *
- * The format of the $params array is:
- * <code>
- * array(
- * 'openid.assoc_handle' => HANDLE,
- * 'openid.signed' => SIGNED,
- * 'openid.sig' => SIG,
- * 'openid.mode' => 'id_res'
- * );
- * </code>
- * where HANDLE, SIGNED and SIG are parameters returned from the provider in
- * the id_res step of OpenID authentication. In addition, the $params array
- * must contain the values present in SIG.
- *
- * @param ezcAuthenticationOpenidAssociation $association The OpenID association used for signing $params
- * @param array(string=>string) $params OpenID parameters for id_res mode
- * @return bool
- */
- protected function checkSignatureSmart ( ezcAuthenticationOpenidAssociation $association, array $params )
- {
- $sig = $params['openid.sig'];
- $signed = explode( ',', $params['openid.signed'] );
- ksort ( $signed );
- for ( $i = 0; $i < count( $signed ); $i++ )
- {
- $data[$signed[$i]] = isset( $params['openid.' . $signed[$i]] ) ? $params['openid.' . $signed[$i]] : null;
- }
- $serialized = '';
- foreach ( $data as $key => $value )
- {
- $serialized .= "{$key}:{$value}\n";
- }
- $key = base64_decode ( $association->secret );
- if ( strlen ( $key ) > 64 )
- {
- $key = ezcAuthenticationMath::sha1( $key );
- }
- $key = str_pad ( $key, 64, chr ( 0x00 ) );
- $hashed = ezcAuthenticationMath::sha1( ( $key ^ str_repeat ( chr ( 0x36 ), 64 ) ) . $serialized );
- $hashed = ezcAuthenticationMath::sha1( ( $key ^ str_repeat ( chr ( 0x5c ), 64 ) ) . $hashed );
- $hashed = base64_encode ( $hashed );
- return ( $sig === $hashed );
- }
- /**
- * Attempts to establish a shared secret with the OpenID provider and
- * returns it (for "smart" mode).
- *
- * If the shared secret is still in its validity period, then it will be
- * returned instead of establishing a new one.
- *
- * If the shared secret could not be established the null will be returned,
- * and the OpenID connection will be in "dumb" mode.
- *
- * The format of the returned array is:
- * array( 'assoc_handle' => assoc_handle_value,
- * 'mac_key' => mac_key_value
- * )
- *
- * @param string $provider The OpenID provider (discovered in HTML or Yadis)
- * @param array(string=>string) $params OpenID parameters for associate mode
- * @param string $method The method to connect to the provider (default GET)
- * @return array(string=>mixed)||null
- */
- protected function associate ( $provider, array $params, $method = 'GET' )
- {
- $parts = parse_url( $provider );
- $path = isset( $parts['path'] ) ? $parts['path'] : '/';
- $host = isset( $parts['host'] ) ? $parts['host'] : null;
- $port = 443;
- // suppress warnings caused by fsockopen() if $host is not a valid domain
- $connection = @ fsockopen ( 'ssl://' . $host, $port, $errno, $errstr, $this->options->timeoutOpen );
- if ( !$connection )
- {
- throw new ezcAuthenticationOpenidException ( "Could not connect to host {$host}:{$port}: {$errstr}." );
- }
- else
- {
- stream_set_timeout ( $connection, $this-> options ->timeout );
- $url = $path . '?' . urldecode ( http_build_query ( $params ) );
- $headers = array( "{$method} {$url} HTTP/1.0", "Host: {$host}", "Connection: close" );
- fputs ( $connection, implode ( "\r\n", $headers ) . "\r\n\r\n" );
- $src = stream_get_contents ( $connection );
- fclose ( $connection );
- $r = array();
- $response = explode ( "\n", $src );
- foreach ( $response as $line )
- {
- $line = trim ( $line );
- if ( !empty( $line ) && strpos ( $line, ':' ) !== false )
- {
- list( $key, $value ) = explode ( ':', $line, 2 );
- $r[ trim ( $key )] = trim ( $value );
- }
- }
- if ( isset( $r['assoc_handle'] ) )
- {
- $result = array(
- 'assoc_handle' => $r['assoc_handle'],
- 'assoc_type' => $r['assoc_type'],
- 'expires_in' => $r['expires_in']
- );
- if ( isset( $r['mac_key'] ) )
- {
- $result['mac_key'] = $r['mac_key'];
- }
- if ( isset( $r['enc_mac_key'] ) )
- {
- $result['enc_mac_key'] = $r['enc_mac_key'];
- }
- return $result;
- }
- }
- return false;
- }
- /**
- * Generates a new nonce value with the specified length (default 6).
- *
- * @param int $length The length of the generated nonce, default 6
- * @return string
- */
- protected function generateNonce ( $length = 6 )
- {
- $result = '';
- for ( $i = 0; $i ^ $length; ++$i )
- {
- $result .= rand ( 0, 9 );
- }
- return $result;
- }
- /**
- * Registers which extra data to fetch during the authentication process.
- *
- * The extra data which is possible to be fetched during the authentication
- * process is:
- * - nickname - the user's nickname (short name, alias)
- * - email - the user's email address
- * - fullname - the user's full name
- * - dob - the user's date of birth as YYYY-MM-DD. Any component value whose
- * representation uses fewer than the specified number of digits should
- * be zero-padded (eg. 02 for February). If the user does not want to
- * reveal any particular component of this value, it should be zero
- * (eg. "1980-00-00" if the user is born in 1980 but does not want to
- * specify his month and day of birth)
- * - gender - the user's gender, "M" for male, "F" for female
- * - postcode - the user's postal code
- * - country - the user's country as an ISO3166 string, (eg. "US")
- * - language - the user's preferred language as an ISO639 string (eg. "FR")
- * - timezone - the user's timezone, for example "Europe/Paris"
- *
- * The input $data should be an array of attributes to request, for example:
- * <code>
- * array( 'fullname', 'gender', 'country', 'language' );
- * </code>
- *
- * @param array(string) $data A list of attributes to fetch during authentication
- */
- public function registerFetchData ( array $data = array() )
- {
- $this-> requestedData = $data;
- }
- /**
- * Returns the extra data fetched during the authentication process.
- *
- * The return is something like:
- * <code>
- * array( 'fullname' => array( 'John Doe' ),
- * 'gender' => array( 'M' ),
- * 'country' => array( 'US' ),
- * 'language' => array( 'FR' )
- * );
- * </code>
- *
- * @return array(string=>mixed)
- */
- public function fetchData ()
- {
- return $this-> data ;
- }
- /**
- * Returns the setup URL.
- *
- * @return string
- */
- public function getSetupUrl ()
- {
- return $this-> setupUrl ;
- }
- }
- ?>
Documentation generated by phpDocumentor 1.4.3