├── CHANGELOG.md ├── LICENSE ├── README.md ├── USAGE.md ├── composer.json ├── examples ├── example-google.php ├── example-google_apps.php └── example.php ├── openid.php └── provider ├── example-mysql.php ├── example.php └── provider.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # LightOpenID Change Log 2 | 3 | ## v1.3.1 (March 04, 2016) 4 | 5 | `fix` Fixed an incorrect function call to get_provider_name(). 6 | 7 | ## v1.3.0 (February 20, 2016) 8 | 9 | `fix` Fixed a probable endless redirection. 10 | `add` Added support for IBM WSSC (a WebSphere product that acts as a reverse proxy). 11 | `add` Added cURL timeouts for OpenID. 12 | 13 | ## v1.2.0 (January 14, 2014) 14 | 15 | `fix` Yahoo OpenID not working on newer versions of PHP/cURL. 16 | `fix` A warning when the realm is shorter than 8 chars. 17 | `fix` Use cURL when 'allow_url_fopen' is disabled. 18 | `fix` Different POST-behavior when using cURL and streams. 19 | `fix` Avoid hiding the real presence/absence of fields. 20 | `add` Added User-Agent header for better compatibility. 21 | `add` Added 'text/html' to the discovery content-types. 22 | `add` Added support for Composer (dependency manager). 23 | `add` Added ability to set the 'CN_match' SSL option. 24 | 25 | 26 | ## v1.1.2 (January 15, 2013) 27 | 28 | `fix` Fixed a bug in the proxy configuration. 29 | 30 | 31 | ## v1.1.1 (December 21, 2012) 32 | 33 | `add` Added support for overriding the initial URL XRDS lookup. 34 | 35 | 36 | ## v1.1.0 (December 02, 2012) 37 | 38 | `add` Added support for connecting through a proxy. 39 | `add` Added support for an OpenID+OAuth hybrid protocol. 40 | `add` Added SSL-validation support for HEAD-requests. 41 | `fix` Fixed a bug in the attribute exchange implementation. 42 | `fix` Fixed a bug in stream defaults after a HEAD request. 43 | 44 | 45 | ## v1.0.0 (June 08, 2012) 46 | `fix` Fixed a bug causing validation failure when using streams. 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Mewp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **NOTICE** 2 | 3 | > I am no longer able to support or maintain this project - if you would like to take over the project, please drop me a line. 4 | 5 | # LightOpenID 6 | 7 | Lightweight PHP5 library for easy OpenID authentication. 8 | 9 | * `Version....:` [**1.3.1** :arrow_double_down:][1] 10 | ( *see [the change log][2] for details* ) 11 | * `Released on:` March 04, 2016 12 | * `Source code:` [Official GitHub Repo :octocat:][3] 13 | * `Homepage...:` http://code.google.com/p/lightopenid/ 14 | * `Author.....:` [Mewp][4] 15 | 16 | [1]: https://github.com/iignatov/LightOpenID/archive/master.zip 17 | [2]: https://github.com/iignatov/LightOpenID/blob/master/CHANGELOG.md 18 | [3]: https://github.com/Mewp/lightopenid 19 | [4]: https://github.com/Mewp 20 | 21 | 22 | ## Quick start 23 | 24 | ### Add to composer.json 25 | 26 | ```javascript 27 | "repositories": [ 28 | { 29 | "type": "vcs", 30 | "url": "https://github.com/iignatov/LightOpenID" 31 | } 32 | ], 33 | 34 | "require": { 35 | "php": ">=5.4.0", 36 | "iignatov/lightopenid": "*" 37 | } 38 | ``` 39 | 40 | ### Sign-on with OpenID in just 2 steps: 41 | 42 | 1. Authentication with the provider: 43 | 44 | ```php 45 | $openid = new LightOpenID('my-host.example.org'); 46 | 47 | $openid->identity = 'ID supplied by user'; 48 | 49 | header('Location: ' . $openid->authUrl()); 50 | ``` 51 | 2. Verification: 52 | 53 | ```php 54 | $openid = new LightOpenID('my-host.example.org'); 55 | 56 | if ($openid->mode) { 57 | echo $openid->validate() ? 'Logged in.' : 'Failed!'; 58 | } 59 | ``` 60 | 61 | ### Support for AX and SREG extensions: 62 | 63 | To use the AX and SREG extensions, specify `$openid->required` and/or `$openid->optional` 64 | before calling `$openid->authUrl()`. These are arrays, with values being AX schema paths 65 | (the 'path' part of the URL). For example: 66 | 67 | ```php 68 | $openid->required = array('namePerson/friendly', 'contact/email'); 69 | $openid->optional = array('namePerson/first'); 70 | ``` 71 | 72 | Note that if the server supports only SREG or OpenID 1.1, these are automaticaly mapped 73 | to SREG names. To get the values use: 74 | 75 | ```php 76 | $openid->getAttributes(); 77 | ``` 78 | 79 | For more information see [USAGE.md](http://github.com/iignatov/LightOpenID/blob/master/USAGE.md). 80 | 81 | 82 | ## Requirements 83 | 84 | This library requires PHP >= 5.1.2 with cURL or HTTP/HTTPS stream wrappers enabled. 85 | 86 | 87 | ## Features 88 | 89 | * Easy to use - you can code a functional client in less than ten lines of code. 90 | * Uses cURL if avaiable, PHP-streams otherwise. 91 | * Supports both OpenID 1.1 and 2.0. 92 | * Supports Yadis discovery. 93 | * Supports only stateless/dumb protocol. 94 | * Works with PHP >= 5. 95 | * Generates no errors with `error_reporting(E_ALL | E_STRICT)`. 96 | 97 | 98 | ## Links 99 | 100 | * [JavaScript OpenID Selector](http://code.google.com/p/openid-selector/) - 101 | simple user interface that can be used with LightOpenID. 102 | * [HybridAuth](http://hybridauth.sourceforge.net/) - 103 | easy to install and use social sign on PHP library, which uses LightOpenID. 104 | * [OpenID Dev Specifications](http://openid.net/developers/specs/) - 105 | documentation for the OpenID extensions and related topics. 106 | 107 | 108 | ## License 109 | 110 | [LightOpenID](http://github.com/iignatov/LightOpenID) 111 | is an open source software available under the 112 | [MIT License](http://opensource.org/licenses/mit-license.php). 113 | -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | # LightOpenID Quick Start 2 | 3 | 4 | ## Sign-on with OpenID is a two step process: 5 | 6 | 1. Step one is authentication with the provider: 7 | 8 | ```php 9 | $openid = new LightOpenID('my-host.example.org'); 10 | 11 | $openid->identity = 'ID supplied by the user'; 12 | 13 | header('Location: ' . $openid->authUrl()); 14 | ``` 15 | 16 | The provider then sends various parameters via GET, one of which is `openid_mode`. 17 | 18 | 2. Step two is verification: 19 | 20 | ```php 21 | $openid = new LightOpenID('my-host.example.org'); 22 | 23 | if ($openid->mode) { 24 | echo $openid->validate() ? 'Logged in.' : 'Failed!'; 25 | } 26 | ``` 27 | 28 | 29 | ### Notes: 30 | 31 | Change 'my-host.example.org' to your domain name. Do NOT use `$_SERVER['HTTP_HOST']` 32 | for that, unless you know what you're doing. 33 | 34 | Optionally, you can set `$returnUrl` and `$realm` (or `$trustRoot`, which is an alias). 35 | The default values for those are: 36 | 37 | ```php 38 | $openid->realm = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST']; 39 | $openid->returnUrl = $openid->realm . $_SERVER['REQUEST_URI']; 40 | ``` 41 | 42 | If you don't know their meaning, refer to any OpenID tutorial, or specification. 43 | 44 | 45 | ## Basic configuration options: 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 74 | 75 | 76 | 77 | 81 | 82 | 83 | 84 | 88 | 89 | 90 | 91 | 96 | 97 |
namedescription
identity 55 | Sets (or gets) the identity supplied by an user. Set it 56 | before calling authUrl(), and get after validate(). 57 |
returnUrl 62 | Users will be redirected to this url after they complete 63 | authentication with their provider. Default: current url. 64 |
realm 69 | The realm user is signing into. Providers usually say 70 | "You are sgning into $realm". Must be in the same domain 71 | as returnUrl. Usually, this should be the host part of 72 | your site's url. And that's the default. 73 |
required and optional 78 | Attempts to fetch more information about an user. 79 | See Common AX attributes. 80 |
verify_peer 85 | When using https, attempts to verify peer's certificate. 86 | See CURLOPT_SSL_VERIFYPEER. 87 |
cainfo and capath 92 | When verify_peer is true, sets the CA info file and directory. 93 | See CURLOPT_SSL_CAINFO 94 | and CURLOPT_SSL_CAPATH. 95 |
98 | 99 | 100 | ## AX and SREG extensions are supported: 101 | 102 | To use them, specify `$openid->required` and/or `$openid->optional` before calling 103 | `$openid->authUrl()`. These are arrays, with values being AX schema paths (the 'path' 104 | part of the URL). For example: 105 | 106 | ```php 107 | $openid->required = array('namePerson/friendly', 'contact/email'); 108 | $openid->optional = array('namePerson/first'); 109 | ``` 110 | 111 | Note that if the server supports only SREG or OpenID 1.1, these are automaticaly mapped 112 | to SREG names, so that user doesn't have to know anything about the server. 113 | To get the values, use `$openid->getAttributes()`. 114 | 115 | 116 | ### Common AX attributes 117 | 118 | Here is a list of the more common AX attributes (from [axschema.org](http://www.axschema.org/types/)): 119 | 120 | Name | Meaning 121 | ------------------------|--------------- 122 | namePerson/friendly | Alias/Username 123 | contact/email | Email 124 | namePerson | Full name 125 | birthDate | Birth date 126 | person/gender | Gender 127 | contact/postalCode/home | Postal code 128 | contact/country/home | Country 129 | pref/language | Language 130 | pref/timezone | Time zone 131 | 132 | Note that even if you mark some field as required, there is no guarantee that you'll get any 133 | information from a provider. Not all providers support all of these attributes, and some don't 134 | support these extensions at all. 135 | 136 | Google, for example, completely ignores optional parameters, and for the required ones, it supports, 137 | according to [it's website](http://code.google.com/apis/accounts/docs/OpenID.html): 138 | 139 | * namePerson/first (first name) 140 | * namePerson/last (last name) 141 | * contact/country/home 142 | * contact/email 143 | * pref/language 144 | 145 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iignatov/lightopenid", 3 | "type": "library", 4 | "description": "Lightweight PHP5 library for easy OpenID authentication.", 5 | "keywords": ["openid", "authentication", "security"], 6 | "homepage": "https://github.com/iignatov/LightOpenID", 7 | "license": "MIT", 8 | "version": "1.3.1", 9 | "authors": [ 10 | { 11 | "name": "Mewp", 12 | "homepage": "https://code.google.com/p/lightopenid/" 13 | }, 14 | { 15 | "name": "Ignat Ignatov", 16 | "homepage": "https://github.com/iignatov/LightOpenID" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=5.2" 21 | }, 22 | "autoload": { 23 | "classmap": ["openid.php", "provider/provider.php"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/example-google.php: -------------------------------------------------------------------------------- 1 | mode) { 12 | if (isset($_GET['login'])) { 13 | $openid->identity = 'https://www.google.com/accounts/o8/id'; 14 | header('Location: ' . $openid->authUrl()); 15 | } 16 | ?> 17 |
18 | 19 |
20 | mode == 'cancel') { 22 | echo 'User has canceled authentication!'; 23 | } else { 24 | echo 'User ' . ($openid->validate() ? $openid->identity . ' has ' : 'has not ') . 'logged in.'; 25 | } 26 | } catch(ErrorException $e) { 27 | echo $e->getMessage(); 28 | } 29 | -------------------------------------------------------------------------------- /examples/example-google_apps.php: -------------------------------------------------------------------------------- 1 | xrdsOverride = array( 11 | '#^http://' . $domain . '/openid\?id=\d+$#', 12 | 'https://www.google.com/accounts/o8/site-xrds?hd=' . $domain 13 | ); 14 | 15 | if (!$openid->mode) { 16 | if (isset($_GET['login'])) { 17 | $openid->identity = 'https://www.google.com/accounts/o8/site-xrds?hd=' . $domain; 18 | header('Location: ' . $openid->authUrl()); 19 | } 20 | ?> 21 |
22 | 23 |
24 | mode == 'cancel') { 26 | echo 'User has canceled authentication!'; 27 | } else { 28 | echo 'User ' . ($openid->validate() ? $openid->identity . ' has ' : 'has not ') . 'logged in.'; 29 | } 30 | } catch(ErrorException $e) { 31 | echo $e->getMessage(); 32 | } 33 | -------------------------------------------------------------------------------- /examples/example.php: -------------------------------------------------------------------------------- 1 | mode) { 7 | if(isset($_POST['openid_identifier'])) { 8 | $openid->identity = $_POST['openid_identifier']; 9 | # The following two lines request email, full name, and a nickname 10 | # from the provider. Remove them if you don't need that data. 11 | $openid->required = array('contact/email'); 12 | $openid->optional = array('namePerson', 'namePerson/friendly'); 13 | header('Location: ' . $openid->authUrl()); 14 | } 15 | ?> 16 |
17 | OpenID: 18 |
19 | mode == 'cancel') { 21 | echo 'User has canceled authentication!'; 22 | } else { 23 | echo 'User ' . ($openid->validate() ? $openid->identity . ' has ' : 'has not ') . 'logged in.'; 24 | print_r($openid->getAttributes()); 25 | } 26 | } catch(ErrorException $e) { 27 | echo $e->getMessage(); 28 | } 29 | -------------------------------------------------------------------------------- /openid.php: -------------------------------------------------------------------------------- 1 | = 5.1.2 with cURL or HTTP/HTTPS stream wrappers enabled. 6 | * 7 | * @version v1.3.1 (2016-03-04) 8 | * @link https://code.google.com/p/lightopenid/ Project URL 9 | * @link https://github.com/iignatov/LightOpenID GitHub Repo 10 | * @author Mewp 11 | * @copyright Copyright (c) 2013 Mewp 12 | * @license http://opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | class LightOpenID 15 | { 16 | public $returnUrl 17 | , $required = array() 18 | , $optional = array() 19 | , $verify_peer = null 20 | , $capath = null 21 | , $cainfo = null 22 | , $cnmatch = null 23 | , $data 24 | , $oauth = array() 25 | , $curl_time_out = 30 // in seconds 26 | , $curl_connect_time_out = 30; // in seconds 27 | private $identity, $claimed_id; 28 | protected $server, $version, $trustRoot, $aliases, $identifier_select = false 29 | , $ax = false, $sreg = false, $setup_url = null, $headers = array() 30 | , $proxy = null, $user_agent = 'LightOpenID' 31 | , $xrds_override_pattern = null, $xrds_override_replacement = null; 32 | static protected $ax_to_sreg = array( 33 | 'namePerson/friendly' => 'nickname', 34 | 'contact/email' => 'email', 35 | 'namePerson' => 'fullname', 36 | 'birthDate' => 'dob', 37 | 'person/gender' => 'gender', 38 | 'contact/postalCode/home' => 'postcode', 39 | 'contact/country/home' => 'country', 40 | 'pref/language' => 'language', 41 | 'pref/timezone' => 'timezone', 42 | ); 43 | 44 | function __construct($host, $proxy = null) 45 | { 46 | $this->set_realm($host); 47 | $this->set_proxy($proxy); 48 | 49 | $uri = rtrim(preg_replace('#((?<=\?)|&)openid\.[^&]+#', '', $_SERVER['REQUEST_URI']), '?'); 50 | $this->returnUrl = $this->trustRoot . $uri; 51 | 52 | $this->data = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : $_GET; 53 | 54 | if(!function_exists('curl_init') && !in_array('https', stream_get_wrappers())) { 55 | throw new ErrorException('You must have either https wrappers or curl enabled.'); 56 | } 57 | } 58 | 59 | function __isset($name) 60 | { 61 | return in_array($name, array('identity', 'trustRoot', 'realm', 'xrdsOverride', 'mode')); 62 | } 63 | 64 | function __set($name, $value) 65 | { 66 | switch ($name) { 67 | case 'identity': 68 | if (strlen($value = trim((String) $value))) { 69 | if (preg_match('#^xri:/*#i', $value, $m)) { 70 | $value = substr($value, strlen($m[0])); 71 | } elseif (!preg_match('/^(?:[=@+\$!\(]|https?:)/i', $value)) { 72 | $value = "http://$value"; 73 | } 74 | if (preg_match('#^https?://[^/]+$#i', $value, $m)) { 75 | $value .= '/'; 76 | } 77 | } 78 | $this->$name = $this->claimed_id = $value; 79 | break; 80 | case 'trustRoot': 81 | case 'realm': 82 | $this->trustRoot = trim($value); 83 | break; 84 | case 'xrdsOverride': 85 | if (is_array($value)) { 86 | list($pattern, $replacement) = $value; 87 | $this->xrds_override_pattern = $pattern; 88 | $this->xrds_override_replacement = $replacement; 89 | } else { 90 | trigger_error('Invalid value specified for "xrdsOverride".', E_USER_ERROR); 91 | } 92 | break; 93 | } 94 | } 95 | 96 | function __get($name) 97 | { 98 | switch ($name) { 99 | case 'identity': 100 | # We return claimed_id instead of identity, 101 | # because the developer should see the claimed identifier, 102 | # i.e. what he set as identity, not the op-local identifier (which is what we verify) 103 | return $this->claimed_id; 104 | case 'trustRoot': 105 | case 'realm': 106 | return $this->trustRoot; 107 | case 'mode': 108 | return empty($this->data['openid_mode']) ? null : $this->data['openid_mode']; 109 | } 110 | } 111 | 112 | function set_proxy($proxy) 113 | { 114 | if (!empty($proxy)) { 115 | // When the proxy is a string - try to parse it. 116 | if (!is_array($proxy)) { 117 | $proxy = parse_url($proxy); 118 | } 119 | 120 | // Check if $proxy is valid after the parsing. 121 | if ($proxy && !empty($proxy['host'])) { 122 | // Make sure that a valid port number is specified. 123 | if (array_key_exists('port', $proxy)) { 124 | if (!is_int($proxy['port'])) { 125 | $proxy['port'] = is_numeric($proxy['port']) ? intval($proxy['port']) : 0; 126 | } 127 | 128 | if ($proxy['port'] <= 0) { 129 | throw new ErrorException('The specified proxy port number is invalid.'); 130 | } 131 | } 132 | 133 | $this->proxy = $proxy; 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * Checks if the server specified in the url exists. 140 | * 141 | * @param $url url to check 142 | * @return true, if the server exists; false otherwise 143 | */ 144 | function hostExists($url) 145 | { 146 | if (strpos($url, '/') === false) { 147 | $server = $url; 148 | } else { 149 | $server = @parse_url($url, PHP_URL_HOST); 150 | } 151 | 152 | if (!$server) { 153 | return false; 154 | } 155 | 156 | return !!gethostbynamel($server); 157 | } 158 | 159 | protected function set_realm($uri) 160 | { 161 | $realm = ''; 162 | 163 | # Set a protocol, if not specified. 164 | $realm .= (($offset = strpos($uri, '://')) === false) ? $this->get_realm_protocol() : ''; 165 | 166 | # Set the offset properly. 167 | $offset = (($offset !== false) ? $offset + 3 : 0); 168 | 169 | # Get only the root, without the path. 170 | $realm .= (($end = strpos($uri, '/', $offset)) === false) ? $uri : substr($uri, 0, $end); 171 | 172 | $this->trustRoot = $realm; 173 | } 174 | 175 | protected function get_realm_protocol() 176 | { 177 | if (!empty($_SERVER['HTTPS'])) { 178 | $use_secure_protocol = ($_SERVER['HTTPS'] != 'off'); 179 | } else if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { 180 | $use_secure_protocol = ($_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'); 181 | } else if (isset($_SERVER['HTTP__WSSC'])) { 182 | $use_secure_protocol = ($_SERVER['HTTP__WSSC'] == 'https'); 183 | } else { 184 | $use_secure_protocol = false; 185 | } 186 | 187 | return $use_secure_protocol ? 'https://' : 'http://'; 188 | } 189 | 190 | protected function request_curl($url, $method='GET', $params=array(), $update_claimed_id) 191 | { 192 | $params = http_build_query($params, '', '&'); 193 | $curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : '')); 194 | curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); 195 | curl_setopt($curl, CURLOPT_HEADER, false); 196 | curl_setopt($curl, CURLOPT_USERAGENT, $this->user_agent); 197 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); 198 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 199 | 200 | if ($method == 'POST') { 201 | curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-type: application/x-www-form-urlencoded')); 202 | } else { 203 | curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/xrds+xml, */*')); 204 | } 205 | 206 | curl_setopt($curl, CURLOPT_TIMEOUT, $this->curl_time_out); // defaults to infinite 207 | curl_setopt($curl, CURLOPT_CONNECTTIMEOUT , $this->curl_connect_time_out); // defaults to 300s 208 | 209 | if (!empty($this->proxy)) { 210 | curl_setopt($curl, CURLOPT_PROXY, $this->proxy['host']); 211 | 212 | if (!empty($this->proxy['port'])) { 213 | curl_setopt($curl, CURLOPT_PROXYPORT, $this->proxy['port']); 214 | } 215 | 216 | if (!empty($this->proxy['user'])) { 217 | curl_setopt($curl, CURLOPT_PROXYUSERPWD, $this->proxy['user'] . ':' . $this->proxy['pass']); 218 | } 219 | } 220 | 221 | if($this->verify_peer !== null) { 222 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer); 223 | if($this->capath) { 224 | curl_setopt($curl, CURLOPT_CAPATH, $this->capath); 225 | } 226 | 227 | if($this->cainfo) { 228 | curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo); 229 | } 230 | } 231 | 232 | if ($method == 'POST') { 233 | curl_setopt($curl, CURLOPT_POST, true); 234 | curl_setopt($curl, CURLOPT_POSTFIELDS, $params); 235 | } elseif ($method == 'HEAD') { 236 | curl_setopt($curl, CURLOPT_HEADER, true); 237 | curl_setopt($curl, CURLOPT_NOBODY, true); 238 | } else { 239 | curl_setopt($curl, CURLOPT_HEADER, true); 240 | curl_setopt($curl, CURLOPT_HTTPGET, true); 241 | } 242 | $response = curl_exec($curl); 243 | 244 | if($method == 'HEAD' && curl_getinfo($curl, CURLINFO_HTTP_CODE) == 405) { 245 | curl_setopt($curl, CURLOPT_HTTPGET, true); 246 | $response = curl_exec($curl); 247 | $response = substr($response, 0, strpos($response, "\r\n\r\n")); 248 | } 249 | 250 | if($method == 'HEAD' || $method == 'GET') { 251 | $header_response = $response; 252 | 253 | # If it's a GET request, we want to only parse the header part. 254 | if($method == 'GET') { 255 | $header_response = substr($response, 0, strpos($response, "\r\n\r\n")); 256 | } 257 | 258 | $headers = array(); 259 | foreach(explode("\n", $header_response) as $header) { 260 | $pos = strpos($header,':'); 261 | if ($pos !== false) { 262 | $name = strtolower(trim(substr($header, 0, $pos))); 263 | $headers[$name] = trim(substr($header, $pos+1)); 264 | } 265 | } 266 | 267 | if($update_claimed_id) { 268 | # Update the claimed_id value in case of redirections. 269 | $effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); 270 | # Ignore the fragment (some cURL versions don't handle it well). 271 | if (strtok($effective_url, '#') != strtok($url, '#')) { 272 | $this->identity = $this->claimed_id = $effective_url; 273 | } 274 | } 275 | 276 | if($method == 'HEAD') { 277 | return $headers; 278 | } else { 279 | $this->headers = $headers; 280 | } 281 | } 282 | 283 | if (curl_errno($curl)) { 284 | throw new ErrorException(curl_error($curl), curl_errno($curl)); 285 | } 286 | 287 | return $response; 288 | } 289 | 290 | protected function parse_header_array($array, $update_claimed_id) 291 | { 292 | $headers = array(); 293 | foreach($array as $header) { 294 | $pos = strpos($header,':'); 295 | if ($pos !== false) { 296 | $name = strtolower(trim(substr($header, 0, $pos))); 297 | $headers[$name] = trim(substr($header, $pos+1)); 298 | 299 | # Following possible redirections. The point is just to have 300 | # claimed_id change with them, because the redirections 301 | # are followed automatically. 302 | # We ignore redirections with relative paths. 303 | # If any known provider uses them, file a bug report. 304 | if($name == 'location' && $update_claimed_id) { 305 | if(strpos($headers[$name], 'http') === 0) { 306 | $this->identity = $this->claimed_id = $headers[$name]; 307 | } elseif($headers[$name][0] == '/') { 308 | $parsed_url = parse_url($this->claimed_id); 309 | $this->identity = 310 | $this->claimed_id = $parsed_url['scheme'] . '://' 311 | . $parsed_url['host'] 312 | . $headers[$name]; 313 | } 314 | } 315 | } 316 | } 317 | return $headers; 318 | } 319 | 320 | protected function request_streams($url, $method='GET', $params=array(), $update_claimed_id) 321 | { 322 | if(!$this->hostExists($url)) { 323 | throw new ErrorException("Could not connect to $url.", 404); 324 | } 325 | 326 | if (empty($this->cnmatch)) { 327 | $this->cnmatch = parse_url($url, PHP_URL_HOST); 328 | } 329 | 330 | $params = http_build_query($params, '', '&'); 331 | switch($method) { 332 | case 'GET': 333 | $opts = array( 334 | 'http' => array( 335 | 'method' => 'GET', 336 | 'header' => 'Accept: application/xrds+xml, */*', 337 | 'user_agent' => $this->user_agent, 338 | 'ignore_errors' => true, 339 | ), 340 | 'ssl' => array( 341 | 'CN_match' => $this->cnmatch 342 | ) 343 | ); 344 | $url = $url . ($params ? '?' . $params : ''); 345 | if (!empty($this->proxy)) { 346 | $opts['http']['proxy'] = $this->proxy_url(); 347 | } 348 | break; 349 | case 'POST': 350 | $opts = array( 351 | 'http' => array( 352 | 'method' => 'POST', 353 | 'header' => 'Content-type: application/x-www-form-urlencoded', 354 | 'user_agent' => $this->user_agent, 355 | 'content' => $params, 356 | 'ignore_errors' => true, 357 | ), 358 | 'ssl' => array( 359 | 'CN_match' => $this->cnmatch 360 | ) 361 | ); 362 | if (!empty($this->proxy)) { 363 | $opts['http']['proxy'] = $this->proxy_url(); 364 | } 365 | break; 366 | case 'HEAD': 367 | // We want to send a HEAD request, but since get_headers() doesn't 368 | // accept $context parameter, we have to change the defaults. 369 | $default = stream_context_get_options(stream_context_get_default()); 370 | 371 | // PHP does not reset all options. Instead, it just sets the options 372 | // available in the passed array, therefore set the defaults manually. 373 | $default += array( 374 | 'http' => array(), 375 | 'ssl' => array() 376 | ); 377 | $default['http'] += array( 378 | 'method' => 'GET', 379 | 'header' => '', 380 | 'user_agent' => '', 381 | 'ignore_errors' => false 382 | ); 383 | $default['ssl'] += array( 384 | 'CN_match' => '' 385 | ); 386 | 387 | $opts = array( 388 | 'http' => array( 389 | 'method' => 'HEAD', 390 | 'header' => 'Accept: application/xrds+xml, */*', 391 | 'user_agent' => $this->user_agent, 392 | 'ignore_errors' => true, 393 | ), 394 | 'ssl' => array( 395 | 'CN_match' => $this->cnmatch 396 | ) 397 | ); 398 | 399 | // Enable validation of the SSL certificates. 400 | if ($this->verify_peer) { 401 | $default['ssl'] += array( 402 | 'verify_peer' => false, 403 | 'capath' => '', 404 | 'cafile' => '' 405 | ); 406 | $opts['ssl'] += array( 407 | 'verify_peer' => true, 408 | 'capath' => $this->capath, 409 | 'cafile' => $this->cainfo 410 | ); 411 | } 412 | 413 | // Change the stream context options. 414 | stream_context_get_default($opts); 415 | 416 | $headers = get_headers($url . ($params ? '?' . $params : '')); 417 | 418 | // Restore the stream context options. 419 | stream_context_get_default($default); 420 | 421 | if (!empty($headers)) { 422 | if (intval(substr($headers[0], strlen('HTTP/1.1 '))) == 405) { 423 | // The server doesn't support HEAD - emulate it with a GET. 424 | $args = func_get_args(); 425 | $args[1] = 'GET'; 426 | call_user_func_array(array($this, 'request_streams'), $args); 427 | $headers = $this->headers; 428 | } else { 429 | $headers = $this->parse_header_array($headers, $update_claimed_id); 430 | } 431 | } else { 432 | $headers = array(); 433 | } 434 | 435 | return $headers; 436 | } 437 | 438 | if ($this->verify_peer) { 439 | $opts['ssl'] += array( 440 | 'verify_peer' => true, 441 | 'capath' => $this->capath, 442 | 'cafile' => $this->cainfo 443 | ); 444 | } 445 | 446 | $context = stream_context_create ($opts); 447 | $data = file_get_contents($url, false, $context); 448 | # This is a hack for providers who don't support HEAD requests. 449 | # It just creates the headers array for the last request in $this->headers. 450 | if(isset($http_response_header)) { 451 | $this->headers = $this->parse_header_array($http_response_header, $update_claimed_id); 452 | } 453 | 454 | return $data; 455 | } 456 | 457 | protected function request($url, $method='GET', $params=array(), $update_claimed_id=false) 458 | { 459 | $use_curl = false; 460 | 461 | if (function_exists('curl_init')) { 462 | if (!$use_curl) { 463 | # When allow_url_fopen is disabled, PHP streams will not work. 464 | $use_curl = !ini_get('allow_url_fopen'); 465 | } 466 | 467 | if (!$use_curl) { 468 | # When there is no HTTPS wrapper, PHP streams cannott be used. 469 | $use_curl = !in_array('https', stream_get_wrappers()); 470 | } 471 | 472 | if (!$use_curl) { 473 | # With open_basedir or safe_mode set, cURL can't follow redirects. 474 | $use_curl = !(ini_get('safe_mode') || ini_get('open_basedir')); 475 | } 476 | } 477 | 478 | return 479 | $use_curl 480 | ? $this->request_curl($url, $method, $params, $update_claimed_id) 481 | : $this->request_streams($url, $method, $params, $update_claimed_id); 482 | } 483 | 484 | protected function proxy_url() 485 | { 486 | $result = ''; 487 | 488 | if (!empty($this->proxy)) { 489 | $result = $this->proxy['host']; 490 | 491 | if (!empty($this->proxy['port'])) { 492 | $result = $result . ':' . $this->proxy['port']; 493 | } 494 | 495 | if (!empty($this->proxy['user'])) { 496 | $result = $this->proxy['user'] . ':' . $this->proxy['pass'] . '@' . $result; 497 | } 498 | 499 | $result = 'http://' . $result; 500 | } 501 | 502 | return $result; 503 | } 504 | 505 | protected function build_url($url, $parts) 506 | { 507 | if (isset($url['query'], $parts['query'])) { 508 | $parts['query'] = $url['query'] . '&' . $parts['query']; 509 | } 510 | 511 | $url = $parts + $url; 512 | $url = $url['scheme'] . '://' 513 | . (empty($url['username'])?'' 514 | :(empty($url['password'])? "{$url['username']}@" 515 | :"{$url['username']}:{$url['password']}@")) 516 | . $url['host'] 517 | . (empty($url['port'])?'':":{$url['port']}") 518 | . (empty($url['path'])?'':$url['path']) 519 | . (empty($url['query'])?'':"?{$url['query']}") 520 | . (empty($url['fragment'])?'':"#{$url['fragment']}"); 521 | return $url; 522 | } 523 | 524 | /** 525 | * Helper function used to scan for / tags and extract information 526 | * from them 527 | */ 528 | protected function htmlTag($content, $tag, $attrName, $attrValue, $valueName) 529 | { 530 | preg_match_all("#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1); 531 | preg_match_all("#<{$tag}[^>]*$valueName=['\"](.+?)['\"][^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*/?>#i", $content, $matches2); 532 | 533 | $result = array_merge($matches1[1], $matches2[1]); 534 | return empty($result)?false:$result[0]; 535 | } 536 | 537 | /** 538 | * Performs Yadis and HTML discovery. Normally not used. 539 | * @param $url Identity URL. 540 | * @return String OP Endpoint (i.e. OpenID provider address). 541 | * @throws ErrorException 542 | */ 543 | function discover($url) 544 | { 545 | if (!$url) throw new ErrorException('No identity supplied.'); 546 | # Use xri.net proxy to resolve i-name identities 547 | if (!preg_match('#^https?:#', $url)) { 548 | $url = "https://xri.net/$url"; 549 | } 550 | 551 | # We save the original url in case of Yadis discovery failure. 552 | # It can happen when we'll be lead to an XRDS document 553 | # which does not have any OpenID2 services. 554 | $originalUrl = $url; 555 | 556 | # A flag to disable yadis discovery in case of failure in headers. 557 | $yadis = true; 558 | 559 | # Allows optional regex replacement of the URL, e.g. to use Google Apps 560 | # as an OpenID provider without setting up XRDS on the domain hosting. 561 | if (!is_null($this->xrds_override_pattern) && !is_null($this->xrds_override_replacement)) { 562 | $url = preg_replace($this->xrds_override_pattern, $this->xrds_override_replacement, $url); 563 | } 564 | 565 | # We'll jump a maximum of 5 times, to avoid endless redirections. 566 | for ($i = 0; $i < 5; $i ++) { 567 | if ($yadis) { 568 | $headers = $this->request($url, 'HEAD', array(), true); 569 | 570 | $next = false; 571 | if (isset($headers['x-xrds-location'])) { 572 | $url = $this->build_url(parse_url($url), parse_url(trim($headers['x-xrds-location']))); 573 | $next = true; 574 | } 575 | 576 | if (isset($headers['content-type']) && $this->is_allowed_type($headers['content-type'])) { 577 | # Found an XRDS document, now let's find the server, and optionally delegate. 578 | $content = $this->request($url, 'GET'); 579 | 580 | preg_match_all('#(.*?)#s', $content, $m); 581 | foreach($m[1] as $content) { 582 | $content = ' ' . $content; # The space is added, so that strpos doesn't return 0. 583 | 584 | # OpenID 2 585 | $ns = preg_quote('http://specs.openid.net/auth/2.0/', '#'); 586 | if(preg_match('#\s*'.$ns.'(server|signon)\s*#s', $content, $type)) { 587 | if ($type[1] == 'server') $this->identifier_select = true; 588 | 589 | preg_match('#(.*)#', $content, $server); 590 | preg_match('#<(Local|Canonical)ID>(.*)#', $content, $delegate); 591 | if (empty($server)) { 592 | return false; 593 | } 594 | # Does the server advertise support for either AX or SREG? 595 | $this->ax = (bool) strpos($content, 'http://openid.net/srv/ax/1.0'); 596 | $this->sreg = strpos($content, 'http://openid.net/sreg/1.0') 597 | || strpos($content, 'http://openid.net/extensions/sreg/1.1'); 598 | 599 | $server = $server[1]; 600 | if (isset($delegate[2])) $this->identity = trim($delegate[2]); 601 | $this->version = 2; 602 | 603 | $this->server = $server; 604 | return $server; 605 | } 606 | 607 | # OpenID 1.1 608 | $ns = preg_quote('http://openid.net/signon/1.1', '#'); 609 | if (preg_match('#\s*'.$ns.'\s*#s', $content)) { 610 | 611 | preg_match('#(.*)#', $content, $server); 612 | preg_match('#<.*?Delegate>(.*)#', $content, $delegate); 613 | if (empty($server)) { 614 | return false; 615 | } 616 | # AX can be used only with OpenID 2.0, so checking only SREG 617 | $this->sreg = strpos($content, 'http://openid.net/sreg/1.0') 618 | || strpos($content, 'http://openid.net/extensions/sreg/1.1'); 619 | 620 | $server = $server[1]; 621 | if (isset($delegate[1])) $this->identity = $delegate[1]; 622 | $this->version = 1; 623 | 624 | $this->server = $server; 625 | return $server; 626 | } 627 | } 628 | 629 | $next = true; 630 | $yadis = false; 631 | $url = $originalUrl; 632 | $content = null; 633 | break; 634 | } 635 | if ($next) continue; 636 | 637 | # There are no relevant information in headers, so we search the body. 638 | $content = $this->request($url, 'GET', array(), true); 639 | 640 | if (isset($this->headers['x-xrds-location'])) { 641 | $url = $this->build_url(parse_url($url), parse_url(trim($this->headers['x-xrds-location']))); 642 | continue; 643 | } 644 | 645 | $location = $this->htmlTag($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content'); 646 | if ($location) { 647 | $url = $this->build_url(parse_url($url), parse_url($location)); 648 | continue; 649 | } 650 | } 651 | 652 | if (!$content) $content = $this->request($url, 'GET'); 653 | 654 | # At this point, the YADIS Discovery has failed, so we'll switch 655 | # to openid2 HTML discovery, then fallback to openid 1.1 discovery. 656 | $server = $this->htmlTag($content, 'link', 'rel', 'openid2.provider', 'href'); 657 | $delegate = $this->htmlTag($content, 'link', 'rel', 'openid2.local_id', 'href'); 658 | $this->version = 2; 659 | 660 | if (!$server) { 661 | # The same with openid 1.1 662 | $server = $this->htmlTag($content, 'link', 'rel', 'openid.server', 'href'); 663 | $delegate = $this->htmlTag($content, 'link', 'rel', 'openid.delegate', 'href'); 664 | $this->version = 1; 665 | } 666 | 667 | if ($server) { 668 | # We found an OpenID2 OP Endpoint 669 | if ($delegate) { 670 | # We have also found an OP-Local ID. 671 | $this->identity = $delegate; 672 | } 673 | $this->server = $server; 674 | return $server; 675 | } 676 | 677 | throw new ErrorException("No OpenID Server found at $url", 404); 678 | } 679 | throw new ErrorException('Endless redirection!', 500); 680 | } 681 | 682 | protected function is_allowed_type($content_type) { 683 | # Apparently, some providers return XRDS documents as text/html. 684 | # While it is against the spec, allowing this here shouldn't break 685 | # compatibility with anything. 686 | $allowed_types = array('application/xrds+xml', 'text/xml'); 687 | 688 | # Only allow text/html content type for the Yahoo logins, since 689 | # it might cause an endless redirection for the other providers. 690 | if ($this->get_provider_name($this->claimed_id) == 'yahoo') { 691 | $allowed_types[] = 'text/html'; 692 | } 693 | 694 | foreach ($allowed_types as $type) { 695 | if (strpos($content_type, $type) !== false) { 696 | return true; 697 | } 698 | } 699 | 700 | return false; 701 | } 702 | 703 | protected function get_provider_name($provider_url) { 704 | $result = ''; 705 | 706 | if (!empty($provider_url)) { 707 | $tokens = array_reverse( 708 | explode('.', parse_url($provider_url, PHP_URL_HOST)) 709 | ); 710 | $result = strtolower( 711 | (count($tokens) > 1 && strlen($tokens[1]) > 3) 712 | ? $tokens[1] 713 | : (count($tokens) > 2 ? $tokens[2] : '') 714 | ); 715 | } 716 | 717 | return $result; 718 | } 719 | 720 | protected function sregParams() 721 | { 722 | $params = array(); 723 | # We always use SREG 1.1, even if the server is advertising only support for 1.0. 724 | # That's because it's fully backwards compatible with 1.0, and some providers 725 | # advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com 726 | $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1'; 727 | if ($this->required) { 728 | $params['openid.sreg.required'] = array(); 729 | foreach ($this->required as $required) { 730 | if (!isset(self::$ax_to_sreg[$required])) continue; 731 | $params['openid.sreg.required'][] = self::$ax_to_sreg[$required]; 732 | } 733 | $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']); 734 | } 735 | 736 | if ($this->optional) { 737 | $params['openid.sreg.optional'] = array(); 738 | foreach ($this->optional as $optional) { 739 | if (!isset(self::$ax_to_sreg[$optional])) continue; 740 | $params['openid.sreg.optional'][] = self::$ax_to_sreg[$optional]; 741 | } 742 | $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']); 743 | } 744 | return $params; 745 | } 746 | 747 | protected function axParams() 748 | { 749 | $params = array(); 750 | if ($this->required || $this->optional) { 751 | $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0'; 752 | $params['openid.ax.mode'] = 'fetch_request'; 753 | $this->aliases = array(); 754 | $counts = array(); 755 | $required = array(); 756 | $optional = array(); 757 | foreach (array('required','optional') as $type) { 758 | foreach ($this->$type as $alias => $field) { 759 | if (is_int($alias)) $alias = strtr($field, '/', '_'); 760 | $this->aliases[$alias] = 'http://axschema.org/' . $field; 761 | if (empty($counts[$alias])) $counts[$alias] = 0; 762 | $counts[$alias] += 1; 763 | ${$type}[] = $alias; 764 | } 765 | } 766 | foreach ($this->aliases as $alias => $ns) { 767 | $params['openid.ax.type.' . $alias] = $ns; 768 | } 769 | foreach ($counts as $alias => $count) { 770 | if ($count == 1) continue; 771 | $params['openid.ax.count.' . $alias] = $count; 772 | } 773 | 774 | # Don't send empty ax.required and ax.if_available. 775 | # Google and possibly other providers refuse to support ax when one of these is empty. 776 | if($required) { 777 | $params['openid.ax.required'] = implode(',', $required); 778 | } 779 | if($optional) { 780 | $params['openid.ax.if_available'] = implode(',', $optional); 781 | } 782 | } 783 | return $params; 784 | } 785 | 786 | protected function authUrl_v1($immediate) 787 | { 788 | $returnUrl = $this->returnUrl; 789 | # If we have an openid.delegate that is different from our claimed id, 790 | # we need to somehow preserve the claimed id between requests. 791 | # The simplest way is to just send it along with the return_to url. 792 | if($this->identity != $this->claimed_id) { 793 | $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->claimed_id; 794 | } 795 | 796 | $params = array( 797 | 'openid.return_to' => $returnUrl, 798 | 'openid.mode' => $immediate ? 'checkid_immediate' : 'checkid_setup', 799 | 'openid.identity' => $this->identity, 800 | 'openid.trust_root' => $this->trustRoot, 801 | ) + $this->sregParams(); 802 | 803 | return $this->build_url(parse_url($this->server) 804 | , array('query' => http_build_query($params, '', '&'))); 805 | } 806 | 807 | protected function authUrl_v2($immediate) 808 | { 809 | $params = array( 810 | 'openid.ns' => 'http://specs.openid.net/auth/2.0', 811 | 'openid.mode' => $immediate ? 'checkid_immediate' : 'checkid_setup', 812 | 'openid.return_to' => $this->returnUrl, 813 | 'openid.realm' => $this->trustRoot, 814 | ); 815 | 816 | if ($this->ax) { 817 | $params += $this->axParams(); 818 | } 819 | 820 | if ($this->sreg) { 821 | $params += $this->sregParams(); 822 | } 823 | 824 | if (!$this->ax && !$this->sreg) { 825 | # If OP doesn't advertise either SREG, nor AX, let's send them both 826 | # in worst case we don't get anything in return. 827 | $params += $this->axParams() + $this->sregParams(); 828 | } 829 | 830 | if (!empty($this->oauth) && is_array($this->oauth)) { 831 | $params['openid.ns.oauth'] = 'http://specs.openid.net/extensions/oauth/1.0'; 832 | $params['openid.oauth.consumer'] = str_replace(array('http://', 'https://'), '', $this->trustRoot); 833 | $params['openid.oauth.scope'] = implode(' ', $this->oauth); 834 | } 835 | 836 | if ($this->identifier_select) { 837 | $params['openid.identity'] = $params['openid.claimed_id'] 838 | = 'http://specs.openid.net/auth/2.0/identifier_select'; 839 | } else { 840 | $params['openid.identity'] = $this->identity; 841 | $params['openid.claimed_id'] = $this->claimed_id; 842 | } 843 | 844 | return $this->build_url(parse_url($this->server) 845 | , array('query' => http_build_query($params, '', '&'))); 846 | } 847 | 848 | /** 849 | * Returns authentication url. Usually, you want to redirect your user to it. 850 | * @return String The authentication url. 851 | * @param String $select_identifier Whether to request OP to select identity for an user in OpenID 2. Does not affect OpenID 1. 852 | * @throws ErrorException 853 | */ 854 | function authUrl($immediate = false) 855 | { 856 | if ($this->setup_url && !$immediate) return $this->setup_url; 857 | if (!$this->server) $this->discover($this->identity); 858 | 859 | if ($this->version == 2) { 860 | return $this->authUrl_v2($immediate); 861 | } 862 | return $this->authUrl_v1($immediate); 863 | } 864 | 865 | /** 866 | * Performs OpenID verification with the OP. 867 | * @return Bool Whether the verification was successful. 868 | * @throws ErrorException 869 | */ 870 | function validate() 871 | { 872 | # If the request was using immediate mode, a failure may be reported 873 | # by presenting user_setup_url (for 1.1) or reporting 874 | # mode 'setup_needed' (for 2.0). Also catching all modes other than 875 | # id_res, in order to avoid throwing errors. 876 | if(isset($this->data['openid_user_setup_url'])) { 877 | $this->setup_url = $this->data['openid_user_setup_url']; 878 | return false; 879 | } 880 | if($this->mode != 'id_res') { 881 | return false; 882 | } 883 | 884 | $this->claimed_id = isset($this->data['openid_claimed_id'])?$this->data['openid_claimed_id']:$this->data['openid_identity']; 885 | $params = array( 886 | 'openid.assoc_handle' => $this->data['openid_assoc_handle'], 887 | 'openid.signed' => $this->data['openid_signed'], 888 | 'openid.sig' => $this->data['openid_sig'], 889 | ); 890 | 891 | if (isset($this->data['openid_ns'])) { 892 | # We're dealing with an OpenID 2.0 server, so let's set an ns 893 | # Even though we should know location of the endpoint, 894 | # we still need to verify it by discovery, so $server is not set here 895 | $params['openid.ns'] = 'http://specs.openid.net/auth/2.0'; 896 | } elseif (isset($this->data['openid_claimed_id']) 897 | && $this->data['openid_claimed_id'] != $this->data['openid_identity'] 898 | ) { 899 | # If it's an OpenID 1 provider, and we've got claimed_id, 900 | # we have to append it to the returnUrl, like authUrl_v1 does. 901 | $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?') 902 | . 'openid.claimed_id=' . $this->claimed_id; 903 | } 904 | 905 | if ($this->data['openid_return_to'] != $this->returnUrl) { 906 | # The return_to url must match the url of current request. 907 | # I'm assuming that no one will set the returnUrl to something that doesn't make sense. 908 | return false; 909 | } 910 | 911 | $server = $this->discover($this->claimed_id); 912 | 913 | foreach (explode(',', $this->data['openid_signed']) as $item) { 914 | # Checking whether magic_quotes_gpc is turned on, because 915 | # the function may fail if it is. For example, when fetching 916 | # AX namePerson, it might contain an apostrophe, which will be escaped. 917 | # In such case, validation would fail, since we'd send different data than OP 918 | # wants to verify. stripslashes() should solve that problem, but we can't 919 | # use it when magic_quotes is off. 920 | $value = $this->data['openid_' . str_replace('.','_',$item)]; 921 | $params['openid.' . $item] = function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc() ? stripslashes($value) : $value; 922 | 923 | } 924 | 925 | $params['openid.mode'] = 'check_authentication'; 926 | 927 | $response = $this->request($server, 'POST', $params); 928 | 929 | return preg_match('/is_valid\s*:\s*true/i', $response); 930 | } 931 | 932 | protected function getAxAttributes() 933 | { 934 | $result = array(); 935 | 936 | if ($alias = $this->getNamespaceAlias('http://openid.net/srv/ax/1.0', 'ax')) { 937 | $prefix = 'openid_' . $alias; 938 | $length = strlen('http://axschema.org/'); 939 | 940 | foreach (explode(',', $this->data['openid_signed']) as $key) { 941 | $keyMatch = $alias . '.type.'; 942 | 943 | if (strncmp($key, $keyMatch, strlen($keyMatch)) !== 0) { 944 | continue; 945 | } 946 | 947 | $key = substr($key, strlen($keyMatch)); 948 | $idv = $prefix . '_value_' . $key; 949 | $idc = $prefix . '_count_' . $key; 950 | $key = substr($this->getItem($prefix . '_type_' . $key), $length); 951 | 952 | if (!empty($key)) { 953 | if (($count = intval($this->getItem($idc))) > 0) { 954 | $value = array(); 955 | 956 | for ($i = 1; $i <= $count; $i++) { 957 | $value[] = $this->getItem($idv . '_' . $i); 958 | } 959 | 960 | $value = ($count == 1) ? reset($value) : $value; 961 | } else { 962 | $value = $this->getItem($idv); 963 | } 964 | 965 | if (!is_null($value)) { 966 | $result[$key] = $value; 967 | } 968 | } 969 | } 970 | } else { 971 | // No alias for the AX schema has been found, 972 | // so there is no AX data in the OP's response. 973 | } 974 | 975 | return $result; 976 | } 977 | 978 | protected function getSregAttributes() 979 | { 980 | $attributes = array(); 981 | $sreg_to_ax = array_flip(self::$ax_to_sreg); 982 | foreach (explode(',', $this->data['openid_signed']) as $key) { 983 | $keyMatch = 'sreg.'; 984 | if (strncmp($key, $keyMatch, strlen($keyMatch)) !== 0) { 985 | continue; 986 | } 987 | $key = substr($key, strlen($keyMatch)); 988 | if (!isset($sreg_to_ax[$key])) { 989 | # The field name isn't part of the SREG spec, so we ignore it. 990 | continue; 991 | } 992 | $attributes[$sreg_to_ax[$key]] = $this->data['openid_sreg_' . $key]; 993 | } 994 | return $attributes; 995 | } 996 | 997 | /** 998 | * Gets AX/SREG attributes provided by OP. should be used only after successful validation. 999 | * Note that it does not guarantee that any of the required/optional parameters will be present, 1000 | * or that there will be no other attributes besides those specified. 1001 | * In other words. OP may provide whatever information it wants to. 1002 | * * SREG names will be mapped to AX names. 1003 | * * @return Array Array of attributes with keys being the AX schema names, e.g. 'contact/email' 1004 | * @see http://www.axschema.org/types/ 1005 | */ 1006 | function getAttributes() 1007 | { 1008 | if (isset($this->data['openid_ns']) 1009 | && $this->data['openid_ns'] == 'http://specs.openid.net/auth/2.0' 1010 | ) { # OpenID 2.0 1011 | # We search for both AX and SREG attributes, with AX taking precedence. 1012 | return $this->getAxAttributes() + $this->getSregAttributes(); 1013 | } 1014 | return $this->getSregAttributes(); 1015 | } 1016 | 1017 | /** 1018 | * Gets an OAuth request token if the OpenID+OAuth hybrid protocol has been used. 1019 | * 1020 | * In order to use the OpenID+OAuth hybrid protocol, you need to add at least one 1021 | * scope to the $openid->oauth array before you get the call to getAuthUrl(), e.g.: 1022 | * $openid->oauth[] = 'https://www.googleapis.com/auth/plus.me'; 1023 | * 1024 | * Furthermore the registered consumer name must fit the OpenID realm. 1025 | * To register an OpenID consumer at Google use: https://www.google.com/accounts/ManageDomains 1026 | * 1027 | * @return string|bool OAuth request token on success, FALSE if no token was provided. 1028 | */ 1029 | function getOAuthRequestToken() 1030 | { 1031 | $alias = $this->getNamespaceAlias('http://specs.openid.net/extensions/oauth/1.0'); 1032 | 1033 | return !empty($alias) ? $this->data['openid_' . $alias . '_request_token'] : false; 1034 | } 1035 | 1036 | /** 1037 | * Gets the alias for the specified namespace, if it's present. 1038 | * 1039 | * @param string $namespace The namespace for which an alias is needed. 1040 | * @param string $hint Common alias of this namespace, used for optimization. 1041 | * @return string|null The namespace alias if found, otherwise - NULL. 1042 | */ 1043 | private function getNamespaceAlias($namespace, $hint = null) 1044 | { 1045 | $result = null; 1046 | 1047 | if (empty($hint) || $this->getItem('openid_ns_' . $hint) != $namespace) { 1048 | // The common alias is either undefined or points to 1049 | // some other extension - search for another alias.. 1050 | $prefix = 'openid_ns_'; 1051 | $length = strlen($prefix); 1052 | 1053 | foreach ($this->data as $key => $val) { 1054 | if (strncmp($key, $prefix, $length) === 0 && $val === $namespace) { 1055 | $result = trim(substr($key, $length)); 1056 | break; 1057 | } 1058 | } 1059 | } else { 1060 | $result = $hint; 1061 | } 1062 | 1063 | return $result; 1064 | } 1065 | 1066 | /** 1067 | * Gets an item from the $data array by the specified id. 1068 | * 1069 | * @param string $id The id of the desired item. 1070 | * @return string|null The item if found, otherwise - NULL. 1071 | */ 1072 | private function getItem($id) 1073 | { 1074 | return isset($this->data[$id]) ? $this->data[$id] : null; 1075 | } 1076 | } 1077 | -------------------------------------------------------------------------------- /provider/example-mysql.php: -------------------------------------------------------------------------------- 1 | dh = false; 37 | * However, the latter one would disable stateful mode, unless connecting via HTTPS. 38 | */ 39 | require 'provider.php'; 40 | 41 | mysql_connect(); 42 | mysql_select_db('test'); 43 | 44 | function getUserData($handle=null) 45 | { 46 | if(isset($_POST['login'],$_POST['password'])) { 47 | $login = mysql_real_escape_string($_POST['login']); 48 | $password = sha1($_POST['password']); 49 | $q = mysql_query("SELECT * FROM Users WHERE login = '$login' AND password = '$password'"); 50 | if($data = mysql_fetch_assoc($q)) { 51 | return $data; 52 | } 53 | if($handle) { 54 | echo 'Wrong login/password.'; 55 | } 56 | } 57 | if($handle) { 58 | ?> 59 |
60 | 61 | Login:
62 | Password:
63 | 64 |
65 | 'First name', 74 | 'namePerson/last' => 'Last name', 75 | 'namePerson/friendly' => 'Nickname (login)' 76 | ); 77 | 78 | private $attrFieldMap = array( 79 | 'namePerson/first' => 'firstName', 80 | 'namePerson/last' => 'lastName', 81 | 'namePerson/friendly' => 'login' 82 | ); 83 | 84 | function setup($identity, $realm, $assoc_handle, $attributes) 85 | { 86 | $data = getUserData($assoc_handle); 87 | echo '
' 88 | . '' 89 | . '' 90 | . '' 91 | . "$realm wishes to authenticate you."; 92 | if($attributes['required'] || $attributes['optional']) { 93 | echo " It also requests following information (required fields marked with *):" 94 | . '
    '; 95 | 96 | foreach($attributes['required'] as $attr) { 97 | if(isset($this->attrMap[$attr])) { 98 | echo '
  • ' 99 | . ' ' 100 | . $this->attrMap[$attr] . '(*)
  • '; 101 | } 102 | } 103 | 104 | foreach($attributes['optional'] as $attr) { 105 | if(isset($this->attrMap[$attr])) { 106 | echo '
  • ' 107 | . ' ' 108 | . $this->attrMap[$attr] . '
  • '; 109 | } 110 | } 111 | echo '
'; 112 | } 113 | echo '
' 114 | . ' ' 115 | . ' ' 116 | . ' ' 117 | . '
'; 118 | } 119 | 120 | function checkid($realm, &$attributes) 121 | { 122 | if(isset($_POST['cancel'])) { 123 | $this->cancel(); 124 | } 125 | 126 | $data = getUserData(); 127 | if(!$data) { 128 | return false; 129 | } 130 | $realm = mysql_real_escape_string($realm); 131 | $q = mysql_query("SELECT attributes FROM AllowedSites WHERE user = '{$data['id']}' AND realm = '$realm'"); 132 | 133 | $attrs = array(); 134 | if($attrs = mysql_fetch_row($q)) { 135 | $attrs = explode(',', $attributes[0]); 136 | } elseif(isset($_POST['attributes'])) { 137 | $attrs = array_keys($_POST['attributes']); 138 | } elseif(!isset($_POST['once']) && !isset($_POST['always'])) { 139 | return false; 140 | } 141 | 142 | $attributes = array(); 143 | foreach($attrs as $attr) { 144 | if(isset($this->attrFieldMap[$attr])) { 145 | $attributes[$attr] = $data[$this->attrFieldMap[$attr]]; 146 | } 147 | } 148 | 149 | if(isset($_POST['always'])) { 150 | $attrs = mysql_real_escape_string(implode(',', array_keys($attributes))); 151 | mysql_query("REPLACE INTO AllowedSites VALUES('{$data['id']}', '$realm', '$attrs')"); 152 | } 153 | 154 | return $this->serverLocation . '?' . $data['login']; 155 | } 156 | 157 | function assoc_handle() 158 | { 159 | # We generate an integer assoc handle, because it's just faster to look up an integer later. 160 | $q = mysql_query("SELECT MAX(id) FROM Associations"); 161 | $result = mysql_fetch_row($q); 162 | return $q[0]+1; 163 | } 164 | 165 | function setAssoc($handle, $data) 166 | { 167 | $data = mysql_real_escape_string(serialize($data)); 168 | mysql_query("REPLACE INTO Associations VALUES('$handle', '$data')"); 169 | } 170 | 171 | function getAssoc($handle) 172 | { 173 | if(!is_numeric($handle)) { 174 | return false; 175 | } 176 | $q = mysql_query("SELECT data FROM Associations WHERE id = '$handle'"); 177 | $data = mysql_fetch_row($q); 178 | if(!$data) { 179 | return false; 180 | } 181 | return unserialize($data[0]); 182 | } 183 | 184 | function delAssoc($handle) 185 | { 186 | if(!is_numeric($handle)) { 187 | return false; 188 | } 189 | mysql_query("DELETE FROM Associations WHERE id = '$handle'"); 190 | } 191 | 192 | } 193 | $op = new MysqlProvider; 194 | $op->server(); 195 | -------------------------------------------------------------------------------- /provider/example.php: -------------------------------------------------------------------------------- 1 | select_id = false; 22 | } 23 | } 24 | 25 | function setup($identity, $realm, $assoc_handle, $attributes) 26 | { 27 | header('WWW-Authenticate: Basic realm="' . $this->data['openid_realm'] . '"'); 28 | header('HTTP/1.0 401 Unauthorized'); 29 | } 30 | 31 | function checkid($realm, &$attributes) 32 | { 33 | if(!isset($_SERVER['PHP_AUTH_USER'])) { 34 | return false; 35 | } 36 | 37 | if ($_SERVER['PHP_AUTH_USER'] == $this->login 38 | && $_SERVER['PHP_AUTH_PW'] == $this->password 39 | ) { 40 | # Returning identity 41 | # It can be any url that leads here, or to any other place that hosts 42 | # an XRDS document pointing here. 43 | return $this->serverLocation . '?id=' . $this->login; 44 | } 45 | 46 | return false; 47 | } 48 | 49 | } 50 | $op = new BasicProvider; 51 | $op->login = 'test'; 52 | $op->password = 'test'; 53 | $op->server(); 54 | -------------------------------------------------------------------------------- /provider/provider.php: -------------------------------------------------------------------------------- 1 | = 5.1.2 8 | * 9 | * This is an alpha version, using it in production code is not recommended, 10 | * until you are *sure* that it works and is secure. 11 | * 12 | * Please send me messages about your testing results 13 | * (even if successful, so I know that it has been tested). 14 | * Also, if you think there's a way to make it easier to use, tell me -- it's an alpha for a reason. 15 | * Same thing applies to bugs in code, suggestions, 16 | * and everything else you'd like to say about the library. 17 | * 18 | * There's no usage documentation here, see the examples. 19 | * 20 | * @author Mewp 21 | * @copyright Copyright (c) 2010, Mewp 22 | * @license http://www.opensource.org/licenses/mit-license.php MIT 23 | */ 24 | ini_set('error_log','log'); 25 | abstract class LightOpenIDProvider 26 | { 27 | # URL-s to XRDS and server location. 28 | public $xrdsLocation, $serverLocation; 29 | 30 | # Should we operate in server, or signon mode? 31 | public $select_id = false; 32 | 33 | # Lifetime of an association. 34 | protected $assoc_lifetime = 600; 35 | 36 | # Variables below are either set automatically, or are constant. 37 | # ----- 38 | # Can we support DH? 39 | protected $dh = true; 40 | protected $ns = 'http://specs.openid.net/auth/2.0'; 41 | protected $data, $assoc; 42 | 43 | # Default DH parameters as defined in the specification. 44 | protected $default_modulus; 45 | protected $default_gen = 'Ag=='; 46 | 47 | # AX <-> SREG transform 48 | protected $ax_to_sreg = array( 49 | 'namePerson/friendly' => 'nickname', 50 | 'contact/email' => 'email', 51 | 'namePerson' => 'fullname', 52 | 'birthDate' => 'dob', 53 | 'person/gender' => 'gender', 54 | 'contact/postalCode/home' => 'postcode', 55 | 'contact/country/home' => 'country', 56 | 'pref/language' => 'language', 57 | 'pref/timezone' => 'timezone', 58 | ); 59 | 60 | # Math 61 | private $add, $mul, $pow, $mod, $div, $powmod; 62 | # ----- 63 | 64 | # ------------------------------------------------------------------------ # 65 | # Functions you probably want to implement when extending the class. 66 | 67 | /** 68 | * Checks whether an user is authenticated. 69 | * The function should determine what fields it wants to send to the RP, 70 | * and put them in the $attributes array. 71 | * @param Array $attributes 72 | * @param String $realm Realm used for authentication. 73 | * @return String OP-local identifier of an authenticated user, or an empty value. 74 | */ 75 | abstract function checkid($realm, &$attributes); 76 | 77 | /** 78 | * Displays an user interface for inputting user's login and password. 79 | * Attributes are always AX field namespaces, with stripped host part. 80 | * For example, the $attributes array may be: 81 | * array( 'required' => array('namePerson/friendly', 'contact/email'), 82 | * 'optional' => array('pref/timezone', 'pref/language') 83 | * @param String $identity Discovered identity string. May be used to extract login, unless using $this->select_id 84 | * @param String $realm Realm used for authentication. 85 | * @param String Association handle. must be sent as openid.assoc_handle in $_GET or $_POST in subsequent requests. 86 | * @param Array User attributes requested by the RP. 87 | */ 88 | abstract function setup($identity, $realm, $assoc_handle, $attributes); 89 | 90 | /** 91 | * Stores an association. 92 | * If you want to use php sessions in your provider code, you have to replace it. 93 | * @param String $handle Association handle -- should be used as a key. 94 | * @param Array $assoc Association data. 95 | */ 96 | protected function setAssoc($handle, $assoc) 97 | { 98 | $oldSession = session_id(); 99 | session_commit(); 100 | session_id($assoc['handle']); 101 | session_start(); 102 | $_SESSION['assoc'] = $assoc; 103 | session_commit(); 104 | if($oldSession) { 105 | session_id($oldSession); 106 | session_start(); 107 | } 108 | } 109 | 110 | /** 111 | * Retreives association data. 112 | * If you want to use php sessions in your provider code, you have to replace it. 113 | * @param String $handle Association handle. 114 | * @return Array Association data. 115 | */ 116 | protected function getAssoc($handle) 117 | { 118 | $oldSession = session_id(); 119 | session_commit(); 120 | session_id($handle); 121 | session_start(); 122 | $assoc = null; 123 | if(!empty($_SESSION['assoc'])) { 124 | $assoc = $_SESSION['assoc']; 125 | } 126 | session_commit(); 127 | if($oldSession) { 128 | session_id($oldSession); 129 | session_start(); 130 | } 131 | return $assoc; 132 | } 133 | 134 | /** 135 | * Deletes an association. 136 | * If you want to use php sessions in your provider code, you have to replace it. 137 | * @param String $handle Association handle. 138 | */ 139 | protected function delAssoc($handle) 140 | { 141 | $oldSession = session_id(); 142 | session_commit(); 143 | session_id($handle); 144 | session_start(); 145 | session_destroy(); 146 | if($oldSession) { 147 | session_id($oldSession); 148 | session_start(); 149 | } 150 | } 151 | 152 | # ------------------------------------------------------------------------ # 153 | # Functions that you might want to implement. 154 | 155 | /** 156 | * Redirects the user to an url. 157 | * @param String $location The url that the user will be redirected to. 158 | */ 159 | protected function redirect($location) 160 | { 161 | header('Location: ' . $location); 162 | die(); 163 | } 164 | 165 | /** 166 | * Generates a new association handle. 167 | * @return string 168 | */ 169 | protected function assoc_handle() 170 | { 171 | return sha1(microtime()); 172 | } 173 | 174 | /** 175 | * Generates a random shared secret. 176 | * @return string 177 | */ 178 | protected function shared_secret($hash) 179 | { 180 | $length = 20; 181 | if($hash == 'sha256') { 182 | $length = 256; 183 | } 184 | 185 | $secret = ''; 186 | for($i = 0; $i < $length; $i++) { 187 | $secret .= mt_rand(0,255); 188 | } 189 | 190 | return $secret; 191 | } 192 | 193 | /** 194 | * Generates a private key. 195 | * @param int $length Length of the key. 196 | */ 197 | protected function keygen($length) 198 | { 199 | $key = ''; 200 | for($i = 1; $i < $length; $i++) { 201 | $key .= mt_rand(0,9); 202 | } 203 | $key .= mt_rand(1,9); 204 | 205 | return $key; 206 | } 207 | 208 | # ------------------------------------------------------------------------ # 209 | # Functions that you probably shouldn't touch. 210 | 211 | function __construct() 212 | { 213 | $this->default_modulus = 214 | 'ANz5OguIOXLsDhmYmsWizjEOHTdxfo2Vcbt2I3MYZuYe91ouJ4mLBX+YkcLiemOcPy' 215 | . 'm2CBRYHNOyyjmG0mg3BVd9RcLn5S3IHHoXGHblzqdLFEi/368Ygo79JRnxTkXjgmY0' 216 | . 'rxlJ5bU1zIKaSDuKdiI+XUkKJX8Fvf8W8vsixYOr'; 217 | 218 | $location = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' 219 | . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; 220 | $location = preg_replace('/\?.*/','',$location); 221 | $this->serverLocation = $location; 222 | $location .= (strpos($location, '?') ? '&' : '?') . 'xrds'; 223 | $this->xrdsLocation = $location; 224 | 225 | $this->data = $_GET + $_POST; 226 | 227 | # We choose GMP if avaiable, and bcmath otherwise 228 | if(function_exists('gmp_add')) { 229 | $this->add = 'gmp_add'; 230 | $this->mul = 'gmp_mul'; 231 | $this->pow = 'gmp_pow'; 232 | $this->mod = 'gmp_mod'; 233 | $this->div = 'gmp_div'; 234 | $this->powmod = 'gmp_powm'; 235 | } elseif(function_exists('bcadd')) { 236 | $this->add = 'bcadd'; 237 | $this->mul = 'bcmul'; 238 | $this->pow = 'bcpow'; 239 | $this->mod = 'bcmod'; 240 | $this->div = 'bcdiv'; 241 | $this->powmod = 'bcpowmod'; 242 | } else { 243 | # If neither are avaiable, we can't use DH 244 | $this->dh = false; 245 | } 246 | 247 | # However, we do require the hash functions. 248 | # They should be built-in anyway. 249 | if(!function_exists('hash_algos')) { 250 | $this->dh = false; 251 | } 252 | } 253 | 254 | /** 255 | * Displays an XRDS document, or redirects to it. 256 | * By default, it detects whether it should display or redirect automatically. 257 | * @param bool|null $force When true, always display the document, when false always redirect. 258 | */ 259 | function xrds($force=null) 260 | { 261 | if($force) { 262 | echo $this->xrdsContent(); 263 | die(); 264 | } elseif($force === false) { 265 | header('X-XRDS-Location: '. $this->xrdsLocation); 266 | return; 267 | } 268 | 269 | if (isset($_GET['xrds']) 270 | || (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/xrds+xml') !== false) 271 | ) { 272 | header('Content-Type: application/xrds+xml'); 273 | echo $this->xrdsContent(); 274 | die(); 275 | } 276 | 277 | header('X-XRDS-Location: ' . $this->xrdsLocation); 278 | } 279 | 280 | /** 281 | * Returns the content of the XRDS document 282 | * @return String The XRDS document. 283 | */ 284 | protected function xrdsContent() 285 | { 286 | $lines = array( 287 | '', 288 | '', 289 | '', 290 | ' ', 291 | ' ' . $this->ns . '/' . ($this->select_id ? 'server' : 'signon') .'', 292 | ' ' . $this->serverLocation . '', 293 | ' ', 294 | '', 295 | '' 296 | ); 297 | return implode("\n", $lines); 298 | } 299 | 300 | /** 301 | * Does everything that a provider has to -- in one function. 302 | */ 303 | function server() 304 | { 305 | if(isset($this->data['openid_assoc_handle'])) { 306 | $this->assoc = $this->getAssoc($this->data['openid_assoc_handle']); 307 | if(isset($this->assoc['data'])) { 308 | # We have additional data stored for setup. 309 | $this->data += $this->assoc['data']; 310 | unset($this->assoc['data']); 311 | } 312 | } 313 | 314 | if (isset($this->data['openid_ns']) 315 | && $this->data['openid_ns'] == $this->ns 316 | ) { 317 | if(!isset($this->data['openid_mode'])) $this->errorResponse(); 318 | 319 | switch($this->data['openid_mode']) 320 | { 321 | case 'checkid_immediate': 322 | case 'checkid_setup': 323 | $this->checkRealm(); 324 | # We support AX xor SREG. 325 | $attributes = $this->ax(); 326 | if(!$attributes) { 327 | $attributes = $this->sreg(); 328 | } 329 | 330 | # Even if some user is authenticated, we need to know if it's 331 | # the same one that want's to authenticate. 332 | # Of course, if we use select_id, we accept any user. 333 | if (($identity = $this->checkid($this->data['openid_realm'], $attrValues)) 334 | && ($this->select_id || $identity == $this->data['openid_identity']) 335 | ) { 336 | $this->positiveResponse($identity, $attrValues); 337 | } elseif($this->data['openid_mode'] == 'checkid_immediate') { 338 | $this->redirect($this->response(array('openid.mode' => 'setup_needed'))); 339 | } else { 340 | if(!$this->assoc) { 341 | $this->generateAssociation(); 342 | $this->assoc['private'] = true; 343 | } 344 | $this->assoc['data'] = $this->data; 345 | $this->setAssoc($this->assoc['handle'], $this->assoc); 346 | $this->setup($this->data['openid_identity'], 347 | $this->data['openid_realm'], 348 | $this->assoc['handle'], 349 | $attributes); 350 | } 351 | break; 352 | case 'associate': 353 | $this->associate(); 354 | break; 355 | case 'check_authentication': 356 | $this->checkRealm(); 357 | if($this->verify()) { 358 | echo "ns:$this->ns\nis_valid:true"; 359 | if(strpos($this->data['openid_signed'],'invalidate_handle') !== false) { 360 | echo "\ninvalidate_handle:" . $this->data['openid_invalidate_handle']; 361 | } 362 | } else { 363 | echo "ns:$this->ns\nis_valid:false"; 364 | } 365 | die(); 366 | break; 367 | default: 368 | $this->errorResponse(); 369 | } 370 | } else { 371 | $this->xrds(); 372 | } 373 | } 374 | 375 | protected function checkRealm() 376 | { 377 | if (!isset($this->data['openid_return_to'], $this->data['openid_realm'])) { 378 | $this->errorResponse(); 379 | } 380 | 381 | $realm = str_replace('\*', '[^/]', preg_quote($this->data['openid_realm'])); 382 | if(!preg_match("#^$realm#", $this->data['openid_return_to'])) { 383 | $this->errorResponse(); 384 | } 385 | } 386 | 387 | protected function ax() 388 | { 389 | # Namespace prefix that the fields must have. 390 | $ns = 'http://axschema.org/'; 391 | 392 | # First, we must find out what alias is used for AX. 393 | # Let's check the most likely one 394 | $alias = null; 395 | if (isset($this->data['openid_ns_ax']) 396 | && $this->data['openid_ns_ax'] == 'http://openid.net/srv/ax/1.0' 397 | ) { 398 | $alias = 'ax'; 399 | } else { 400 | foreach($this->data as $name => $value) { 401 | if ($value == 'http://openid.net/srv/ax/1.0' 402 | && preg_match('/openid_ns_(.+)/', $name, $m) 403 | ) { 404 | $alias = $m[1]; 405 | break; 406 | } 407 | } 408 | } 409 | 410 | if(!$alias) { 411 | return null; 412 | } 413 | 414 | $fields = array(); 415 | # Now, we must search again, this time for field aliases 416 | foreach($this->data as $name => $value) { 417 | if (strpos($name, 'openid_' . $alias . '_type') === false 418 | || strpos($value, $ns) === false) { 419 | continue; 420 | } 421 | 422 | $name = substr($name, strlen('openid_' . $alias . '_type_')); 423 | $value = substr($value, strlen($ns)); 424 | 425 | $fields[$name] = $value; 426 | } 427 | 428 | # Then, we find out what fields are required and optional 429 | $required = array(); 430 | $if_available = array(); 431 | foreach(array('required','if_available') as $type) { 432 | if(empty($this->data["openid_{$alias}_{$type}"])) { 433 | continue; 434 | } 435 | $attributes = explode(',', $this->data["openid_{$alias}_{$type}"]); 436 | foreach($attributes as $attr) { 437 | if(empty($fields[$attr])) { 438 | # There is an undefined field here, so we ignore it. 439 | continue; 440 | } 441 | 442 | ${$type}[] = $fields[$attr]; 443 | } 444 | } 445 | 446 | $this->data['ax'] = true; 447 | return array('required' => $required, 'optional' => $if_available); 448 | } 449 | 450 | protected function sreg() 451 | { 452 | $sreg_to_ax = array_flip($this->ax_to_sreg); 453 | 454 | $attributes = array('required' => array(), 'optional' => array()); 455 | 456 | if (empty($this->data['openid_sreg_required']) 457 | && empty($this->data['openid_sreg_optional']) 458 | ) { 459 | return $attributes; 460 | } 461 | 462 | foreach(array('required', 'optional') as $type) { 463 | foreach(explode(',',$this->data['openid_sreg_' . $type]) as $attr) { 464 | if(empty($sreg_to_ax[$attr])) { 465 | # Undefined attribute in SREG request. 466 | # Shouldn't happen, but we check anyway. 467 | continue; 468 | } 469 | 470 | $attributes[$type][] = $sreg_to_ax[$attr]; 471 | } 472 | } 473 | 474 | return $attributes; 475 | } 476 | 477 | /** 478 | * Aids an RP in assertion verification. 479 | * @return bool Information whether the verification suceeded. 480 | */ 481 | protected function verify() 482 | { 483 | # Firstly, we need to make sure that there's an association. 484 | # Otherwise the verification will fail, 485 | # because we've signed assoc_handle in the assertion 486 | if(empty($this->assoc)) { 487 | return false; 488 | } 489 | 490 | # Next, we check that it's a private association, 491 | # i.e. one made without RP input. 492 | # Otherwise, the RP shouldn't ask us to verify. 493 | if(empty($this->assoc['private'])) { 494 | return false; 495 | } 496 | 497 | # Now we have to check if the nonce is correct, to prevent replay attacks. 498 | if($this->data['openid_response_nonce'] != $this->assoc['nonce']) { 499 | return false; 500 | } 501 | 502 | # Getting the signed fields for signature. 503 | $sig = array(); 504 | $signed = explode(',', $this->data['openid_signed']); 505 | foreach($signed as $field) { 506 | $name = strtr($field, '.', '_'); 507 | if(!isset($this->data['openid_' . $name])) { 508 | return false; 509 | } 510 | 511 | $sig[$field] = $this->data['openid_' . $name]; 512 | } 513 | 514 | # Computing the signature and checking if it matches. 515 | $sig = $this->keyValueForm($sig); 516 | if ($this->data['openid_sig'] != 517 | base64_encode(hash_hmac($this->assoc['hash'], $sig, $this->assoc['mac'], true)) 518 | ) { 519 | return false; 520 | } 521 | 522 | # Clearing the nonce, so that it won't be used again. 523 | $this->assoc['nonce'] = null; 524 | 525 | if(empty($this->assoc['private'])) { 526 | # Commiting changes to the association. 527 | $this->setAssoc($this->assoc['handle'], $this->assoc); 528 | } else { 529 | # Private associations shouldn't be used again, se we can as well delete them. 530 | $this->delAssoc($this->assoc['handle']); 531 | } 532 | 533 | # Nothing has failed, so the verification was a success. 534 | return true; 535 | } 536 | 537 | /** 538 | * Performs association with an RP. 539 | */ 540 | protected function associate() 541 | { 542 | # Rejecting no-encryption without TLS. 543 | if(empty($_SERVER['HTTPS']) && $this->data['openid_session_type'] == 'no-encryption') { 544 | $this->directErrorResponse(); 545 | } 546 | 547 | # Checking whether we support DH at all. 548 | if (!$this->dh && substr($this->data['openid_session_type'], 0, 2) == 'DH') { 549 | $this->redirect($this->response(array( 550 | 'openid.error' => 'DH not supported', 551 | 'openid.error_code' => 'unsupported-type', 552 | 'openid.session_type' => 'no-encryption' 553 | ))); 554 | } 555 | 556 | # Creating the association 557 | $this->assoc = array(); 558 | $this->assoc['hash'] = $this->data['openid_assoc_type'] == 'HMAC-SHA256' ? 'sha256' : 'sha1'; 559 | $this->assoc['handle'] = $this->assoc_handle(); 560 | 561 | # Getting the shared secret 562 | if($this->data['openid_session_type'] == 'no-encryption') { 563 | $this->assoc['mac'] = base64_encode($this->shared_secret($this->assoc['hash'])); 564 | } else { 565 | $this->dh(); 566 | } 567 | 568 | # Preparing the direct response... 569 | $response = array( 570 | 'ns' => $this->ns, 571 | 'assoc_handle' => $this->assoc['handle'], 572 | 'assoc_type' => $this->data['openid_assoc_type'], 573 | 'session_type' => $this->data['openid_session_type'], 574 | 'expires_in' => $this->assoc_lifetime 575 | ); 576 | 577 | if(isset($this->assoc['dh_server_public'])) { 578 | $response['dh_server_public'] = $this->assoc['dh_server_public']; 579 | $response['enc_mac_key'] = $this->assoc['mac']; 580 | } else { 581 | $response['mac_key'] = $this->assoc['mac']; 582 | } 583 | 584 | # ...and sending it. 585 | echo $this->keyValueForm($response); 586 | die(); 587 | } 588 | 589 | /** 590 | * Creates a private association. 591 | */ 592 | protected function generateAssociation() 593 | { 594 | $this->assoc = array(); 595 | # We use sha1 by default. 596 | $this->assoc['hash'] = 'sha1'; 597 | $this->assoc['mac'] = $this->shared_secret('sha1'); 598 | $this->assoc['handle'] = $this->assoc_handle(); 599 | } 600 | 601 | /** 602 | * Encrypts the MAC key using DH key exchange. 603 | */ 604 | protected function dh() 605 | { 606 | if(empty($this->data['openid_dh_modulus'])) { 607 | $this->data['openid_dh_modulus'] = $this->default_modulus; 608 | } 609 | 610 | if(empty($this->data['openid_dh_gen'])) { 611 | $this->data['openid_dh_gen'] = $this->default_gen; 612 | } 613 | 614 | if(empty($this->data['openid_dh_consumer_public'])) { 615 | $this->directErrorResponse(); 616 | } 617 | 618 | $modulus = $this->b64dec($this->data['openid_dh_modulus']); 619 | $gen = $this->b64dec($this->data['openid_dh_gen']); 620 | $consumerKey = $this->b64dec($this->data['openid_dh_consumer_public']); 621 | 622 | $privateKey = $this->keygen(strlen($modulus)); 623 | $publicKey = $this->powmod($gen, $privateKey, $modulus); 624 | $ss = $this->powmod($consumerKey, $privateKey, $modulus); 625 | 626 | $mac = $this->x_or(hash($this->assoc['hash'], $ss, true), $this->shared_secret($this->assoc['hash'])); 627 | $this->assoc['dh_server_public'] = $this->decb64($publicKey); 628 | $this->assoc['mac'] = base64_encode($mac); 629 | } 630 | 631 | /** 632 | * XORs two strings. 633 | * @param String $a 634 | * @param String $b 635 | * @return String $a ^ $b 636 | */ 637 | protected function x_or($a, $b) 638 | { 639 | $length = strlen($a); 640 | for($i = 0; $i < $length; $i++) { 641 | $a[$i] = $a[$i] ^ $b[$i]; 642 | } 643 | 644 | return $a; 645 | } 646 | 647 | /** 648 | * Prepares an indirect response url. 649 | * @param array $params Parameters to be sent. 650 | */ 651 | protected function response($params) 652 | { 653 | $params += array('openid.ns' => $this->ns); 654 | return $this->data['openid_return_to'] 655 | . (strpos($this->data['openid_return_to'],'?') ? '&' : '?') 656 | . http_build_query($params, '', '&'); 657 | } 658 | 659 | /** 660 | * Outputs a direct error. 661 | */ 662 | protected function errorResponse() 663 | { 664 | if(!empty($this->data['openid_return_to'])) { 665 | $response = array( 666 | 'openid.mode' => 'error', 667 | 'openid.error' => 'Invalid request' 668 | ); 669 | $this->redirect($this->response($response)); 670 | } else { 671 | header('HTTP/1.1 400 Bad Request'); 672 | $response = array( 673 | 'ns' => $this->ns, 674 | 'error' => 'Invalid request' 675 | ); 676 | echo $this->keyValueForm($response); 677 | } 678 | die(); 679 | } 680 | 681 | /** 682 | * Sends an positive assertion. 683 | * @param String $identity the OP-Local Identifier that is being authenticated. 684 | * @param Array $attributes User attributes to be sent. 685 | */ 686 | protected function positiveResponse($identity, $attributes) 687 | { 688 | # We generate a private association if there is none established. 689 | if(!$this->assoc) { 690 | $this->generateAssociation(); 691 | $this->assoc['private'] = true; 692 | } 693 | 694 | # We set openid.identity (and openid.claimed_id if necessary) to our $identity 695 | if($this->data['openid_identity'] == $this->data['openid_claimed_id'] || $this->select_id) { 696 | $this->data['openid_claimed_id'] = $identity; 697 | } 698 | $this->data['openid_identity'] = $identity; 699 | 700 | # Preparing fields to be signed 701 | $params = array( 702 | 'op_endpoint' => $this->serverLocation, 703 | 'claimed_id' => $this->data['openid_claimed_id'], 704 | 'identity' => $this->data['openid_identity'], 705 | 'return_to' => $this->data['openid_return_to'], 706 | 'realm' => $this->data['openid_realm'], 707 | 'response_nonce' => gmdate("Y-m-d\TH:i:s\Z"), 708 | 'assoc_handle' => $this->assoc['handle'], 709 | ); 710 | 711 | $params += $this->responseAttributes($attributes); 712 | 713 | # Has the RP used an invalid association handle? 714 | if (isset($this->data['openid_assoc_handle']) 715 | && $this->data['openid_assoc_handle'] != $this->assoc['handle'] 716 | ) { 717 | $params['invalidate_handle'] = $this->data['openid_assoc_handle']; 718 | } 719 | 720 | # Signing the $params 721 | $sig = hash_hmac($this->assoc['hash'], $this->keyValueForm($params), $this->assoc['mac'], true); 722 | $req = array( 723 | 'openid.mode' => 'id_res', 724 | 'openid.signed' => implode(',', array_keys($params)), 725 | 'openid.sig' => base64_encode($sig), 726 | ); 727 | 728 | # Saving the nonce and commiting the association. 729 | $this->assoc['nonce'] = $params['response_nonce']; 730 | $this->setAssoc($this->assoc['handle'], $this->assoc); 731 | 732 | # Preparing and sending the response itself 733 | foreach($params as $name => $value) { 734 | $req['openid.' . $name] = $value; 735 | } 736 | 737 | $this->redirect($this->response($req)); 738 | } 739 | 740 | /** 741 | * Prepares an array of attributes to send 742 | */ 743 | protected function responseAttributes($attributes) 744 | { 745 | if(!$attributes) return array(); 746 | 747 | $ns = 'http://axschema.org/'; 748 | 749 | $response = array(); 750 | if(isset($this->data['ax'])) { 751 | $response['ns.ax'] = 'http://openid.net/srv/ax/1.0'; 752 | foreach($attributes as $name => $value) { 753 | $alias = strtr($name, '/', '_'); 754 | $response['ax.type.' . $alias] = $ns . $name; 755 | $response['ax.value.' . $alias] = $value; 756 | } 757 | return $response; 758 | } 759 | 760 | foreach($attributes as $name => $value) { 761 | if(!isset($this->ax_to_sreg[$name])) { 762 | continue; 763 | } 764 | 765 | $response['sreg.' . $this->ax_to_sreg[$name]] = $value; 766 | } 767 | return $response; 768 | } 769 | 770 | /** 771 | * Encodes fields in key-value form. 772 | * @param Array $params Fields to be encoded. 773 | * @return String $params in key-value form. 774 | */ 775 | protected function keyValueForm($params) 776 | { 777 | $str = ''; 778 | foreach($params as $name => $value) { 779 | $str .= "$name:$value\n"; 780 | } 781 | 782 | return $str; 783 | } 784 | 785 | /** 786 | * Responds with an information that the user has canceled authentication. 787 | */ 788 | protected function cancel() 789 | { 790 | $this->redirect($this->response(array('openid.mode' => 'cancel'))); 791 | } 792 | 793 | /** 794 | * Converts base64 encoded number to it's decimal representation. 795 | * @param String $str base64 encoded number. 796 | * @return String Decimal representation of that number. 797 | */ 798 | protected function b64dec($str) 799 | { 800 | $bytes = unpack('C*', base64_decode($str)); 801 | $n = 0; 802 | foreach($bytes as $byte) { 803 | $n = $this->add($this->mul($n, 256), $byte); 804 | } 805 | 806 | return $n; 807 | } 808 | 809 | /** 810 | * Complements b64dec. 811 | */ 812 | protected function decb64($num) 813 | { 814 | $bytes = array(); 815 | while($num) { 816 | array_unshift($bytes, $this->mod($num, 256)); 817 | $num = $this->div($num, 256); 818 | } 819 | 820 | if($bytes && $bytes[0] > 127) { 821 | array_unshift($bytes,0); 822 | } 823 | 824 | array_unshift($bytes, 'C*'); 825 | 826 | return base64_encode(call_user_func_array('pack', $bytes)); 827 | } 828 | 829 | function __call($name, $args) 830 | { 831 | switch($name) { 832 | case 'add': 833 | case 'mul': 834 | case 'pow': 835 | case 'mod': 836 | case 'div': 837 | case 'powmod': 838 | if(function_exists('gmp_strval')) { 839 | return gmp_strval(call_user_func_array($this->$name, $args)); 840 | } 841 | return call_user_func_array($this->$name, $args); 842 | default: 843 | throw new BadMethodCallException(); 844 | } 845 | } 846 | } 847 | --------------------------------------------------------------------------------