├── 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>(.*)\1ID>#', $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 |
13 |
14 |
21 |
22 |
--------------------------------------------------------------------------------