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