1<?php 2/** 3 * This class provides a simple interface for OpenID 1.1/2.0 authentication. 4 * 5 * It requires PHP >= 5.1.2 with cURL or HTTP/HTTPS stream wrappers enabled. 6 * 7 * @version v1.3.1 (2016-03-04) 8 * @link https://code.google.com/p/lightopenid/ Project URL 9 * @link https://github.com/iignatov/LightOpenID GitHub Repo 10 * @author Mewp <mewp151 at gmail dot com> 11 * @copyright Copyright (c) 2013 Mewp 12 * @license http://opensource.org/licenses/mit-license.php MIT License 13 */ 14class LightOpenID 15{ 16 public $returnUrl 17 , $required = array() 18 , $optional = array() 19 , $verify_peer = null 20 , $capath = null 21 , $cainfo = null 22 , $cnmatch = null 23 , $data 24 , $oauth = array() 25 , $curl_time_out = 30 // in seconds 26 , $curl_connect_time_out = 30; // in seconds 27 private $identity, $claimed_id; 28 protected $server, $version, $trustRoot, $aliases, $identifier_select = false 29 , $ax = false, $sreg = false, $setup_url = null, $headers = array() 30 , $proxy = null, $user_agent = 'LightOpenID' 31 , $xrds_override_pattern = null, $xrds_override_replacement = null; 32 static protected $ax_to_sreg = array( 33 'namePerson/friendly' => 'nickname', 34 'contact/email' => 'email', 35 'namePerson' => 'fullname', 36 'birthDate' => 'dob', 37 'person/gender' => 'gender', 38 'contact/postalCode/home' => 'postcode', 39 'contact/country/home' => 'country', 40 'pref/language' => 'language', 41 'pref/timezone' => 'timezone', 42 ); 43 44 function __construct($host, $proxy = null) 45 { 46 $this->set_realm($host); 47 $this->set_proxy($proxy); 48 49 $uri = rtrim(preg_replace('#((?<=\?)|&)openid\.[^&]+#', '', $_SERVER['REQUEST_URI']), '?'); 50 $this->returnUrl = $this->trustRoot . $uri; 51 52 $this->data = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : $_GET; 53 54 if(!function_exists('curl_init') && !in_array('https', stream_get_wrappers())) { 55 throw new ErrorException('You must have either https wrappers or curl enabled.'); 56 } 57 } 58 59 function __isset($name) 60 { 61 return in_array($name, array('identity', 'trustRoot', 'realm', 'xrdsOverride', 'mode')); 62 } 63 64 function __set($name, $value) 65 { 66 switch ($name) { 67 case 'identity': 68 if (strlen($value = trim((String) $value))) { 69 if (preg_match('#^xri:/*#i', $value, $m)) { 70 $value = substr($value, strlen($m[0])); 71 } elseif (!preg_match('/^(?:[=@+\$!\(]|https?:)/i', $value)) { 72 $value = "http://$value"; 73 } 74 if (preg_match('#^https?://[^/]+$#i', $value, $m)) { 75 $value .= '/'; 76 } 77 } 78 $this->$name = $this->claimed_id = $value; 79 break; 80 case 'trustRoot': 81 case 'realm': 82 $this->trustRoot = trim($value); 83 break; 84 case 'xrdsOverride': 85 if (is_array($value)) { 86 list($pattern, $replacement) = $value; 87 $this->xrds_override_pattern = $pattern; 88 $this->xrds_override_replacement = $replacement; 89 } else { 90 trigger_error('Invalid value specified for "xrdsOverride".', E_USER_ERROR); 91 } 92 break; 93 } 94 } 95 96 function __get($name) 97 { 98 switch ($name) { 99 case 'identity': 100 # We return claimed_id instead of identity, 101 # because the developer should see the claimed identifier, 102 # i.e. what he set as identity, not the op-local identifier (which is what we verify) 103 return $this->claimed_id; 104 case 'trustRoot': 105 case 'realm': 106 return $this->trustRoot; 107 case 'mode': 108 return empty($this->data['openid_mode']) ? null : $this->data['openid_mode']; 109 } 110 } 111 112 function set_proxy($proxy) 113 { 114 if (!empty($proxy)) { 115 // When the proxy is a string - try to parse it. 116 if (!is_array($proxy)) { 117 $proxy = parse_url($proxy); 118 } 119 120 // Check if $proxy is valid after the parsing. 121 if ($proxy && !empty($proxy['host'])) { 122 // Make sure that a valid port number is specified. 123 if (array_key_exists('port', $proxy)) { 124 if (!is_int($proxy['port'])) { 125 $proxy['port'] = is_numeric($proxy['port']) ? intval($proxy['port']) : 0; 126 } 127 128 if ($proxy['port'] <= 0) { 129 throw new ErrorException('The specified proxy port number is invalid.'); 130 } 131 } 132 133 $this->proxy = $proxy; 134 } 135 } 136 } 137 138 /** 139 * Checks if the server specified in the url exists. 140 * 141 * @param $url url to check 142 * @return true, if the server exists; false otherwise 143 */ 144 function hostExists($url) 145 { 146 if (strpos($url, '/') === false) { 147 $server = $url; 148 } else { 149 $server = @parse_url($url, PHP_URL_HOST); 150 } 151 152 if (!$server) { 153 return false; 154 } 155 156 return !!gethostbynamel($server); 157 } 158 159 protected function set_realm($uri) 160 { 161 $realm = ''; 162 163 # Set a protocol, if not specified. 164 $realm .= (($offset = strpos($uri, '://')) === false) ? $this->get_realm_protocol() : ''; 165 166 # Set the offset properly. 167 $offset = (($offset !== false) ? $offset + 3 : 0); 168 169 # Get only the root, without the path. 170 $realm .= (($end = strpos($uri, '/', $offset)) === false) ? $uri : substr($uri, 0, $end); 171 172 $this->trustRoot = $realm; 173 } 174 175 protected function get_realm_protocol() 176 { 177 if (!empty($_SERVER['HTTPS'])) { 178 $use_secure_protocol = ($_SERVER['HTTPS'] != 'off'); 179 } else if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { 180 $use_secure_protocol = ($_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'); 181 } else if (isset($_SERVER['HTTP__WSSC'])) { 182 $use_secure_protocol = ($_SERVER['HTTP__WSSC'] == 'https'); 183 } else { 184 $use_secure_protocol = false; 185 } 186 187 return $use_secure_protocol ? 'https://' : 'http://'; 188 } 189 190 protected function request_curl($url, $method='GET', $params=array(), $update_claimed_id) 191 { 192 $params = http_build_query($params, '', '&'); 193 $curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : '')); 194 curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); 195 curl_setopt($curl, CURLOPT_HEADER, false); 196 curl_setopt($curl, CURLOPT_USERAGENT, $this->user_agent); 197 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); 198 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 199 200 if ($method == 'POST') { 201 curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-type: application/x-www-form-urlencoded')); 202 } else { 203 curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/xrds+xml, */*')); 204 } 205 206 curl_setopt($curl, CURLOPT_TIMEOUT, $this->curl_time_out); // defaults to infinite 207 curl_setopt($curl, CURLOPT_CONNECTTIMEOUT , $this->curl_connect_time_out); // defaults to 300s 208 209 if (!empty($this->proxy)) { 210 curl_setopt($curl, CURLOPT_PROXY, $this->proxy['host']); 211 212 if (!empty($this->proxy['port'])) { 213 curl_setopt($curl, CURLOPT_PROXYPORT, $this->proxy['port']); 214 } 215 216 if (!empty($this->proxy['user'])) { 217 curl_setopt($curl, CURLOPT_PROXYUSERPWD, $this->proxy['user'] . ':' . $this->proxy['pass']); 218 } 219 } 220 221 if($this->verify_peer !== null) { 222 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer); 223 if($this->capath) { 224 curl_setopt($curl, CURLOPT_CAPATH, $this->capath); 225 } 226 227 if($this->cainfo) { 228 curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo); 229 } 230 } 231 232 if ($method == 'POST') { 233 curl_setopt($curl, CURLOPT_POST, true); 234 curl_setopt($curl, CURLOPT_POSTFIELDS, $params); 235 } elseif ($method == 'HEAD') { 236 curl_setopt($curl, CURLOPT_HEADER, true); 237 curl_setopt($curl, CURLOPT_NOBODY, true); 238 } else { 239 curl_setopt($curl, CURLOPT_HEADER, true); 240 curl_setopt($curl, CURLOPT_HTTPGET, true); 241 } 242 $response = curl_exec($curl); 243 244 if($method == 'HEAD' && curl_getinfo($curl, CURLINFO_HTTP_CODE) == 405) { 245 curl_setopt($curl, CURLOPT_HTTPGET, true); 246 $response = curl_exec($curl); 247 $response = substr($response, 0, strpos($response, "\r\n\r\n")); 248 } 249 250 if($method == 'HEAD' || $method == 'GET') { 251 $header_response = $response; 252 253 # If it's a GET request, we want to only parse the header part. 254 if($method == 'GET') { 255 $header_response = substr($response, 0, strpos($response, "\r\n\r\n")); 256 } 257 258 $headers = array(); 259 foreach(explode("\n", $header_response) as $header) { 260 $pos = strpos($header,':'); 261 if ($pos !== false) { 262 $name = strtolower(trim(substr($header, 0, $pos))); 263 $headers[$name] = trim(substr($header, $pos+1)); 264 } 265 } 266 267 if($update_claimed_id) { 268 # Update the claimed_id value in case of redirections. 269 $effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); 270 # Ignore the fragment (some cURL versions don't handle it well). 271 if (strtok($effective_url, '#') != strtok($url, '#')) { 272 $this->identity = $this->claimed_id = $effective_url; 273 } 274 } 275 276 if($method == 'HEAD') { 277 return $headers; 278 } else { 279 $this->headers = $headers; 280 } 281 } 282 283 if (curl_errno($curl)) { 284 throw new ErrorException(curl_error($curl), curl_errno($curl)); 285 } 286 287 return $response; 288 } 289 290 protected function parse_header_array($array, $update_claimed_id) 291 { 292 $headers = array(); 293 foreach($array as $header) { 294 $pos = strpos($header,':'); 295 if ($pos !== false) { 296 $name = strtolower(trim(substr($header, 0, $pos))); 297 $headers[$name] = trim(substr($header, $pos+1)); 298 299 # Following possible redirections. The point is just to have 300 # claimed_id change with them, because the redirections 301 # are followed automatically. 302 # We ignore redirections with relative paths. 303 # If any known provider uses them, file a bug report. 304 if($name == 'location' && $update_claimed_id) { 305 if(strpos($headers[$name], 'http') === 0) { 306 $this->identity = $this->claimed_id = $headers[$name]; 307 } elseif($headers[$name][0] == '/') { 308 $parsed_url = parse_url($this->claimed_id); 309 $this->identity = 310 $this->claimed_id = $parsed_url['scheme'] . '://' 311 . $parsed_url['host'] 312 . $headers[$name]; 313 } 314 } 315 } 316 } 317 return $headers; 318 } 319 320 protected function request_streams($url, $method='GET', $params=array(), $update_claimed_id) 321 { 322 if(!$this->hostExists($url)) { 323 throw new ErrorException("Could not connect to $url.", 404); 324 } 325 326 if (empty($this->cnmatch)) { 327 $this->cnmatch = parse_url($url, PHP_URL_HOST); 328 } 329 330 $params = http_build_query($params, '', '&'); 331 switch($method) { 332 case 'GET': 333 $opts = array( 334 'http' => array( 335 'method' => 'GET', 336 'header' => 'Accept: application/xrds+xml, */*', 337 'user_agent' => $this->user_agent, 338 'ignore_errors' => true, 339 ), 340 'ssl' => array( 341 'peer_name' => $this->cnmatch 342 ) 343 ); 344 $url = $url . ($params ? '?' . $params : ''); 345 if (!empty($this->proxy)) { 346 $opts['http']['proxy'] = $this->proxy_url(); 347 } 348 break; 349 case 'POST': 350 $opts = array( 351 'http' => array( 352 'method' => 'POST', 353 'header' => 'Content-type: application/x-www-form-urlencoded', 354 'user_agent' => $this->user_agent, 355 'content' => $params, 356 'ignore_errors' => true, 357 ), 358 'ssl' => array( 359 'peer_name' => $this->cnmatch 360 ) 361 ); 362 if (!empty($this->proxy)) { 363 $opts['http']['proxy'] = $this->proxy_url(); 364 } 365 break; 366 case 'HEAD': 367 // We want to send a HEAD request, but since get_headers() doesn't 368 // accept $context parameter, we have to change the defaults. 369 $default = stream_context_get_options(stream_context_get_default()); 370 371 // PHP does not reset all options. Instead, it just sets the options 372 // available in the passed array, therefore set the defaults manually. 373 $default += array( 374 'http' => array(), 375 'ssl' => array() 376 ); 377 $default['http'] += array( 378 'method' => 'GET', 379 'header' => '', 380 'user_agent' => '', 381 'ignore_errors' => false 382 ); 383 $default['ssl'] += array( 384 'peer_name' => '' 385 ); 386 387 $opts = array( 388 'http' => array( 389 'method' => 'HEAD', 390 'header' => 'Accept: application/xrds+xml, */*', 391 'user_agent' => $this->user_agent, 392 'ignore_errors' => true, 393 ), 394 'ssl' => array( 395 'peer_name' => $this->cnmatch 396 ) 397 ); 398 399 // Enable validation of the SSL certificates. 400 if ($this->verify_peer) { 401 $default['ssl'] += array( 402 'verify_peer' => false, 403 'capath' => '', 404 'cafile' => '' 405 ); 406 $opts['ssl'] += array( 407 'verify_peer' => true, 408 'capath' => $this->capath, 409 'cafile' => $this->cainfo 410 ); 411 } 412 413 // Change the stream context options. 414 stream_context_get_default($opts); 415 416 $headers = get_headers($url . ($params ? '?' . $params : '')); 417 418 // Restore the stream context options. 419 stream_context_get_default($default); 420 421 if (!empty($headers)) { 422 if (intval(substr($headers[0], strlen('HTTP/1.1 '))) == 405) { 423 // The server doesn't support HEAD - emulate it with a GET. 424 $args = func_get_args(); 425 $args[1] = 'GET'; 426 call_user_func_array(array($this, 'request_streams'), $args); 427 $headers = $this->headers; 428 } else { 429 $headers = $this->parse_header_array($headers, $update_claimed_id); 430 } 431 } else { 432 $headers = array(); 433 } 434 435 return $headers; 436 } 437 438 if ($this->verify_peer) { 439 $opts['ssl'] += array( 440 'verify_peer' => true, 441 'capath' => $this->capath, 442 'cafile' => $this->cainfo 443 ); 444 } 445 446 $context = stream_context_create ($opts); 447 $data = file_get_contents($url, false, $context); 448 # This is a hack for providers who don't support HEAD requests. 449 # It just creates the headers array for the last request in $this->headers. 450 if(isset($http_response_header)) { 451 $this->headers = $this->parse_header_array($http_response_header, $update_claimed_id); 452 } 453 454 return $data; 455 } 456 457 protected function request($url, $method='GET', $params=array(), $update_claimed_id=false) 458 { 459 $use_curl = false; 460 461 if (function_exists('curl_init')) { 462 if (!$use_curl) { 463 # When allow_url_fopen is disabled, PHP streams will not work. 464 $use_curl = !ini_get('allow_url_fopen'); 465 } 466 467 if (!$use_curl) { 468 # When there is no HTTPS wrapper, PHP streams cannott be used. 469 $use_curl = !in_array('https', stream_get_wrappers()); 470 } 471 472 if (!$use_curl) { 473 # With open_basedir or safe_mode set, cURL can't follow redirects. 474 $use_curl = !(ini_get('safe_mode') || ini_get('open_basedir')); 475 } 476 } 477 478 return 479 $use_curl 480 ? $this->request_curl($url, $method, $params, $update_claimed_id) 481 : $this->request_streams($url, $method, $params, $update_claimed_id); 482 } 483 484 protected function proxy_url() 485 { 486 $result = ''; 487 488 if (!empty($this->proxy)) { 489 $result = $this->proxy['host']; 490 491 if (!empty($this->proxy['port'])) { 492 $result = $result . ':' . $this->proxy['port']; 493 } 494 495 if (!empty($this->proxy['user'])) { 496 $result = $this->proxy['user'] . ':' . $this->proxy['pass'] . '@' . $result; 497 } 498 499 $result = 'http://' . $result; 500 } 501 502 return $result; 503 } 504 505 protected function build_url($url, $parts) 506 { 507 if (isset($url['query'], $parts['query'])) { 508 $parts['query'] = $url['query'] . '&' . $parts['query']; 509 } 510 511 $url = $parts + $url; 512 $url = $url['scheme'] . '://' 513 . (empty($url['username'])?'' 514 :(empty($url['password'])? "{$url['username']}@" 515 :"{$url['username']}:{$url['password']}@")) 516 . $url['host'] 517 . (empty($url['port'])?'':":{$url['port']}") 518 . (empty($url['path'])?'':$url['path']) 519 . (empty($url['query'])?'':"?{$url['query']}") 520 . (empty($url['fragment'])?'':"#{$url['fragment']}"); 521 return $url; 522 } 523 524 /** 525 * Helper function used to scan for <meta>/<link> tags and extract information 526 * from them 527 */ 528 protected function htmlTag($content, $tag, $attrName, $attrValue, $valueName) 529 { 530 preg_match_all("#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1); 531 preg_match_all("#<{$tag}[^>]*$valueName=['\"](.+?)['\"][^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*/?>#i", $content, $matches2); 532 533 $result = array_merge($matches1[1], $matches2[1]); 534 return empty($result)?false:$result[0]; 535 } 536 537 /** 538 * Performs Yadis and HTML discovery. Normally not used. 539 * @param $url Identity URL. 540 * @return String OP Endpoint (i.e. OpenID provider address). 541 * @throws ErrorException 542 */ 543 function discover($url) 544 { 545 if (!$url) throw new ErrorException('No identity supplied.'); 546 # Use xri.net proxy to resolve i-name identities 547 if (!preg_match('#^https?:#', $url)) { 548 $url = "https://xri.net/$url"; 549 } 550 551 # We save the original url in case of Yadis discovery failure. 552 # It can happen when we'll be lead to an XRDS document 553 # which does not have any OpenID2 services. 554 $originalUrl = $url; 555 556 # A flag to disable yadis discovery in case of failure in headers. 557 $yadis = true; 558 559 # Allows optional regex replacement of the URL, e.g. to use Google Apps 560 # as an OpenID provider without setting up XRDS on the domain hosting. 561 if (!is_null($this->xrds_override_pattern) && !is_null($this->xrds_override_replacement)) { 562 $url = preg_replace($this->xrds_override_pattern, $this->xrds_override_replacement, $url); 563 } 564 565 # We'll jump a maximum of 5 times, to avoid endless redirections. 566 for ($i = 0; $i < 5; $i ++) { 567 if ($yadis) { 568 $headers = $this->request($url, 'HEAD', array(), true); 569 570 $next = false; 571 if (isset($headers['x-xrds-location'])) { 572 $url = $this->build_url(parse_url($url), parse_url(trim($headers['x-xrds-location']))); 573 $next = true; 574 } 575 576 if (isset($headers['content-type']) && $this->is_allowed_type($headers['content-type'])) { 577 # Found an XRDS document, now let's find the server, and optionally delegate. 578 $content = $this->request($url, 'GET'); 579 580 preg_match_all('#<Service.*?>(.*?)</Service>#s', $content, $m); 581 foreach($m[1] as $content) { 582 $content = ' ' . $content; # The space is added, so that strpos doesn't return 0. 583 584 # OpenID 2 585 $ns = preg_quote('http://specs.openid.net/auth/2.0/', '#'); 586 if(preg_match('#<Type>\s*'.$ns.'(server|signon)\s*</Type>#s', $content, $type)) { 587 if ($type[1] == 'server') $this->identifier_select = true; 588 589 preg_match('#<URI.*?>(.*)</URI>#', $content, $server); 590 preg_match('#<(Local|Canonical)ID>(.*)</\1ID>#', $content, $delegate); 591 if (empty($server)) { 592 return false; 593 } 594 # Does the server advertise support for either AX or SREG? 595 $this->ax = (bool) strpos($content, '<Type>http://openid.net/srv/ax/1.0</Type>'); 596 $this->sreg = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>') 597 || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>'); 598 599 $server = $server[1]; 600 if (isset($delegate[2])) $this->identity = trim($delegate[2]); 601 $this->version = 2; 602 603 $this->server = $server; 604 return $server; 605 } 606 607 # OpenID 1.1 608 $ns = preg_quote('http://openid.net/signon/1.1', '#'); 609 if (preg_match('#<Type>\s*'.$ns.'\s*</Type>#s', $content)) { 610 611 preg_match('#<URI.*?>(.*)</URI>#', $content, $server); 612 preg_match('#<.*?Delegate>(.*)</.*?Delegate>#', $content, $delegate); 613 if (empty($server)) { 614 return false; 615 } 616 # AX can be used only with OpenID 2.0, so checking only SREG 617 $this->sreg = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>') 618 || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>'); 619 620 $server = $server[1]; 621 if (isset($delegate[1])) $this->identity = $delegate[1]; 622 $this->version = 1; 623 624 $this->server = $server; 625 return $server; 626 } 627 } 628 629 $next = true; 630 $yadis = false; 631 $url = $originalUrl; 632 $content = null; 633 break; 634 } 635 if ($next) continue; 636 637 # There are no relevant information in headers, so we search the body. 638 $content = $this->request($url, 'GET', array(), true); 639 640 if (isset($this->headers['x-xrds-location'])) { 641 $url = $this->build_url(parse_url($url), parse_url(trim($this->headers['x-xrds-location']))); 642 continue; 643 } 644 645 $location = $this->htmlTag($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content'); 646 if ($location) { 647 $url = $this->build_url(parse_url($url), parse_url($location)); 648 continue; 649 } 650 } 651 652 if (!$content) $content = $this->request($url, 'GET'); 653 654 # At this point, the YADIS Discovery has failed, so we'll switch 655 # to openid2 HTML discovery, then fallback to openid 1.1 discovery. 656 $server = $this->htmlTag($content, 'link', 'rel', 'openid2.provider', 'href'); 657 $delegate = $this->htmlTag($content, 'link', 'rel', 'openid2.local_id', 'href'); 658 $this->version = 2; 659 660 if (!$server) { 661 # The same with openid 1.1 662 $server = $this->htmlTag($content, 'link', 'rel', 'openid.server', 'href'); 663 $delegate = $this->htmlTag($content, 'link', 'rel', 'openid.delegate', 'href'); 664 $this->version = 1; 665 } 666 667 if ($server) { 668 # We found an OpenID2 OP Endpoint 669 if ($delegate) { 670 # We have also found an OP-Local ID. 671 $this->identity = $delegate; 672 } 673 $this->server = $server; 674 return $server; 675 } 676 677 throw new ErrorException("No OpenID Server found at $url", 404); 678 } 679 throw new ErrorException('Endless redirection!', 500); 680 } 681 682 protected function is_allowed_type($content_type) { 683 # Apparently, some providers return XRDS documents as text/html. 684 # While it is against the spec, allowing this here shouldn't break 685 # compatibility with anything. 686 $allowed_types = array('application/xrds+xml', 'text/xml'); 687 688 # Only allow text/html content type for the Yahoo logins, since 689 # it might cause an endless redirection for the other providers. 690 if ($this->get_provider_name($this->claimed_id) == 'yahoo') { 691 $allowed_types[] = 'text/html'; 692 } 693 694 foreach ($allowed_types as $type) { 695 if (strpos($content_type, $type) !== false) { 696 return true; 697 } 698 } 699 700 return false; 701 } 702 703 protected function get_provider_name($provider_url) { 704 $result = ''; 705 706 if (!empty($provider_url)) { 707 $tokens = array_reverse( 708 explode('.', parse_url($provider_url, PHP_URL_HOST)) 709 ); 710 $result = strtolower( 711 (count($tokens) > 1 && strlen($tokens[1]) > 3) 712 ? $tokens[1] 713 : (count($tokens) > 2 ? $tokens[2] : '') 714 ); 715 } 716 717 return $result; 718 } 719 720 protected function sregParams() 721 { 722 $params = array(); 723 # We always use SREG 1.1, even if the server is advertising only support for 1.0. 724 # That's because it's fully backwards compatible with 1.0, and some providers 725 # advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com 726 $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1'; 727 if ($this->required) { 728 $params['openid.sreg.required'] = array(); 729 foreach ($this->required as $required) { 730 if (!isset(self::$ax_to_sreg[$required])) continue; 731 $params['openid.sreg.required'][] = self::$ax_to_sreg[$required]; 732 } 733 $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']); 734 } 735 736 if ($this->optional) { 737 $params['openid.sreg.optional'] = array(); 738 foreach ($this->optional as $optional) { 739 if (!isset(self::$ax_to_sreg[$optional])) continue; 740 $params['openid.sreg.optional'][] = self::$ax_to_sreg[$optional]; 741 } 742 $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']); 743 } 744 return $params; 745 } 746 747 protected function axParams() 748 { 749 $params = array(); 750 if ($this->required || $this->optional) { 751 $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0'; 752 $params['openid.ax.mode'] = 'fetch_request'; 753 $this->aliases = array(); 754 $counts = array(); 755 $required = array(); 756 $optional = array(); 757 foreach (array('required','optional') as $type) { 758 foreach ($this->$type as $alias => $field) { 759 if (is_int($alias)) $alias = strtr($field, '/', '_'); 760 $this->aliases[$alias] = 'http://axschema.org/' . $field; 761 if (empty($counts[$alias])) $counts[$alias] = 0; 762 $counts[$alias] += 1; 763 ${$type}[] = $alias; 764 } 765 } 766 foreach ($this->aliases as $alias => $ns) { 767 $params['openid.ax.type.' . $alias] = $ns; 768 } 769 foreach ($counts as $alias => $count) { 770 if ($count == 1) continue; 771 $params['openid.ax.count.' . $alias] = $count; 772 } 773 774 # Don't send empty ax.required and ax.if_available. 775 # Google and possibly other providers refuse to support ax when one of these is empty. 776 if($required) { 777 $params['openid.ax.required'] = implode(',', $required); 778 } 779 if($optional) { 780 $params['openid.ax.if_available'] = implode(',', $optional); 781 } 782 } 783 return $params; 784 } 785 786 protected function authUrl_v1($immediate) 787 { 788 $returnUrl = $this->returnUrl; 789 # If we have an openid.delegate that is different from our claimed id, 790 # we need to somehow preserve the claimed id between requests. 791 # The simplest way is to just send it along with the return_to url. 792 if($this->identity != $this->claimed_id) { 793 $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->claimed_id; 794 } 795 796 $params = array( 797 'openid.return_to' => $returnUrl, 798 'openid.mode' => $immediate ? 'checkid_immediate' : 'checkid_setup', 799 'openid.identity' => $this->identity, 800 'openid.trust_root' => $this->trustRoot, 801 ) + $this->sregParams(); 802 803 return $this->build_url(parse_url($this->server) 804 , array('query' => http_build_query($params, '', '&'))); 805 } 806 807 protected function authUrl_v2($immediate) 808 { 809 $params = array( 810 'openid.ns' => 'http://specs.openid.net/auth/2.0', 811 'openid.mode' => $immediate ? 'checkid_immediate' : 'checkid_setup', 812 'openid.return_to' => $this->returnUrl, 813 'openid.realm' => $this->trustRoot, 814 ); 815 816 if ($this->ax) { 817 $params += $this->axParams(); 818 } 819 820 if ($this->sreg) { 821 $params += $this->sregParams(); 822 } 823 824 if (!$this->ax && !$this->sreg) { 825 # If OP doesn't advertise either SREG, nor AX, let's send them both 826 # in worst case we don't get anything in return. 827 $params += $this->axParams() + $this->sregParams(); 828 } 829 830 if (!empty($this->oauth) && is_array($this->oauth)) { 831 $params['openid.ns.oauth'] = 'http://specs.openid.net/extensions/oauth/1.0'; 832 $params['openid.oauth.consumer'] = str_replace(array('http://', 'https://'), '', $this->trustRoot); 833 $params['openid.oauth.scope'] = implode(' ', $this->oauth); 834 } 835 836 if ($this->identifier_select) { 837 $params['openid.identity'] = $params['openid.claimed_id'] 838 = 'http://specs.openid.net/auth/2.0/identifier_select'; 839 } else { 840 $params['openid.identity'] = $this->identity; 841 $params['openid.claimed_id'] = $this->claimed_id; 842 } 843 844 return $this->build_url(parse_url($this->server) 845 , array('query' => http_build_query($params, '', '&'))); 846 } 847 848 /** 849 * Returns authentication url. Usually, you want to redirect your user to it. 850 * @return String The authentication url. 851 * @param String $select_identifier Whether to request OP to select identity for an user in OpenID 2. Does not affect OpenID 1. 852 * @throws ErrorException 853 */ 854 function authUrl($immediate = false) 855 { 856 if ($this->setup_url && !$immediate) return $this->setup_url; 857 if (!$this->server) $this->discover($this->identity); 858 859 if ($this->version == 2) { 860 return $this->authUrl_v2($immediate); 861 } 862 return $this->authUrl_v1($immediate); 863 } 864 865 /** 866 * Performs OpenID verification with the OP. 867 * @return Bool Whether the verification was successful. 868 * @throws ErrorException 869 */ 870 function validate() 871 { 872 # If the request was using immediate mode, a failure may be reported 873 # by presenting user_setup_url (for 1.1) or reporting 874 # mode 'setup_needed' (for 2.0). Also catching all modes other than 875 # id_res, in order to avoid throwing errors. 876 if(isset($this->data['openid_user_setup_url'])) { 877 $this->setup_url = $this->data['openid_user_setup_url']; 878 return false; 879 } 880 if($this->mode != 'id_res') { 881 return false; 882 } 883 884 $this->claimed_id = isset($this->data['openid_claimed_id'])?$this->data['openid_claimed_id']:$this->data['openid_identity']; 885 $params = array( 886 'openid.assoc_handle' => $this->data['openid_assoc_handle'], 887 'openid.signed' => $this->data['openid_signed'], 888 'openid.sig' => $this->data['openid_sig'], 889 ); 890 891 if (isset($this->data['openid_ns'])) { 892 # We're dealing with an OpenID 2.0 server, so let's set an ns 893 # Even though we should know location of the endpoint, 894 # we still need to verify it by discovery, so $server is not set here 895 $params['openid.ns'] = 'http://specs.openid.net/auth/2.0'; 896 } elseif (isset($this->data['openid_claimed_id']) 897 && $this->data['openid_claimed_id'] != $this->data['openid_identity'] 898 ) { 899 # If it's an OpenID 1 provider, and we've got claimed_id, 900 # we have to append it to the returnUrl, like authUrl_v1 does. 901 $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?') 902 . 'openid.claimed_id=' . $this->claimed_id; 903 } 904 905 if ($this->data['openid_return_to'] != $this->returnUrl) { 906 # The return_to url must match the url of current request. 907 # I'm assuming that no one will set the returnUrl to something that doesn't make sense. 908 return false; 909 } 910 911 $server = $this->discover($this->claimed_id); 912 913 foreach (explode(',', $this->data['openid_signed']) as $item) { 914 # Checking whether magic_quotes_gpc is turned on, because 915 # the function may fail if it is. For example, when fetching 916 # AX namePerson, it might contain an apostrophe, which will be escaped. 917 # In such case, validation would fail, since we'd send different data than OP 918 # wants to verify. stripslashes() should solve that problem, but we can't 919 # use it when magic_quotes is off. 920 $value = $this->data['openid_' . str_replace('.','_',$item)]; 921 $params['openid.' . $item] = function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc() ? stripslashes($value) : $value; 922 923 } 924 925 $params['openid.mode'] = 'check_authentication'; 926 927 $response = $this->request($server, 'POST', $params); 928 929 return preg_match('/is_valid\s*:\s*true/i', $response); 930 } 931 932 protected function getAxAttributes() 933 { 934 $result = array(); 935 936 if ($alias = $this->getNamespaceAlias('http://openid.net/srv/ax/1.0', 'ax')) { 937 $prefix = 'openid_' . $alias; 938 $length = strlen('http://axschema.org/'); 939 940 foreach (explode(',', $this->data['openid_signed']) as $key) { 941 $keyMatch = $alias . '.type.'; 942 943 if (strncmp($key, $keyMatch, strlen($keyMatch)) !== 0) { 944 continue; 945 } 946 947 $key = substr($key, strlen($keyMatch)); 948 $idv = $prefix . '_value_' . $key; 949 $idc = $prefix . '_count_' . $key; 950 $key = substr($this->getItem($prefix . '_type_' . $key), $length); 951 952 if (!empty($key)) { 953 if (($count = intval($this->getItem($idc))) > 0) { 954 $value = array(); 955 956 for ($i = 1; $i <= $count; $i++) { 957 $value[] = $this->getItem($idv . '_' . $i); 958 } 959 960 $value = ($count == 1) ? reset($value) : $value; 961 } else { 962 $value = $this->getItem($idv); 963 } 964 965 if (!is_null($value)) { 966 $result[$key] = $value; 967 } 968 } 969 } 970 } else { 971 // No alias for the AX schema has been found, 972 // so there is no AX data in the OP's response. 973 } 974 975 return $result; 976 } 977 978 protected function getSregAttributes() 979 { 980 $attributes = array(); 981 $sreg_to_ax = array_flip(self::$ax_to_sreg); 982 foreach (explode(',', $this->data['openid_signed']) as $key) { 983 $keyMatch = 'sreg.'; 984 if (strncmp($key, $keyMatch, strlen($keyMatch)) !== 0) { 985 continue; 986 } 987 $key = substr($key, strlen($keyMatch)); 988 if (!isset($sreg_to_ax[$key])) { 989 # The field name isn't part of the SREG spec, so we ignore it. 990 continue; 991 } 992 $attributes[$sreg_to_ax[$key]] = $this->data['openid_sreg_' . $key]; 993 } 994 return $attributes; 995 } 996 997 /** 998 * Gets AX/SREG attributes provided by OP. should be used only after successful validation. 999 * Note that it does not guarantee that any of the required/optional parameters will be present, 1000 * or that there will be no other attributes besides those specified. 1001 * In other words. OP may provide whatever information it wants to. 1002 * * SREG names will be mapped to AX names. 1003 * * @return Array Array of attributes with keys being the AX schema names, e.g. 'contact/email' 1004 * @see http://www.axschema.org/types/ 1005 */ 1006 function getAttributes() 1007 { 1008 if (isset($this->data['openid_ns']) 1009 && $this->data['openid_ns'] == 'http://specs.openid.net/auth/2.0' 1010 ) { # OpenID 2.0 1011 # We search for both AX and SREG attributes, with AX taking precedence. 1012 return $this->getAxAttributes() + $this->getSregAttributes(); 1013 } 1014 return $this->getSregAttributes(); 1015 } 1016 1017 /** 1018 * Gets an OAuth request token if the OpenID+OAuth hybrid protocol has been used. 1019 * 1020 * In order to use the OpenID+OAuth hybrid protocol, you need to add at least one 1021 * scope to the $openid->oauth array before you get the call to getAuthUrl(), e.g.: 1022 * $openid->oauth[] = 'https://www.googleapis.com/auth/plus.me'; 1023 * 1024 * Furthermore the registered consumer name must fit the OpenID realm. 1025 * To register an OpenID consumer at Google use: https://www.google.com/accounts/ManageDomains 1026 * 1027 * @return string|bool OAuth request token on success, FALSE if no token was provided. 1028 */ 1029 function getOAuthRequestToken() 1030 { 1031 $alias = $this->getNamespaceAlias('http://specs.openid.net/extensions/oauth/1.0'); 1032 1033 return !empty($alias) ? $this->data['openid_' . $alias . '_request_token'] : false; 1034 } 1035 1036 /** 1037 * Gets the alias for the specified namespace, if it's present. 1038 * 1039 * @param string $namespace The namespace for which an alias is needed. 1040 * @param string $hint Common alias of this namespace, used for optimization. 1041 * @return string|null The namespace alias if found, otherwise - NULL. 1042 */ 1043 private function getNamespaceAlias($namespace, $hint = null) 1044 { 1045 $result = null; 1046 1047 if (empty($hint) || $this->getItem('openid_ns_' . $hint) != $namespace) { 1048 // The common alias is either undefined or points to 1049 // some other extension - search for another alias.. 1050 $prefix = 'openid_ns_'; 1051 $length = strlen($prefix); 1052 1053 foreach ($this->data as $key => $val) { 1054 if (strncmp($key, $prefix, $length) === 0 && $val === $namespace) { 1055 $result = trim(substr($key, $length)); 1056 break; 1057 } 1058 } 1059 } else { 1060 $result = $hint; 1061 } 1062 1063 return $result; 1064 } 1065 1066 /** 1067 * Gets an item from the $data array by the specified id. 1068 * 1069 * @param string $id The id of the desired item. 1070 * @return string|null The item if found, otherwise - NULL. 1071 */ 1072 private function getItem($id) 1073 { 1074 return isset($this->data[$id]) ? $this->data[$id] : null; 1075 } 1076} 1077