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

OpenID Authentication

7 | 8 |
9 | 10 | 11 | 12 |
13 | 14 | 21 | 22 | --------------------------------------------------------------------------------