55 private $identity, $claimed_id;
56 protected $server, $version, $trustRoot, $aliases, $identifier_select = false
57 , $ax =
false, $sreg =
false, $data;
58 static protected $ax_to_sreg = array(
59 'namePerson/friendly' =>
'nickname',
60 'contact/email' =>
'email',
61 'namePerson' =>
'fullname',
63 'person/gender' =>
'gender',
64 'contact/postalCode/home' =>
'postcode',
65 'contact/country/home' =>
'country',
66 'pref/language' =>
'language',
67 'pref/timezone' =>
'timezone',
70 function __construct()
72 $this->trustRoot = (!empty($_SERVER[
'HTTPS']) ?
'https' :
'http') .
'://' . $_SERVER[
'HTTP_HOST'];
73 $uri = rtrim(preg_replace(
'#((?<=\?)|&)openid\.[^&]+#',
'', $_SERVER[
'REQUEST_URI']),
'?');
74 $this->returnUrl = $this->trustRoot . $uri;
76 $this->data = $_POST + $_GET; # OPs may send data as POST or GET.
79 function __set($name, $value)
83 if (strlen($value = trim((String) $value))) {
84 if (preg_match(
'#^xri:/*#i', $value, $m)) {
85 $value = substr($value, strlen($m[0]));
86 } elseif (!preg_match(
'/^(?:[=@+\$!\(]|https?:)/i', $value)) {
87 $value =
"http://$value";
89 if (preg_match(
'#^https?://[^/]+$#i', $value, $m)) {
93 $this->$name = $this->claimed_id = $value;
97 $this->trustRoot = trim($value);
101 function __get($name)
105 # We return claimed_id instead of identity,
106 # because the developer should see the claimed identifier,
107 # i.e. what he set as identity, not the op-local identifier (which is what we verify)
108 return $this->claimed_id;
111 return $this->trustRoot;
113 return empty($this->data[
'openid_mode']) ? null : $this->data[
'openid_mode'];
125 if (strpos($url,
'/') ===
false) {
128 $server = @parse_url($url, PHP_URL_HOST);
135 return !!gethostbynamel($server);
138 protected function request_curl($url, $method=
'GET', $params=array())
140 $params = http_build_query($params,
'',
'&');
141 $curl = curl_init($url . ($method ==
'GET' && $params ?
'?' . $params :
''));
142 curl_setopt($curl, CURLOPT_FOLLOWLOCATION,
true);
143 curl_setopt($curl, CURLOPT_HEADER,
false);
144 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER,
false);
145 curl_setopt($curl, CURLOPT_RETURNTRANSFER,
true);
146 curl_setopt($curl, CURLOPT_HTTPHEADER, array(
'Accept: application/xrds+xml, */*'));
148 if($this->verify_peer !== null) {
149 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer);
151 curl_setopt($curl, CURLOPT_CAPATH, $this->capath);
155 curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo);
159 if ($method ==
'POST') {
160 curl_setopt($curl, CURLOPT_POST,
true);
161 curl_setopt($curl, CURLOPT_POSTFIELDS, $params);
162 } elseif ($method ==
'HEAD') {
163 curl_setopt($curl, CURLOPT_HEADER,
true);
164 curl_setopt($curl, CURLOPT_NOBODY,
true);
166 curl_setopt($curl, CURLOPT_HTTPGET,
true);
168 $response = curl_exec($curl);
170 if($method ==
'HEAD') {
172 foreach(explode(
"\n", $response) as $header) {
173 $pos = strpos($header,
':');
174 $name = strtolower(trim(substr($header, 0, $pos)));
175 $headers[$name] = trim(substr($header, $pos+1));
178 # Updating claimed_id in case of redirections.
179 $effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
180 if($effective_url != $url) {
181 $this->identity = $this->claimed_id = $effective_url;
187 if (curl_errno($curl)) {
188 throw new ErrorException(curl_error($curl), curl_errno($curl));
194 protected function request_streams($url, $method=
'GET', $params=array())
197 throw new ErrorException(
'Invalid request.');
200 $params = http_build_query($params,
'',
'&');
206 'header' =>
'Accept: application/xrds+xml, */*',
207 'ignore_errors' =>
true,
210 $url = $url . ($params ?
'?' . $params :
'');
216 'header' =>
'Content-type: application/x-www-form-urlencoded',
217 'content' => $params,
218 'ignore_errors' =>
true,
223 # We want to send a HEAD request,
224 # but since get_headers doesn't accept $context parameter,
225 # we have to change the defaults.
226 $default = stream_context_get_options(stream_context_get_default());
227 stream_context_get_default(
228 array(
'http' => array(
230 'header' =>
'Accept: application/xrds+xml, */*',
231 'ignore_errors' =>
true,
235 $url = $url . ($params ?
'?' . $params :
'');
236 $headers_tmp = get_headers ($url);
243 foreach($headers_tmp as $header) {
244 $pos = strpos($header,
':');
245 $name = strtolower(trim(substr($header, 0, $pos)));
246 $headers[$name] = trim(substr($header, $pos+1));
248 # Following possible redirections. The point is just to have
249 # claimed_id change with them, because get_headers() will
250 # follow redirections automatically.
251 # We ignore redirections with relative paths.
252 # If any known provider uses them, file a bug report.
253 if($name ==
'location') {
254 if(strpos($headers[$name],
'http') === 0) {
255 $this->identity = $this->claimed_id = $headers[$name];
256 } elseif($headers[$name][0] ==
'/') {
257 $parsed_url = parse_url($this->claimed_id);
259 $this->claimed_id = $parsed_url[
'scheme'] .
'://'
260 . $parsed_url[
'host']
267 stream_context_get_default($default);
271 if($this->verify_peer) {
272 $opts += array(
'ssl' => array(
273 'verify_peer' =>
true,
274 'capath' => $this->capath,
275 'cafile' => $this->cainfo,
279 $context = stream_context_create ($opts);
281 return file_get_contents($url,
false, $context);
284 protected function request($url, $method=
'GET', $params=array())
286 if(function_exists(
'curl_init') && !ini_get(
'safe_mode')) {
287 return $this->request_curl($url, $method, $params);
289 return $this->request_streams($url, $method, $params);
292 protected function build_url($url, $parts)
294 if (isset($url[
'query'], $parts[
'query'])) {
295 $parts[
'query'] = $url[
'query'] .
'&' . $parts[
'query'];
298 $url = $parts + $url;
299 $url = $url[
'scheme'] .
'://'
300 . (empty($url[
'username'])?
''
301 :(empty($url[
'password'])?
"{$url['username']}@"
302 :
"{$url['username']}:{$url['password']}@"))
304 . (empty($url[
'port'])?
'':
":{$url['port']}")
305 . (empty($url[
'path'])?
'':$url[
'path'])
306 . (empty($url[
'query'])?
'':
"?{$url['query']}")
307 . (empty($url[
'fragment'])?
'':
"#{$url['fragment']}");
315 protected function htmlTag($content, $tag, $attrName, $attrValue, $valueName)
317 preg_match_all(
"#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1);
318 preg_match_all(
"#<{$tag}[^>]*$valueName=['\"](.+?)['\"][^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*/?>#i", $content, $matches2);
320 $result = array_merge($matches1[1], $matches2[1]);
321 return empty($result)?
false:$result[0];
332 if (!$url)
throw new ErrorException(
'No identity supplied.');
333 # Use xri.net proxy to resolve i-name identities
334 if (!preg_match(
'#^https?:#', $url)) {
335 $url =
"https://xri.net/$url";
338 # We save the original url in case of Yadis discovery failure.
339 # It can happen when we'll be lead to an XRDS document
340 # which does not have any OpenID2 services.
343 # A flag to disable yadis discovery in case of failure in headers.
346 # We'll jump a maximum of 5 times, to avoid endless redirections.
347 for ($i = 0; $i < 5; $i ++) {
349 $headers = $this->request($url,
'HEAD');
352 if (isset($headers[
'x-xrds-location'])) {
353 $url = $this->build_url(parse_url($url), parse_url(trim($headers[
'x-xrds-location'])));
357 if (isset($headers[
'content-type'])
358 && (strpos($headers[
'content-type'],
'application/xrds+xml') !==
false
359 || strpos($headers[
'content-type'],
'text/xml') !==
false)
361 # Apparently, some providers return XRDS documents as text/html.
362 # While it is against the spec, allowing this here shouldn't break
363 # compatibility with anything.
365 # Found an XRDS document, now let's find the server, and optionally delegate.
366 $content = $this->request($url,
'GET');
368 preg_match_all(
'#<Service.*?>(.*?)</Service>#s', $content, $m);
369 foreach($m[1] as $content) {
370 $content =
' ' . $content; # The space is added, so that strpos doesn
't return 0.
373 $ns = preg_quote('http:
374 if(preg_match(
'#<Type>\s*'.$ns.
'(server|signon)\s*</Type>#s', $content, $type)) {
375 if ($type[1] ==
'server') $this->identifier_select =
true;
377 preg_match(
'#<URI.*?>(.*)</URI>#', $content, $server);
378 preg_match(
'#<(Local|Canonical)ID>(.*)</\1ID>#', $content, $delegate);
379 if (empty($server)) {
382 # Does the server advertise support for either AX or SREG?
383 $this->ax = (bool) strpos($content,
'<Type>http://openid.net/srv/ax/1.0</Type>');
384 $this->sreg = strpos($content,
'<Type>http://openid.net/sreg/1.0</Type>')
385 || strpos($content,
'<Type>http://openid.net/extensions/sreg/1.1</Type>');
387 $server = $server[1];
388 if (isset($delegate[2])) $this->identity = trim($delegate[2]);
391 $this->server = $server;
396 $ns = preg_quote(
'http://openid.net/signon/1.1');
397 if (preg_match(
'#<Type>\s*'.$ns.
'\s*</Type>#s', $content)) {
399 preg_match(
'#<URI.*?>(.*)</URI>#', $content, $server);
400 preg_match(
'#<.*?Delegate>(.*)</.*?Delegate>#', $content, $delegate);
401 if (empty($server)) {
404 # AX can be used only with OpenID 2.0, so checking only SREG
405 $this->sreg = strpos($content,
'<Type>http://openid.net/sreg/1.0</Type>')
406 || strpos($content,
'<Type>http://openid.net/extensions/sreg/1.1</Type>');
408 $server = $server[1];
409 if (isset($delegate[1])) $this->identity = $delegate[1];
412 $this->server = $server;
425 # There are no relevant information in headers, so we search the body.
426 $content = $this->request($url,
'GET');
427 $location = $this->
htmlTag($content,
'meta',
'http-equiv',
'X-XRDS-Location',
'content');
429 $url = $this->build_url(parse_url($url), parse_url($location));
434 if (!$content) $content = $this->request($url,
'GET');
436 # At this point, the YADIS Discovery has failed, so we'll switch
437 # to openid2 HTML discovery, then fallback to openid 1.1 discovery.
438 $server = $this->
htmlTag($content,
'link',
'rel',
'openid2.provider',
'href');
439 $delegate = $this->
htmlTag($content,
'link',
'rel',
'openid2.local_id',
'href');
443 # The same with openid 1.1
444 $server = $this->
htmlTag($content,
'link',
'rel',
'openid.server',
'href');
445 $delegate = $this->
htmlTag($content,
'link',
'rel',
'openid.delegate',
'href');
450 # We found an OpenID2 OP Endpoint
452 # We have also found an OP-Local ID.
453 $this->identity = $delegate;
455 $this->server = $server;
458 throw new ErrorException(
'No servers found!');
460 throw new ErrorException(
'Endless redirection!');
463 protected function sregParams()
466 # We always use SREG 1.1, even if the server is advertising only support for 1.0.
467 # That's because it's fully backwards compatibile with 1.0, and some providers
468 # advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com
469 $params[
'openid.ns.sreg'] =
'http://openid.net/extensions/sreg/1.1';
470 if ($this->required) {
471 $params[
'openid.sreg.required'] = array();
472 foreach ($this->required as $required) {
473 if (!isset(self::$ax_to_sreg[$required]))
continue;
474 $params[
'openid.sreg.required'][] = self::$ax_to_sreg[$required];
476 $params[
'openid.sreg.required'] = implode(
',', $params[
'openid.sreg.required']);
479 if ($this->optional) {
480 $params[
'openid.sreg.optional'] = array();
481 foreach ($this->optional as $optional) {
482 if (!isset(self::$ax_to_sreg[$optional]))
continue;
483 $params[
'openid.sreg.optional'][] = self::$ax_to_sreg[$optional];
485 $params[
'openid.sreg.optional'] = implode(
',', $params[
'openid.sreg.optional']);
490 protected function axParams()
493 if ($this->required || $this->optional) {
494 $params[
'openid.ns.ax'] =
'http://openid.net/srv/ax/1.0';
495 $params[
'openid.ax.mode'] =
'fetch_request';
496 $this->aliases = array();
500 foreach (array(
'required',
'optional') as $type) {
501 foreach ($this->$type as $alias => $field) {
502 if (is_int($alias)) $alias = strtr($field,
'/',
'_');
503 $this->aliases[$alias] =
'http://axschema.org/' . $field;
504 if (empty($counts[$alias])) $counts[$alias] = 0;
505 $counts[$alias] += 1;
509 foreach ($this->aliases as $alias => $ns) {
510 $params[
'openid.ax.type.' . $alias] = $ns;
512 foreach ($counts as $alias => $count) {
513 if ($count == 1)
continue;
514 $params[
'openid.ax.count.' . $alias] = $count;
517 # Don't send empty ax.requied and ax.if_available.
518 # Google and possibly other providers refuse to support ax when one of these is empty.
520 $params[
'openid.ax.required'] = implode(
',', $required);
523 $params[
'openid.ax.if_available'] = implode(
',', $optional);
529 protected function authUrl_v1()
531 $returnUrl = $this->returnUrl;
532 # If we have an openid.delegate that is different from our claimed id,
533 # we need to somehow preserve the claimed id between requests.
534 # The simplest way is to just send it along with the return_to url.
535 if($this->identity != $this->claimed_id) {
536 $returnUrl .= (strpos($returnUrl,
'?') ?
'&' :
'?') .
'openid.claimed_id=' . $this->claimed_id;
540 'openid.return_to' => $returnUrl,
541 'openid.mode' =>
'checkid_setup',
542 'openid.identity' => $this->identity,
543 'openid.trust_root' => $this->trustRoot,
544 ) + $this->sregParams();
546 return $this->build_url(parse_url($this->server)
547 , array(
'query' => http_build_query($params,
'',
'&')));
550 protected function authUrl_v2($identifier_select)
553 'openid.ns' =>
'http://specs.openid.net/auth/2.0',
554 'openid.mode' =>
'checkid_setup',
555 'openid.return_to' => $this->returnUrl,
556 'openid.realm' => $this->trustRoot,
559 $params += $this->axParams();
562 $params += $this->sregParams();
564 if (!$this->ax && !$this->sreg) {
565 # If OP doesn't advertise either SREG, nor AX, let's send them both
566 # in worst case we don't get anything in return.
567 $params += $this->axParams() + $this->sregParams();
570 if ($identifier_select) {
571 $params[
'openid.identity'] = $params[
'openid.claimed_id']
572 =
'http://specs.openid.net/auth/2.0/identifier_select';
574 $params[
'openid.identity'] = $this->identity;
575 $params[
'openid.claimed_id'] = $this->claimed_id;
578 return $this->build_url(parse_url($this->server)
579 , array(
'query' => http_build_query($params,
'',
'&')));
590 if (!$this->server) $this->
discover($this->identity);
592 if ($this->version == 2) {
593 if ($identifier_select === null) {
594 return $this->authUrl_v2($this->identifier_select);
596 return $this->authUrl_v2($identifier_select);
598 return $this->authUrl_v1();
608 $this->claimed_id = isset($this->data[
'openid_claimed_id'])?$this->data[
'openid_claimed_id']:$this->data[
'openid_identity'];
610 'openid.assoc_handle' => $this->data[
'openid_assoc_handle'],
611 'openid.signed' => $this->data[
'openid_signed'],
612 'openid.sig' => $this->data[
'openid_sig'],
615 if (isset($this->data[
'openid_ns'])) {
616 # We're dealing with an OpenID 2.0 server, so let's set an ns
617 # Even though we should know location of the endpoint,
618 # we still need to verify it by discovery, so $server is not set here
619 $params[
'openid.ns'] =
'http://specs.openid.net/auth/2.0';
620 } elseif (isset($this->data[
'openid_claimed_id'])
621 && $this->data[
'openid_claimed_id'] != $this->data[
'openid_identity']
623 # If it's an OpenID 1 provider, and we've got claimed_id,
624 # we have to append it to the returnUrl, like authUrl_v1 does.
625 $this->returnUrl .= (strpos($this->returnUrl,
'?') ?
'&' :
'?')
626 .
'openid.claimed_id=' . $this->claimed_id;
629 if ($this->data[
'openid_return_to'] != $this->returnUrl) {
630 # The return_to url must match the url of current request.
631 # I'm assuing that noone will set the returnUrl to something that doesn't make sense.
635 $server = $this->
discover($this->claimed_id);
637 foreach (explode(
',', $this->data[
'openid_signed']) as $item) {
638 # Checking whether magic_quotes_gpc is turned on, because
639 # the function may fail if it is. For example, when fetching
640 # AX namePerson, it might containg an apostrophe, which will be escaped.
641 # In such case, validation would fail, since we'd send different data than OP
642 # wants to verify. stripslashes() should solve that problem, but we can't
643 # use it when magic_quotes is off.
644 $value = $this->data[
'openid_' . str_replace(
'.',
'_',$item)];
645 $params[
'openid.' . $item] = get_magic_quotes_gpc() ? stripslashes($value) : $value;
649 $params[
'openid.mode'] =
'check_authentication';
651 $response = $this->request($server,
'POST', $params);
653 return preg_match(
'/is_valid\s*:\s*true/i', $response);
656 protected function getAxAttributes()
659 if (isset($this->data[
'openid_ns_ax'])
660 && $this->data[
'openid_ns_ax'] !=
'http://openid.net/srv/ax/1.0'
661 ) { # It
's the most likely case, so we'll check it before
664 # 'ax' prefix is either undefined, or points to another extension,
665 # so we search for another prefix
666 foreach ($this->data as $key => $val) {
667 if (substr($key, 0, strlen(
'openid_ns_')) ==
'openid_ns_'
668 && $val ==
'http://openid.net/srv/ax/1.0'
670 $alias = substr($key, strlen(
'openid_ns_'));
676 # An alias for AX schema has not been found,
677 # so there is no AX data in the OP's response
681 $attributes = array();
682 foreach ($this->data as $key => $value) {
683 $keyMatch =
'openid_' . $alias .
'_value_';
684 if (substr($key, 0, strlen($keyMatch)) != $keyMatch) {
687 $key = substr($key, strlen($keyMatch));
688 if (!isset($this->data[
'openid_' . $alias .
'_type_' . $key])) {
689 # OP is breaking the spec by returning a field without
690 # associated ns. This shouldn't happen, but it's better
691 # to check, than cause an E_NOTICE.
694 $key = substr($this->data[
'openid_' . $alias .
'_type_' . $key],
695 strlen(
'http://axschema.org/'));
696 $attributes[$key] = $value;
701 protected function getSregAttributes()
703 $attributes = array();
704 $sreg_to_ax = array_flip(self::$ax_to_sreg);
705 foreach ($this->data as $key => $value) {
706 $keyMatch =
'openid_sreg_';
707 if (substr($key, 0, strlen($keyMatch)) != $keyMatch) {
710 $key = substr($key, strlen($keyMatch));
711 if (!isset($sreg_to_ax[$key])) {
712 # The field name isn't part of the SREG spec, so we ignore it.
715 $attributes[$sreg_to_ax[$key]] = $value;
731 if (isset($this->data[
'openid_ns'])
732 && $this->data[
'openid_ns'] ==
'http://specs.openid.net/auth/2.0'
734 # We search
for both AX and SREG attributes, with AX taking precedence.
735 return $this->getAxAttributes() + $this->getSregAttributes();
737 return $this->getSregAttributes();