|
1 <?php |
|
2 |
|
3 /** |
|
4 * @file |
|
5 * OpenID utility functions. |
|
6 */ |
|
7 |
|
8 /** |
|
9 * Diffie-Hellman Key Exchange Default Value. |
|
10 * |
|
11 * This is used to establish an association between the Relying Party and the |
|
12 * OpenID Provider. |
|
13 * |
|
14 * See RFC 2631: http://www.ietf.org/rfc/rfc2631.txt |
|
15 */ |
|
16 define('OPENID_DH_DEFAULT_MOD', '155172898181473697471232257763715539915724801' . |
|
17 '966915404479707795314057629378541917580651227423698188993727816152646631' . |
|
18 '438561595825688188889951272158842675419950341258706556549803580104870537' . |
|
19 '681476726513255747040765857479291291572334510643245094715007229621094194' . |
|
20 '349783925984760375594985848253359305585439638443'); |
|
21 |
|
22 /** |
|
23 * Diffie-Hellman generator; used for Diffie-Hellman key exchange computations. |
|
24 */ |
|
25 define('OPENID_DH_DEFAULT_GEN', '2'); |
|
26 |
|
27 /** |
|
28 * SHA-1 hash block size; used for Diffie-Hellman key exchange computations. |
|
29 */ |
|
30 define('OPENID_SHA1_BLOCKSIZE', 64); |
|
31 |
|
32 /** |
|
33 * Random number generator; used for Diffie-Hellman key exchange computations. |
|
34 */ |
|
35 define('OPENID_RAND_SOURCE', '/dev/urandom'); |
|
36 |
|
37 /** |
|
38 * OpenID Authentication 2.0 namespace URL. |
|
39 */ |
|
40 define('OPENID_NS_2_0', 'http://specs.openid.net/auth/2.0'); |
|
41 |
|
42 /** |
|
43 * OpenID Authentication 1.1 namespace URL; used for backwards-compatibility. |
|
44 */ |
|
45 define('OPENID_NS_1_1', 'http://openid.net/signon/1.1'); |
|
46 |
|
47 /** |
|
48 * OpenID Authentication 1.0 namespace URL; used for backwards-compatibility. |
|
49 */ |
|
50 define('OPENID_NS_1_0', 'http://openid.net/signon/1.0'); |
|
51 |
|
52 /** |
|
53 * OpenID namespace used in Yadis documents. |
|
54 */ |
|
55 define('OPENID_NS_OPENID', 'http://openid.net/xmlns/1.0'); |
|
56 |
|
57 /** |
|
58 * OpenID Simple Registration extension. |
|
59 */ |
|
60 define('OPENID_NS_SREG', 'http://openid.net/extensions/sreg/1.1'); |
|
61 |
|
62 /** |
|
63 * OpenID Attribute Exchange extension. |
|
64 */ |
|
65 define('OPENID_NS_AX', 'http://openid.net/srv/ax/1.0'); |
|
66 |
|
67 /** |
|
68 * Extensible Resource Descriptor documents. |
|
69 */ |
|
70 define('OPENID_NS_XRD', 'xri://$xrd*($v*2.0)'); |
|
71 |
|
72 /** |
|
73 * Performs an HTTP 302 redirect (for the 1.x protocol). |
|
74 */ |
|
75 function openid_redirect_http($url, $message) { |
|
76 $query = array(); |
|
77 foreach ($message as $key => $val) { |
|
78 $query[] = $key . '=' . urlencode($val); |
|
79 } |
|
80 |
|
81 $sep = (strpos($url, '?') === FALSE) ? '?' : '&'; |
|
82 header('Location: ' . $url . $sep . implode('&', $query), TRUE, 302); |
|
83 |
|
84 drupal_exit(); |
|
85 } |
|
86 |
|
87 /** |
|
88 * Creates a js auto-submit redirect for (for the 2.x protocol) |
|
89 */ |
|
90 function openid_redirect($url, $message) { |
|
91 global $language; |
|
92 |
|
93 $output = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n"; |
|
94 $output .= '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="' . $language->language . '" lang="' . $language->language . '">' . "\n"; |
|
95 $output .= "<head>\n"; |
|
96 $output .= "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n"; |
|
97 $output .= "<title>" . t('OpenID redirect') . "</title>\n"; |
|
98 $output .= "</head>\n"; |
|
99 $output .= "<body>\n"; |
|
100 $elements = drupal_get_form('openid_redirect_form', $url, $message); |
|
101 $output .= drupal_render($elements); |
|
102 $output .= '<script type="text/javascript">document.getElementById("openid-redirect-form").submit();</script>' . "\n"; |
|
103 $output .= "</body>\n"; |
|
104 $output .= "</html>\n"; |
|
105 print $output; |
|
106 |
|
107 drupal_exit(); |
|
108 } |
|
109 |
|
110 function openid_redirect_form($form, &$form_state, $url, $message) { |
|
111 $form['#action'] = $url; |
|
112 $form['#method'] = "post"; |
|
113 foreach ($message as $key => $value) { |
|
114 $form[$key] = array( |
|
115 '#type' => 'hidden', |
|
116 '#name' => $key, |
|
117 '#value' => $value, |
|
118 ); |
|
119 } |
|
120 $form['actions'] = array('#type' => 'actions'); |
|
121 $form['actions']['submit'] = array( |
|
122 '#type' => 'submit', |
|
123 '#prefix' => '<noscript><div>', |
|
124 '#suffix' => '</div></noscript>', |
|
125 '#value' => t('Send'), |
|
126 ); |
|
127 |
|
128 return $form; |
|
129 } |
|
130 |
|
131 /** |
|
132 * Parse an XRDS document. |
|
133 * |
|
134 * @param $raw_xml |
|
135 * A string containing the XRDS document. |
|
136 * @return |
|
137 * An array of service entries. |
|
138 */ |
|
139 function _openid_xrds_parse($raw_xml) { |
|
140 $services = array(); |
|
141 |
|
142 // For PHP version >= 5.2.11, we can use this function to protect against |
|
143 // malicious doctype declarations and other unexpected entity loading. |
|
144 // However, we will not rely on it, and reject any XML with a DOCTYPE. |
|
145 $disable_entity_loader = function_exists('libxml_disable_entity_loader'); |
|
146 if ($disable_entity_loader) { |
|
147 $load_entities = libxml_disable_entity_loader(TRUE); |
|
148 } |
|
149 |
|
150 // Load the XML into a DOM document. |
|
151 $dom = new DOMDocument(); |
|
152 @$dom->loadXML($raw_xml); |
|
153 |
|
154 // Since DOCTYPE declarations from an untrusted source could be malicious, we |
|
155 // stop parsing here and treat the XML as invalid since XRDS documents do not |
|
156 // require, and are not expected to have, a DOCTYPE. |
|
157 if (isset($dom->doctype)) { |
|
158 return array(); |
|
159 } |
|
160 |
|
161 // Also stop parsing if there is an unreasonably large number of tags. |
|
162 if ($dom->getElementsByTagName('*')->length > variable_get('openid_xrds_maximum_tag_count', 30000)) { |
|
163 return array(); |
|
164 } |
|
165 |
|
166 // Parse the DOM document for the information we need. |
|
167 if ($xml = simplexml_import_dom($dom)) { |
|
168 foreach ($xml->children(OPENID_NS_XRD)->XRD as $xrd) { |
|
169 foreach ($xrd->children(OPENID_NS_XRD)->Service as $service_element) { |
|
170 $service = array( |
|
171 'priority' => $service_element->attributes()->priority ? (int)$service_element->attributes()->priority : PHP_INT_MAX, |
|
172 'types' => array(), |
|
173 'uri' => (string)$service_element->children(OPENID_NS_XRD)->URI, |
|
174 'service' => $service_element, |
|
175 'xrd' => $xrd, |
|
176 ); |
|
177 foreach ($service_element->Type as $type) { |
|
178 $service['types'][] = (string)$type; |
|
179 } |
|
180 if ($service_element->children(OPENID_NS_XRD)->LocalID) { |
|
181 $service['identity'] = (string)$service_element->children(OPENID_NS_XRD)->LocalID; |
|
182 } |
|
183 elseif ($service_element->children(OPENID_NS_OPENID)->Delegate) { |
|
184 $service['identity'] = (string)$service_element->children(OPENID_NS_OPENID)->Delegate; |
|
185 } |
|
186 else { |
|
187 $service['identity'] = FALSE; |
|
188 } |
|
189 $services[] = $service; |
|
190 } |
|
191 } |
|
192 } |
|
193 |
|
194 // Return the LIBXML options to the previous state before returning. |
|
195 if ($disable_entity_loader) { |
|
196 libxml_disable_entity_loader($load_entities); |
|
197 } |
|
198 |
|
199 return $services; |
|
200 } |
|
201 |
|
202 /** |
|
203 * Select a service element. |
|
204 * |
|
205 * The procedure is described in OpenID Authentication 2.0, section 7.3.2. |
|
206 * |
|
207 * A new entry is added to the returned array with the key 'version' and the |
|
208 * value 1 or 2 specifying the protocol version used by the service. |
|
209 * |
|
210 * @param $services |
|
211 * An array of service arrays as returned by openid_discovery(). |
|
212 * @return |
|
213 * The selected service array, or NULL if no valid services were found. |
|
214 */ |
|
215 function _openid_select_service(array $services) { |
|
216 // Extensible Resource Identifier (XRI) Resolution Version 2.0, section 4.3.3: |
|
217 // Find the service with the highest priority (lowest integer value). If there |
|
218 // is a tie, select a random one, not just the first in the XML document. |
|
219 shuffle($services); |
|
220 $selected_service = NULL; |
|
221 $selected_type_priority = FALSE; |
|
222 |
|
223 // Search for an OP Identifier Element. |
|
224 foreach ($services as $service) { |
|
225 if (!empty($service['uri'])) { |
|
226 $type_priority = FALSE; |
|
227 if (in_array('http://specs.openid.net/auth/2.0/server', $service['types'])) { |
|
228 $service['version'] = 2; |
|
229 $type_priority = 1; |
|
230 } |
|
231 elseif (in_array('http://specs.openid.net/auth/2.0/signon', $service['types'])) { |
|
232 $service['version'] = 2; |
|
233 $type_priority = 2; |
|
234 } |
|
235 elseif (in_array(OPENID_NS_1_0, $service['types']) || in_array(OPENID_NS_1_1, $service['types'])) { |
|
236 $service['version'] = 1; |
|
237 $type_priority = 3; |
|
238 } |
|
239 |
|
240 if ($type_priority |
|
241 && (!$selected_service |
|
242 || $type_priority < $selected_type_priority |
|
243 || ($type_priority == $selected_type_priority && $service['priority'] < $selected_service['priority']))) { |
|
244 $selected_service = $service; |
|
245 $selected_type_priority = $type_priority; |
|
246 } |
|
247 } |
|
248 } |
|
249 |
|
250 if ($selected_service) { |
|
251 // Unset SimpleXMLElement instances that cannot be saved in $_SESSION. |
|
252 unset($selected_service['xrd']); |
|
253 unset($selected_service['service']); |
|
254 } |
|
255 |
|
256 return $selected_service; |
|
257 } |
|
258 |
|
259 /** |
|
260 * Determine if the given identifier is an XRI ID. |
|
261 */ |
|
262 function _openid_is_xri($identifier) { |
|
263 // Strip the xri:// scheme from the identifier if present. |
|
264 if (stripos($identifier, 'xri://') === 0) { |
|
265 $identifier = substr($identifier, 6); |
|
266 } |
|
267 |
|
268 // Test whether the identifier starts with an XRI global context symbol or (. |
|
269 $firstchar = substr($identifier, 0, 1); |
|
270 if (strpos("=@+$!(", $firstchar) !== FALSE) { |
|
271 return TRUE; |
|
272 } |
|
273 |
|
274 return FALSE; |
|
275 } |
|
276 |
|
277 /** |
|
278 * Normalize the given identifier. |
|
279 * |
|
280 * The procedure is described in OpenID Authentication 2.0, section 7.2. |
|
281 */ |
|
282 function openid_normalize($identifier) { |
|
283 $methods = module_invoke_all('openid_normalization_method_info'); |
|
284 drupal_alter('openid_normalization_method_info', $methods); |
|
285 |
|
286 // Execute each method in turn, stopping after the first method accepted |
|
287 // the identifier. |
|
288 foreach ($methods as $method) { |
|
289 $result = $method($identifier); |
|
290 if ($result !== NULL) { |
|
291 $identifier = $result; |
|
292 break; |
|
293 } |
|
294 } |
|
295 |
|
296 return $identifier; |
|
297 } |
|
298 |
|
299 /** |
|
300 * OpenID normalization method: normalize XRI identifiers. |
|
301 */ |
|
302 function _openid_xri_normalize($identifier) { |
|
303 if (_openid_is_xri($identifier)) { |
|
304 if (stristr($identifier, 'xri://') !== FALSE) { |
|
305 $identifier = substr($identifier, 6); |
|
306 } |
|
307 return $identifier; |
|
308 } |
|
309 } |
|
310 |
|
311 /** |
|
312 * OpenID normalization method: normalize URL identifiers. |
|
313 */ |
|
314 function _openid_url_normalize($url) { |
|
315 $normalized_url = $url; |
|
316 |
|
317 if (stristr($url, '://') === FALSE) { |
|
318 $normalized_url = 'http://' . $url; |
|
319 } |
|
320 |
|
321 // Strip the fragment and fragment delimiter if present. |
|
322 $normalized_url = strtok($normalized_url, '#'); |
|
323 |
|
324 if (substr_count($normalized_url, '/') < 3) { |
|
325 $normalized_url .= '/'; |
|
326 } |
|
327 |
|
328 return $normalized_url; |
|
329 } |
|
330 |
|
331 /** |
|
332 * Create a serialized message packet as per spec: $key:$value\n . |
|
333 */ |
|
334 function _openid_create_message($data) { |
|
335 $serialized = ''; |
|
336 |
|
337 foreach ($data as $key => $value) { |
|
338 if ((strpos($key, ':') !== FALSE) || (strpos($key, "\n") !== FALSE) || (strpos($value, "\n") !== FALSE)) { |
|
339 return NULL; |
|
340 } |
|
341 $serialized .= "$key:$value\n"; |
|
342 } |
|
343 return $serialized; |
|
344 } |
|
345 |
|
346 /** |
|
347 * Encode a message from _openid_create_message for HTTP Post |
|
348 */ |
|
349 function _openid_encode_message($message) { |
|
350 $encoded_message = ''; |
|
351 |
|
352 $items = explode("\n", $message); |
|
353 foreach ($items as $item) { |
|
354 $parts = explode(':', $item, 2); |
|
355 |
|
356 if (count($parts) == 2) { |
|
357 if ($encoded_message != '') { |
|
358 $encoded_message .= '&'; |
|
359 } |
|
360 $encoded_message .= rawurlencode(trim($parts[0])) . '=' . rawurlencode(trim($parts[1])); |
|
361 } |
|
362 } |
|
363 |
|
364 return $encoded_message; |
|
365 } |
|
366 |
|
367 /** |
|
368 * Convert a direct communication message |
|
369 * into an associative array. |
|
370 */ |
|
371 function _openid_parse_message($message) { |
|
372 $parsed_message = array(); |
|
373 |
|
374 $items = explode("\n", $message); |
|
375 foreach ($items as $item) { |
|
376 $parts = explode(':', $item, 2); |
|
377 |
|
378 if (count($parts) == 2) { |
|
379 $parsed_message[$parts[0]] = $parts[1]; |
|
380 } |
|
381 } |
|
382 |
|
383 return $parsed_message; |
|
384 } |
|
385 |
|
386 /** |
|
387 * Return a nonce value - formatted per OpenID spec. |
|
388 * |
|
389 * NOTE: This nonce is not cryptographically secure and only suitable for use |
|
390 * by the test framework. |
|
391 */ |
|
392 function _openid_nonce() { |
|
393 // YYYY-MM-DDThh:mm:ssZ, plus some optional extra unique characters. |
|
394 return gmdate('Y-m-d\TH:i:s\Z') . |
|
395 chr(mt_rand(0, 25) + 65) . |
|
396 chr(mt_rand(0, 25) + 65) . |
|
397 chr(mt_rand(0, 25) + 65) . |
|
398 chr(mt_rand(0, 25) + 65); |
|
399 } |
|
400 |
|
401 /** |
|
402 * Pull the href attribute out of an html link element. |
|
403 */ |
|
404 function _openid_link_href($rel, $html) { |
|
405 $rel = preg_quote($rel); |
|
406 preg_match('|<link\s+rel=["\'](.*)' . $rel . '(.*)["\'](.*)/?>|iUs', $html, $matches); |
|
407 if (isset($matches[3])) { |
|
408 preg_match('|href=["\']([^"]+)["\']|iU', $matches[3], $href); |
|
409 return trim($href[1]); |
|
410 } |
|
411 return FALSE; |
|
412 } |
|
413 |
|
414 /** |
|
415 * Pull the http-equiv attribute out of an html meta element |
|
416 */ |
|
417 function _openid_meta_httpequiv($equiv, $html) { |
|
418 preg_match('|<meta\s+http-equiv=["\']' . $equiv . '["\'](.*)/?>|iUs', $html, $matches); |
|
419 if (isset($matches[1])) { |
|
420 preg_match('|content=["\']([^"]+)["\']|iUs', $matches[1], $content); |
|
421 if (isset($content[1])) { |
|
422 return $content[1]; |
|
423 } |
|
424 } |
|
425 return FALSE; |
|
426 } |
|
427 |
|
428 /** |
|
429 * Sign certain keys in a message |
|
430 * @param $association - object loaded from openid_association or openid_server_association table |
|
431 * - important fields are ->assoc_type and ->mac_key |
|
432 * @param $message_array - array of entire message about to be sent |
|
433 * @param $keys_to_sign - keys in the message to include in signature (without |
|
434 * 'openid.' appended) |
|
435 */ |
|
436 function _openid_signature($association, $message_array, $keys_to_sign) { |
|
437 $signature = ''; |
|
438 $sign_data = array(); |
|
439 |
|
440 foreach ($keys_to_sign as $key) { |
|
441 if (isset($message_array['openid.' . $key])) { |
|
442 $sign_data[$key] = $message_array['openid.' . $key]; |
|
443 } |
|
444 } |
|
445 |
|
446 $message = _openid_create_message($sign_data); |
|
447 $secret = base64_decode($association->mac_key); |
|
448 $signature = _openid_hmac($secret, $message); |
|
449 |
|
450 return base64_encode($signature); |
|
451 } |
|
452 |
|
453 function _openid_hmac($key, $text) { |
|
454 if (strlen($key) > OPENID_SHA1_BLOCKSIZE) { |
|
455 $key = sha1($key, TRUE); |
|
456 } |
|
457 |
|
458 $key = str_pad($key, OPENID_SHA1_BLOCKSIZE, chr(0x00)); |
|
459 $ipad = str_repeat(chr(0x36), OPENID_SHA1_BLOCKSIZE); |
|
460 $opad = str_repeat(chr(0x5c), OPENID_SHA1_BLOCKSIZE); |
|
461 $hash1 = sha1(($key ^ $ipad) . $text, TRUE); |
|
462 $hmac = sha1(($key ^ $opad) . $hash1, TRUE); |
|
463 |
|
464 return $hmac; |
|
465 } |
|
466 |
|
467 function _openid_dh_base64_to_long($str) { |
|
468 $b64 = base64_decode($str); |
|
469 |
|
470 return _openid_dh_binary_to_long($b64); |
|
471 } |
|
472 |
|
473 function _openid_dh_long_to_base64($str) { |
|
474 return base64_encode(_openid_dh_long_to_binary($str)); |
|
475 } |
|
476 |
|
477 function _openid_dh_binary_to_long($str) { |
|
478 $bytes = array_merge(unpack('C*', $str)); |
|
479 |
|
480 $n = 0; |
|
481 foreach ($bytes as $byte) { |
|
482 $n = _openid_math_mul($n, pow(2, 8)); |
|
483 $n = _openid_math_add($n, $byte); |
|
484 } |
|
485 |
|
486 return $n; |
|
487 } |
|
488 |
|
489 function _openid_dh_long_to_binary($long) { |
|
490 $cmp = _openid_math_cmp($long, 0); |
|
491 if ($cmp < 0) { |
|
492 return FALSE; |
|
493 } |
|
494 |
|
495 if ($cmp == 0) { |
|
496 return "\x00"; |
|
497 } |
|
498 |
|
499 $bytes = array(); |
|
500 |
|
501 while (_openid_math_cmp($long, 0) > 0) { |
|
502 array_unshift($bytes, _openid_math_mod($long, 256)); |
|
503 $long = _openid_math_div($long, pow(2, 8)); |
|
504 } |
|
505 |
|
506 if ($bytes && ($bytes[0] > 127)) { |
|
507 array_unshift($bytes, 0); |
|
508 } |
|
509 |
|
510 $string = ''; |
|
511 foreach ($bytes as $byte) { |
|
512 $string .= pack('C', $byte); |
|
513 } |
|
514 |
|
515 return $string; |
|
516 } |
|
517 |
|
518 function _openid_dh_xorsecret($shared, $secret) { |
|
519 $dh_shared_str = _openid_dh_long_to_binary($shared); |
|
520 $sha1_dh_shared = sha1($dh_shared_str, TRUE); |
|
521 $xsecret = ""; |
|
522 for ($i = 0; $i < strlen($secret); $i++) { |
|
523 $xsecret .= chr(ord($secret[$i]) ^ ord($sha1_dh_shared[$i])); |
|
524 } |
|
525 |
|
526 return $xsecret; |
|
527 } |
|
528 |
|
529 function _openid_dh_rand($stop) { |
|
530 $duplicate_cache = &drupal_static(__FUNCTION__, array()); |
|
531 |
|
532 // Used as the key for the duplicate cache |
|
533 $rbytes = _openid_dh_long_to_binary($stop); |
|
534 |
|
535 if (isset($duplicate_cache[$rbytes])) { |
|
536 list($duplicate, $nbytes) = $duplicate_cache[$rbytes]; |
|
537 } |
|
538 else { |
|
539 if ($rbytes[0] == "\x00") { |
|
540 $nbytes = strlen($rbytes) - 1; |
|
541 } |
|
542 else { |
|
543 $nbytes = strlen($rbytes); |
|
544 } |
|
545 |
|
546 $mxrand = _openid_math_pow(256, $nbytes); |
|
547 |
|
548 // If we get a number less than this, then it is in the |
|
549 // duplicated range. |
|
550 $duplicate = _openid_math_mod($mxrand, $stop); |
|
551 |
|
552 if (count($duplicate_cache) > 10) { |
|
553 $duplicate_cache = array(); |
|
554 } |
|
555 |
|
556 $duplicate_cache[$rbytes] = array($duplicate, $nbytes); |
|
557 } |
|
558 |
|
559 do { |
|
560 $bytes = "\x00" . drupal_random_bytes($nbytes); |
|
561 $n = _openid_dh_binary_to_long($bytes); |
|
562 // Keep looping if this value is in the low duplicated range. |
|
563 } while (_openid_math_cmp($n, $duplicate) < 0); |
|
564 |
|
565 return _openid_math_mod($n, $stop); |
|
566 } |
|
567 |
|
568 function _openid_get_bytes($num_bytes) { |
|
569 return drupal_random_bytes($num_bytes); |
|
570 } |
|
571 |
|
572 function _openid_response($str = NULL) { |
|
573 $data = array(); |
|
574 |
|
575 if (isset($_SERVER['REQUEST_METHOD'])) { |
|
576 $data = _openid_get_params($_SERVER['QUERY_STRING']); |
|
577 |
|
578 if ($_SERVER['REQUEST_METHOD'] == 'POST') { |
|
579 $str = file_get_contents('php://input'); |
|
580 |
|
581 $post = array(); |
|
582 if ($str !== FALSE) { |
|
583 $post = _openid_get_params($str); |
|
584 } |
|
585 |
|
586 $data = array_merge($data, $post); |
|
587 } |
|
588 } |
|
589 |
|
590 return $data; |
|
591 } |
|
592 |
|
593 function _openid_get_params($str) { |
|
594 $chunks = explode("&", $str); |
|
595 |
|
596 $data = array(); |
|
597 foreach ($chunks as $chunk) { |
|
598 $parts = explode("=", $chunk, 2); |
|
599 |
|
600 if (count($parts) == 2) { |
|
601 list($k, $v) = $parts; |
|
602 $data[$k] = urldecode($v); |
|
603 } |
|
604 } |
|
605 return $data; |
|
606 } |
|
607 |
|
608 /** |
|
609 * Extract all the parameters belonging to an extension in a response message. |
|
610 * |
|
611 * OpenID 2.0 defines a simple extension mechanism, based on a namespace prefix. |
|
612 * |
|
613 * Each request or response can define a prefix using: |
|
614 * @code |
|
615 * openid.ns.[prefix] = [extension_namespace] |
|
616 * openid.[prefix].[key1] = [value1] |
|
617 * openid.[prefix].[key2] = [value2] |
|
618 * ... |
|
619 * @endcode |
|
620 * |
|
621 * This function extracts all the keys belonging to an extension namespace in a |
|
622 * response, optionally using a fallback prefix if none is provided in the response. |
|
623 * |
|
624 * Note that you cannot assume that a given extension namespace will use the same |
|
625 * prefix on the response and the request: each party may use a different prefix |
|
626 * to refer to the same namespace. |
|
627 * |
|
628 * @param $response |
|
629 * The response array. |
|
630 * @param $extension_namespace |
|
631 * The namespace of the extension. |
|
632 * @param $fallback_prefix |
|
633 * An optional prefix that will be used in case no prefix is found for the |
|
634 * target extension namespace. |
|
635 * @param $only_signed |
|
636 * Return only keys that are included in the message signature in openid.sig. |
|
637 * Unsigned fields may have been modified or added by other parties than the |
|
638 * OpenID Provider. |
|
639 * |
|
640 * @return |
|
641 * An associative array containing all the parameters in the response message |
|
642 * that belong to the extension. The keys are stripped from their namespace |
|
643 * prefix. |
|
644 * |
|
645 * @see http://openid.net/specs/openid-authentication-2_0.html#extensions |
|
646 */ |
|
647 function openid_extract_namespace($response, $extension_namespace, $fallback_prefix = NULL, $only_signed = FALSE) { |
|
648 $signed_keys = explode(',', $response['openid.signed']); |
|
649 |
|
650 // Find the namespace prefix. |
|
651 $prefix = $fallback_prefix; |
|
652 foreach ($response as $key => $value) { |
|
653 if ($value == $extension_namespace && preg_match('/^openid\.ns\.([^.]+)$/', $key, $matches)) { |
|
654 $prefix = $matches[1]; |
|
655 if ($only_signed && !in_array('ns.' . $matches[1], $signed_keys)) { |
|
656 // The namespace was defined but was not signed as required. In this |
|
657 // case we do not fall back to $fallback_prefix. |
|
658 $prefix = NULL; |
|
659 } |
|
660 break; |
|
661 } |
|
662 } |
|
663 |
|
664 // Now extract the namespace keys from the response. |
|
665 $output = array(); |
|
666 if (!isset($prefix)) { |
|
667 return $output; |
|
668 } |
|
669 foreach ($response as $key => $value) { |
|
670 if (preg_match('/^openid\.' . $prefix . '\.(.+)$/', $key, $matches)) { |
|
671 $local_key = $matches[1]; |
|
672 if (!$only_signed || in_array($prefix . '.' . $local_key, $signed_keys)) { |
|
673 $output[$local_key] = $value; |
|
674 } |
|
675 } |
|
676 } |
|
677 |
|
678 return $output; |
|
679 } |
|
680 |
|
681 /** |
|
682 * Extracts values from an OpenID AX Response. |
|
683 * |
|
684 * The values can be returned in two forms: |
|
685 * - only openid.ax.value.<alias> (for single-valued answers) |
|
686 * - both openid.ax.count.<alias> and openid.ax.value.<alias>.<count> (for both |
|
687 * single and multiple-valued answers) |
|
688 * |
|
689 * @param $values |
|
690 * An array as returned by openid_extract_namespace(..., OPENID_NS_AX). |
|
691 * @param $uris |
|
692 * An array of identifier URIs. |
|
693 * @return |
|
694 * An array of values. |
|
695 * @see http://openid.net/specs/openid-attribute-exchange-1_0.html#fetch_response |
|
696 */ |
|
697 function openid_extract_ax_values($values, $uris) { |
|
698 $output = array(); |
|
699 foreach ($values as $key => $value) { |
|
700 if (in_array($value, $uris) && preg_match('/^type\.([^.]+)$/', $key, $matches)) { |
|
701 $alias = $matches[1]; |
|
702 if (isset($values['count.' . $alias])) { |
|
703 for ($i = 1; $i <= $values['count.' . $alias]; $i++) { |
|
704 $output[] = $values['value.' . $alias . '.' . $i]; |
|
705 } |
|
706 } |
|
707 elseif (isset($values['value.' . $alias])) { |
|
708 $output[] = $values['value.' . $alias]; |
|
709 } |
|
710 break; |
|
711 } |
|
712 } |
|
713 return $output; |
|
714 } |
|
715 |
|
716 /** |
|
717 * Determine the available math library GMP vs. BCMath, favouring GMP for performance. |
|
718 */ |
|
719 function _openid_get_math_library() { |
|
720 // Not drupal_static(), because a function is not going to disappear and |
|
721 // change the output of this under any circumstances. |
|
722 static $library; |
|
723 |
|
724 if (empty($library)) { |
|
725 if (function_exists('gmp_add')) { |
|
726 $library = 'gmp'; |
|
727 } |
|
728 elseif (function_exists('bcadd')) { |
|
729 $library = 'bcmath'; |
|
730 } |
|
731 } |
|
732 |
|
733 return $library; |
|
734 } |
|
735 |
|
736 /** |
|
737 * Calls the add function from the available math library for OpenID. |
|
738 */ |
|
739 function _openid_math_add($x, $y) { |
|
740 $library = _openid_get_math_library(); |
|
741 switch ($library) { |
|
742 case 'gmp': |
|
743 return gmp_strval(gmp_add($x, $y)); |
|
744 case 'bcmath': |
|
745 return bcadd($x, $y); |
|
746 } |
|
747 } |
|
748 |
|
749 /** |
|
750 * Calls the mul function from the available math library for OpenID. |
|
751 */ |
|
752 function _openid_math_mul($x, $y) { |
|
753 $library = _openid_get_math_library(); |
|
754 switch ($library) { |
|
755 case 'gmp': |
|
756 return gmp_mul($x, $y); |
|
757 case 'bcmath': |
|
758 return bcmul($x, $y); |
|
759 } |
|
760 } |
|
761 |
|
762 /** |
|
763 * Calls the div function from the available math library for OpenID. |
|
764 */ |
|
765 function _openid_math_div($x, $y) { |
|
766 $library = _openid_get_math_library(); |
|
767 switch ($library) { |
|
768 case 'gmp': |
|
769 return gmp_div($x, $y); |
|
770 case 'bcmath': |
|
771 return bcdiv($x, $y); |
|
772 } |
|
773 } |
|
774 |
|
775 /** |
|
776 * Calls the cmp function from the available math library for OpenID. |
|
777 */ |
|
778 function _openid_math_cmp($x, $y) { |
|
779 $library = _openid_get_math_library(); |
|
780 switch ($library) { |
|
781 case 'gmp': |
|
782 return gmp_cmp($x, $y); |
|
783 case 'bcmath': |
|
784 return bccomp($x, $y); |
|
785 } |
|
786 } |
|
787 |
|
788 /** |
|
789 * Calls the mod function from the available math library for OpenID. |
|
790 */ |
|
791 function _openid_math_mod($x, $y) { |
|
792 $library = _openid_get_math_library(); |
|
793 switch ($library) { |
|
794 case 'gmp': |
|
795 return gmp_mod($x, $y); |
|
796 case 'bcmath': |
|
797 return bcmod($x, $y); |
|
798 } |
|
799 } |
|
800 |
|
801 /** |
|
802 * Calls the pow function from the available math library for OpenID. |
|
803 */ |
|
804 function _openid_math_pow($x, $y) { |
|
805 $library = _openid_get_math_library(); |
|
806 switch ($library) { |
|
807 case 'gmp': |
|
808 return gmp_pow($x, $y); |
|
809 case 'bcmath': |
|
810 return bcpow($x, $y); |
|
811 } |
|
812 } |
|
813 |
|
814 /** |
|
815 * Calls the mul function from the available math library for OpenID. |
|
816 */ |
|
817 function _openid_math_powmod($x, $y, $z) { |
|
818 $library = _openid_get_math_library(); |
|
819 switch ($library) { |
|
820 case 'gmp': |
|
821 return gmp_powm($x, $y, $z); |
|
822 case 'bcmath': |
|
823 return bcpowmod($x, $y, $z); |
|
824 } |
|
825 } |
|
826 |
|
827 /** |
|
828 * Provides transition for accounts with possibly invalid OpenID identifiers in authmap. |
|
829 * |
|
830 * This function provides a less safe but more unobtrusive procedure for users |
|
831 * who cannot login with their OpenID identifiers. OpenID identifiers in the |
|
832 * authmap could be incomplete due to invalid OpenID implementation in previous |
|
833 * versions of Drupal (e.g. fragment part of the identifier could be missing). |
|
834 * For more information see http://drupal.org/node/1120290. |
|
835 * |
|
836 * @param string $identity |
|
837 * The user's claimed OpenID identifier. |
|
838 * |
|
839 * @return |
|
840 * A fully-loaded user object if the user is found or FALSE if not found. |
|
841 */ |
|
842 function _openid_invalid_openid_transition($identity, $response) { |
|
843 $account = FALSE; |
|
844 $fallback_account = NULL; |
|
845 $fallback_identity = $identity; |
|
846 |
|
847 // Try to strip the fragment if it is present. |
|
848 if (strpos($fallback_identity, '#') !== FALSE) { |
|
849 $fallback_identity = preg_replace('/#.*/', '', $fallback_identity); |
|
850 $fallback_account = user_external_load($fallback_identity); |
|
851 } |
|
852 |
|
853 // Try to replace HTTPS with HTTP. OpenID providers often redirect |
|
854 // from http to https, but Drupal didn't follow the redirect. |
|
855 if (!$fallback_account && strpos($fallback_identity, 'https://') !== FALSE) { |
|
856 $fallback_identity = str_replace('https://', 'http://', $fallback_identity); |
|
857 $fallback_account = user_external_load($fallback_identity); |
|
858 } |
|
859 |
|
860 // Try to use original identifier. |
|
861 if (!$fallback_account && isset($_SESSION['openid']['user_login_values']['openid_identifier'])) { |
|
862 $fallback_identity = openid_normalize($_SESSION['openid']['user_login_values']['openid_identifier']); |
|
863 $fallback_account = user_external_load($fallback_identity); |
|
864 } |
|
865 |
|
866 if ($fallback_account) { |
|
867 // Try to extract e-mail address from Simple Registration (SREG) or |
|
868 // Attribute Exchanges (AX) keys. |
|
869 $email = ''; |
|
870 $sreg_values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg', TRUE); |
|
871 $ax_values = openid_extract_namespace($response, OPENID_NS_AX, 'ax', TRUE); |
|
872 if (!empty($sreg_values['email']) && valid_email_address($sreg_values['email'])) { |
|
873 $email = $sreg_values['email']; |
|
874 } |
|
875 elseif ($ax_mail_values = openid_extract_ax_values($ax_values, array('http://axschema.org/contact/email', 'http://schema.openid.net/contact/email'))) { |
|
876 $email = current($ax_mail_values); |
|
877 } |
|
878 |
|
879 // If this e-mail address is the same as the e-mail address found in user |
|
880 // account, login the user and update the claimed identifier. |
|
881 if ($email && ($email == $fallback_account->mail || $email == $fallback_account->init)) { |
|
882 $query = db_insert('authmap') |
|
883 ->fields(array( |
|
884 'authname' => $identity, |
|
885 'uid' => $fallback_account->uid, |
|
886 'module' => 'openid', |
|
887 )) |
|
888 ->execute(); |
|
889 drupal_set_message(t('New OpenID identifier %identity was added as a replacement for invalid identifier %invalid_identity. To finish the invalid OpenID transition process, please go to your <a href="@openid_url">OpenID identities page</a> and remove the old identifier %invalid_identity.', array('%invalid_identity' => $fallback_identity, '%identity' => $identity, '@openid_url' => 'user/' . $fallback_account->uid . '/openid'))); |
|
890 // Set the account to the found one. |
|
891 $account = $fallback_account; |
|
892 } |
|
893 else { |
|
894 drupal_set_message(t('There is already an existing account associated with the OpenID identifier that you have provided. However, due to a bug in the previous version of the authentication system, we can\'t be sure that this account belongs to you. If you are new on this site, please continue registering the new user account. If you already have a registered account on this site associated with the provided OpenID identifier, please try to <a href="@url_password">reset the password</a> or contact the site administrator.', array('@url_password' => 'user/password')), 'warning'); |
|
895 } |
|
896 } |
|
897 |
|
898 return $account; |
|
899 } |