├── .travis.yml ├── LICENSE.md ├── README.md ├── composer.json ├── src └── Yubikey │ ├── Client.php │ ├── Request.php │ ├── RequestCollection.php │ ├── Response.php │ ├── ResponseCollection.php │ └── Validate.php ├── test.php └── tests ├── ValidateTest.php ├── bootstrap.php └── phpunit.xml /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.6 4 | - 7.1 5 | script: phpunit --configuration tests/phpunit.xml tests/ 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Chris Cornutt 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yubikey PHP Library 2 | ======================= 3 | 4 | [![Travis-CI Build Status](https://secure.travis-ci.org/enygma/yubikey.png?branch=master)](http://travis-ci.org/enygma/yubikey) 5 | [![Codacy Badge](https://www.codacy.com/project/badge/6b73c56a21734a6d93dae6019f733c5e)](https://www.codacy.com) 6 | [![Code Climate](https://codeclimate.com/github/enygma/yubikey/badges/gpa.svg)](https://codeclimate.com/github/enygma/yubikey) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/enygma/yubikey.svg?style=flat-square)](https://packagist.org/packages/enygma/yubikey) 8 | 9 | This library lets you easily interface with the Yubico REST API for validating 10 | the codes created by the Yubikey. 11 | 12 | ### Requirements: 13 | 14 | - An API as requested from the Yubico site 15 | - A client ID requested from Yubico 16 | - A Yubikey to test out the implementation 17 | 18 | ### Installation 19 | 20 | Use the followng command to install the library via Composer: 21 | 22 | ``` 23 | composer require enygma/yubikey 24 | ``` 25 | 26 | ### Usage: 27 | 28 | Look at the `test.php` example script to see how to use it. This can be executed like: 29 | 30 | `php test.php [generated key]` 31 | 32 | Example code: 33 | 34 | ```php 35 | check($inputtedKey); 41 | 42 | echo ($response->success() === true) ? 'success!' : 'you failed. aw.'; 43 | ?> 44 | ``` 45 | 46 | ### HTTP vs HTTPS 47 | 48 | By default the library will try to use a `HTTPS` request to the host given. If you need to disable this for some reason 49 | (like no SSL support), you can use the `setUseSecure` method and set it to false: 50 | 51 | ```php 52 | $v = new \Yubikey\Validate($apiKey, $clientId); 53 | $v->setUseSecure(false); 54 | ``` 55 | 56 | ### Overriding hosts 57 | 58 | The library comes with a set of hostnames for the Yubico external API servers (api.yubico.com through api5.yubico.com). If 59 | you ever have a need to override these, you can use `setHosts`: 60 | 61 | ```php 62 | $v = new \Yubikey\Validate($apiKey, $clientId); 63 | $v->setHosts(array( 64 | 'api.myhost1.com', 65 | 'api1.myhost.com' 66 | )); 67 | ``` 68 | Remember, this will *overwrite* the current hosts in the class, so be sure you don't still need those. If you just want to add 69 | another host, look at the `addHost` method. 70 | 71 | ### Multi-Server Requests: 72 | 73 | Additonally, the library also supports simultaneous connections to multiple servers. By default it will only make 74 | the request to the first server in the `hosts` list. You can enable the multi-server checking with a second parameter on 75 | the `check()` method: 76 | 77 | ```php 78 | check($inputtedKey, true); 81 | 82 | echo ($response->success() === true) ? 'success!' : 'you failed. aw.'; 83 | ?> 84 | ```` 85 | 86 | This will make multiple requests and return the pass/fail status of the aggregate responses from each. So, if you have all but one 87 | server pass, the overall response will be a fail. If all return `OK` though, you're in the clear. 88 | 89 | ### "First in" result 90 | 91 | Additionally, you can also switch on and off this aggregation of the results and go with only the "first in" response. You do this 92 | with a flag on the `success` checking method: 93 | 94 | ```php 95 | check($inputtedKey, true); 98 | 99 | echo ($response->success(true) === true) ? 'success!' : 'you failed. aw.'; 100 | ?> 101 | ```` 102 | 103 | **NOTE:** This will still work without multi-server checking. The "first in" will just always be the single response. 104 | 105 | 106 | @author Chris Cornutt 107 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"enygma/yubikey", 3 | "type":"library", 4 | "description":"PHP library to interface with the Yubikey REST API", 5 | "keywords":["yubikey", "yubico", "twofactor", "rest", "api"], 6 | "homepage":"https://github.com/enygma/yubikey.git", 7 | "license":"MIT", 8 | "authors":[ 9 | { 10 | "name":"Chris Cornutt", 11 | "email":"ccornutt@phpdeveloper.org", 12 | "homepage":"http://www.phpdeveloper.org/" 13 | } 14 | ], 15 | "require":{ 16 | "php":">=7.4" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "4.3.2", 20 | "codeclimate/php-test-reporter": "dev-master" 21 | }, 22 | "autoload":{ 23 | "psr-0":{ 24 | "Yubikey":"src/" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Yubikey/Client.php: -------------------------------------------------------------------------------- 1 | request($requests); 19 | return $responses; 20 | } 21 | 22 | /** 23 | * Make the request given the Request set and content 24 | * 25 | * @param \Yubikey\RequestCollection $requests Request collection 26 | * @return \Yubikey\ResponseCollection instance 27 | */ 28 | public function request(\Yubikey\RequestCollection $requests) 29 | { 30 | $responses = new \Yubikey\ResponseCollection(); 31 | $startTime = microtime(true); 32 | $multi = curl_multi_init(); 33 | $curls = array(); 34 | 35 | foreach ($requests as $index => $request) { 36 | $curls[$index] = curl_init(); 37 | curl_setopt_array($curls[$index], array( 38 | CURLOPT_URL => $request->getUrl(), 39 | CURLOPT_HEADER => 0, 40 | CURLOPT_RETURNTRANSFER => 1 41 | )); 42 | curl_multi_add_handle($multi, $curls[$index]); 43 | } 44 | 45 | do { 46 | while ((curl_multi_exec($multi, $active)) == CURLM_CALL_MULTI_PERFORM); 47 | while ($info = curl_multi_info_read($multi)) { 48 | if ($info['result'] == CURLE_OK) { 49 | $return = curl_multi_getcontent($info['handle']); 50 | $cinfo = curl_getinfo($info['handle']); 51 | $url = parse_url($cinfo['url']); 52 | 53 | $response = new \Yubikey\Response(array( 54 | 'host' => $url['host'], 55 | 'mt' => (microtime(true)-$startTime) 56 | )); 57 | $response->parse($return); 58 | $responses->add($response); 59 | } 60 | } 61 | } while ($active); 62 | 63 | return $responses; 64 | } 65 | } -------------------------------------------------------------------------------- /src/Yubikey/Request.php: -------------------------------------------------------------------------------- 1 | setUrl($url); 28 | } 29 | } 30 | 31 | /** 32 | * Get the type of request 33 | * 34 | * @return string HTTP verb type 35 | */ 36 | public function getType() 37 | { 38 | return $this->type; 39 | } 40 | 41 | /** 42 | * Set the type of the request 43 | * 44 | * @param string $type HTTP verb type 45 | */ 46 | public function setType($type) 47 | { 48 | $this->type = $type; 49 | } 50 | 51 | /** 52 | * Get the current request URL location 53 | * 54 | * @return string URL location 55 | */ 56 | public function getUrl() 57 | { 58 | return $this->url; 59 | } 60 | 61 | /** 62 | * Set the URL location for the request 63 | * 64 | * @param string $url URL location 65 | */ 66 | public function setUrl($url) 67 | { 68 | if (filter_var($url, FILTER_VALIDATE_URL) !== $url) { 69 | throw new \Exception('Invalid URL: '.$url); 70 | } 71 | $this->url = $url; 72 | } 73 | } -------------------------------------------------------------------------------- /src/Yubikey/RequestCollection.php: -------------------------------------------------------------------------------- 1 | add($request); 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * Add the given request to the set 35 | * 36 | * @param \Yubikey\Request $request Request object 37 | */ 38 | public function add(\Yubikey\Request $request) 39 | { 40 | $this->requests[] = $request; 41 | } 42 | 43 | /** 44 | * For Countable 45 | * 46 | * @return integer Count of current Requests 47 | */ 48 | #[\ReturnTypeWillChange] 49 | public function count() 50 | { 51 | return count($this->requests); 52 | } 53 | 54 | /** 55 | * For Iterator 56 | * 57 | * @return Current Request object 58 | */ 59 | #[\ReturnTypeWillChange] 60 | public function current() 61 | { 62 | return $this->requests[$this->position]; 63 | } 64 | 65 | /** 66 | * For Iterator 67 | * 68 | * @return integer Current position in set 69 | */ 70 | #[\ReturnTypeWillChange] 71 | public function key() 72 | { 73 | return $this->position; 74 | } 75 | 76 | /** 77 | * For Iterator 78 | * 79 | * @return integer Next positiion in set 80 | */ 81 | #[\ReturnTypeWillChange] 82 | public function next() 83 | { 84 | return ++$this->position; 85 | } 86 | 87 | /** 88 | * For Iterator, rewind set location to beginning 89 | */ 90 | #[\ReturnTypeWillChange] 91 | public function rewind() 92 | { 93 | $this->position = 0; 94 | } 95 | 96 | /** 97 | * For Iterator, check to see if set item is valid 98 | * 99 | * @return boolean Valid/invalid result 100 | */ 101 | #[\ReturnTypeWillChange] 102 | public function valid() 103 | { 104 | return isset($this->requests[$this->position]); 105 | } 106 | 107 | /** 108 | * For ArrayAccess 109 | * 110 | * @param mixed $offset Offset identifier 111 | * @return boolean Found/not found result 112 | */ 113 | #[\ReturnTypeWillChange] 114 | public function offsetExists($offset) 115 | { 116 | return (isset($this->requests[$offset])); 117 | } 118 | 119 | /** 120 | * For ArrayAccess 121 | * 122 | * @param mixed $offset Offset to locate 123 | * @return \Yubikey\Request object if found 124 | */ 125 | #[\ReturnTypeWillChange] 126 | public function offsetGet($offset) 127 | { 128 | return $this->requests[$offset]; 129 | } 130 | 131 | /** 132 | * For ArrayAccess 133 | * 134 | * @param mixed $offset Offset to use in data set 135 | * @param mixed $data Data to assign 136 | */ 137 | #[\ReturnTypeWillChange] 138 | public function offsetSet($offset, $data) 139 | { 140 | $this->requests[$offset] = $data; 141 | } 142 | 143 | /** 144 | * For ArrayAccess 145 | * 146 | * @param mixed $offset Offset to remove 147 | */ 148 | #[\ReturnTypeWillChange] 149 | public function offsetUnset($offset) 150 | { 151 | unset($this->requests[$offset]); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Yubikey/Response.php: -------------------------------------------------------------------------------- 1 | load($data); 108 | } 109 | } 110 | 111 | /** 112 | * Load the data into the object 113 | * 114 | * @param array $data Data from the object 115 | * @return boolean True when loading is done 116 | */ 117 | public function load($data) 118 | { 119 | foreach ($data as $index => $value) { 120 | if (property_exists($this, $index)) { 121 | $this->$index = trim($value); 122 | } 123 | } 124 | return true; 125 | } 126 | 127 | /** 128 | * Parse the return data from the request and 129 | * load it into the object properties 130 | * 131 | * @param string $data API return data 132 | */ 133 | public function parse($data) 134 | { 135 | $result = array(); 136 | $parts = explode("\n", $data); 137 | 138 | foreach($parts as $index => $part) { 139 | $kv = explode("=", $part); 140 | if (!empty($kv[1])) { 141 | $result[$kv[0]] = $kv[1]; 142 | } 143 | } 144 | 145 | $this->load($result); 146 | } 147 | 148 | /** 149 | * Get the time value for the response 150 | * 151 | * @return string Date/time string 152 | */ 153 | public function getTime() 154 | { 155 | return $this->t; 156 | } 157 | 158 | /** 159 | * Return the time to execute (microtime) 160 | * 161 | * @return integer Time result 162 | */ 163 | public function getMt() 164 | { 165 | return $this->mt; 166 | } 167 | 168 | /** 169 | * Set the OTP used in the request 170 | * @param string $otp OTP string (from key) 171 | */ 172 | public function setInputOtp($otp) 173 | { 174 | $this->inputOtp = $otp; 175 | return $this; 176 | } 177 | 178 | /** 179 | * Get the OTP used in the request 180 | * @return string OTP string 181 | */ 182 | public function getInputOtp() 183 | { 184 | return $this->inputOtp; 185 | } 186 | 187 | /** 188 | * Set the nonce used in the request 189 | * @param string $nonce Nonce from request 190 | */ 191 | public function setInputNonce($nonce) 192 | { 193 | $this->inputNonce = $nonce; 194 | return $this; 195 | } 196 | 197 | /** 198 | * Get the nonce used in the request 199 | * @return string Nonce from request 200 | */ 201 | public function getInputNonce() 202 | { 203 | return $this->inputNonce; 204 | } 205 | 206 | /** 207 | * Set response hostname 208 | * @param string $host Hostname 209 | */ 210 | public function setHost($host) 211 | { 212 | $this->host = $host; 213 | return $this; 214 | } 215 | 216 | /** 217 | * Get the current hostname 218 | * @return string 219 | */ 220 | public function getHost() 221 | { 222 | return $this->host; 223 | } 224 | 225 | /** 226 | * Get the hash from the response 227 | * 228 | * @param boolean $encode "Encode" the data (replace + with %2B) 229 | * @return string Hash value 230 | */ 231 | public function getHash($encode = false) 232 | { 233 | $hash = $this->h; 234 | if (substr($hash, -1) !== '=') { 235 | $hash .= '='; 236 | } 237 | if ($encode === true) { 238 | $hash = str_replace('+', '%2B', $hash); 239 | } 240 | return $hash; 241 | } 242 | 243 | /** 244 | * Get the properties of the response 245 | * 246 | * @return array Response property list 247 | */ 248 | public function getProperties() 249 | { 250 | return array( 251 | 't', 'otp', 'nonce', 'sl', 'status', 252 | 'timestamp', 'sessioncounter', 'sessionuse' 253 | ); 254 | } 255 | 256 | /** 257 | * Magic method to get access to the private properties 258 | * @param string $name Property name 259 | * @return string|null If found, returns teh value. If not, null 260 | */ 261 | public function __get($name) 262 | { 263 | return (property_exists($this, $name)) ? $this->$name : null; 264 | } 265 | 266 | /** 267 | * Check the success of the response 268 | * Validates: status, OTP and nonce 269 | * @return boolean Success/fail of request 270 | */ 271 | public function success() 272 | { 273 | $inputOtp = $this->getInputOtp(); 274 | $inputNonce = $this->getInputNonce(); 275 | 276 | if ($inputNonce === null || $inputOtp === null) { 277 | return false; 278 | } 279 | 280 | return ( 281 | $inputOtp == $this->otp 282 | && $inputNonce === $this->nonce 283 | && $this->status === Response::SUCCESS 284 | ) ? true : false; 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/Yubikey/ResponseCollection.php: -------------------------------------------------------------------------------- 1 | add($response); 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * Determine, based on the Response status (success) 35 | * if the overall operation was successful 36 | * 37 | * @return boolean Success/fail status 38 | */ 39 | public function success($first = false) 40 | { 41 | $success = false; 42 | if ($first === true) { 43 | // Sort them by timestamp, pop the first one and return pass/fail 44 | usort($this->responses, function(\Yubikey\Response $r1, \Yubikey\Response $r2) { 45 | return $r1->getMt() > $r2->getMt(); 46 | }); 47 | $response = $this->responses[0]; 48 | return $response->success(); 49 | } else { 50 | foreach ($this->responses as $response) { 51 | if ($response->success() === false 52 | && $response->status !== Response::REPLAY_REQUEST) { 53 | return false; 54 | } elseif ($response->success()) { 55 | $success = true; 56 | } 57 | } 58 | } 59 | return $success; 60 | } 61 | 62 | /** 63 | * Add a new Response object to the set 64 | * 65 | * @param \Yubikey\Response $response Response object 66 | */ 67 | public function add(\Yubikey\Response $response) 68 | { 69 | $this->responses[] = $response; 70 | } 71 | 72 | /** 73 | * For Countable 74 | * 75 | * @return integer Count of current Requests 76 | */ 77 | #[\ReturnTypeWillChange] 78 | public function count() 79 | { 80 | return count($this->responses); 81 | } 82 | 83 | /** 84 | * For Iterator 85 | * 86 | * @return Current Request object 87 | */ 88 | #[\ReturnTypeWillChange] 89 | public function current() 90 | { 91 | return $this->responses[$this->position]; 92 | } 93 | 94 | /** 95 | * For Iterator 96 | * 97 | * @return integer Current position in set 98 | */ 99 | #[\ReturnTypeWillChange] 100 | public function key() 101 | { 102 | return $this->position; 103 | } 104 | 105 | /** 106 | * For Iterator 107 | * 108 | * @return integer Next positiion in set 109 | */ 110 | #[\ReturnTypeWillChange] 111 | public function next() 112 | { 113 | return ++$this->position; 114 | } 115 | 116 | /** 117 | * For Iterator, rewind set location to beginning 118 | */ 119 | #[\ReturnTypeWillChange] 120 | public function rewind() 121 | { 122 | $this->position = 0; 123 | } 124 | 125 | /** 126 | * For Iterator, check to see if set item is valid 127 | * 128 | * @return boolean Valid/invalid result 129 | */ 130 | #[\ReturnTypeWillChange] 131 | public function valid() 132 | { 133 | return isset($this->responses[$this->position]); 134 | } 135 | 136 | /** 137 | * For ArrayAccess 138 | * 139 | * @param mixed $offset Offset identifier 140 | * @return boolean Found/not found result 141 | */ 142 | #[\ReturnTypeWillChange] 143 | public function offsetExists($offset) 144 | { 145 | return (isset($this->responses[$offset])); 146 | } 147 | 148 | /** 149 | * For ArrayAccess 150 | * 151 | * @param mixed $offset Offset to locate 152 | * @return \Yubikey\Request object if found 153 | */ 154 | #[\ReturnTypeWillChange] 155 | public function offsetGet($offset) 156 | { 157 | return $this->responses[$offset]; 158 | } 159 | 160 | /** 161 | * For ArrayAccess 162 | * 163 | * @param mixed $offset Offset to use in data set 164 | * @param mixed $data Data to assign 165 | */ 166 | #[\ReturnTypeWillChange] 167 | public function offsetSet($offset, $data) 168 | { 169 | $this->responses[$offset] = $data; 170 | } 171 | 172 | /** 173 | * For ArrayAccess 174 | * 175 | * @param mixed $offset Offset to remove 176 | */ 177 | #[\ReturnTypeWillChange] 178 | public function offsetUnset($offset) 179 | { 180 | unset($this->responses[$offset]); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Yubikey/Validate.php: -------------------------------------------------------------------------------- 1 | checkCurlSupport() === false) { 67 | throw new \DomainException('cURL support is required and is not enabled!'); 68 | } 69 | 70 | $this->setApiKey($apiKey); 71 | $this->setClientId($clientId); 72 | 73 | if (!empty($hosts)) { 74 | $this->setHosts($hosts); 75 | } 76 | } 77 | 78 | /** 79 | * Check for enabled curl support (requirement) 80 | * 81 | * @return boolean Enabled/not found flag 82 | */ 83 | public function checkCurlSupport() 84 | { 85 | return (function_exists('curl_init')); 86 | } 87 | 88 | /** 89 | * Get the currently set API key 90 | * 91 | * @return string API key 92 | */ 93 | public function getApiKey($decoded = false) 94 | { 95 | return ($decoded === false) ? $this->apiKey : base64_decode($this->apiKey); 96 | } 97 | 98 | /** 99 | * Set the API key 100 | * 101 | * @param string $apiKey API request key 102 | */ 103 | public function setApiKey($apiKey) 104 | { 105 | $key = base64_decode($apiKey, true); 106 | if ($key === false) { 107 | throw new \InvalidArgumentException('Invalid API key'); 108 | } 109 | 110 | $this->apiKey = $key; 111 | return $this; 112 | } 113 | 114 | /** 115 | * Set the OTP for the request 116 | * 117 | * @param string $otp One-time password 118 | */ 119 | public function setOtp($otp) 120 | { 121 | $this->otp = $otp; 122 | return $this; 123 | } 124 | 125 | /** 126 | * Get the currently set OTP 127 | * 128 | * @return string One-time password 129 | */ 130 | public function getOtp() 131 | { 132 | return $this->otp; 133 | } 134 | 135 | /** 136 | * Get the current Client ID 137 | * 138 | * @return integer Client ID 139 | */ 140 | public function getClientId() 141 | { 142 | return $this->clientId; 143 | } 144 | 145 | /** 146 | * Set the current Client ID 147 | * 148 | * @param integer $clientId Client ID 149 | */ 150 | public function setClientId($clientId) 151 | { 152 | $this->clientId = $clientId; 153 | return $this; 154 | } 155 | 156 | /** 157 | * Get the "use secure" setting 158 | * 159 | * @return boolean Use flag 160 | */ 161 | public function getUseSecure() 162 | { 163 | return $this->useSecure; 164 | } 165 | 166 | /** 167 | * Set the "use secure" setting 168 | * 169 | * @param boolean $use Use/don't use secure 170 | * @throws \InvalidArgumentException when value is not boolean 171 | */ 172 | public function setUseSecure($use) 173 | { 174 | if (!is_bool($use)) { 175 | throw new \InvalidArgumentException('"Use secure" value must be boolean'); 176 | } 177 | $this->useSecure = $use; 178 | return $this; 179 | } 180 | 181 | /** 182 | * Get the host for the request 183 | * If one is not set, it returns a random one from the host set 184 | * 185 | * @return string Hostname string 186 | */ 187 | public function getHost() 188 | { 189 | if ($this->host === null) { 190 | // pick a "random" host 191 | $host = $this->hosts[mt_rand(0,count($this->hosts)-1)]; 192 | $this->setHost($host); 193 | return $host; 194 | } else { 195 | return $this->host; 196 | } 197 | } 198 | 199 | /** 200 | * Get the current hosts list 201 | * 202 | * @return array Hosts list 203 | */ 204 | public function getHosts() 205 | { 206 | return $this->hosts; 207 | } 208 | 209 | /** 210 | * Set the API host for the request 211 | * 212 | * @param string $host Hostname 213 | */ 214 | public function setHost($host) 215 | { 216 | $this->host = $host; 217 | return $this; 218 | } 219 | 220 | /** 221 | * Add a new host to the list 222 | * 223 | * @param string $host Hostname to add 224 | */ 225 | public function addHost($host) 226 | { 227 | $this->hosts[] = $host; 228 | return $this; 229 | } 230 | 231 | /** 232 | * Set the hosts to request results from 233 | * 234 | * @param array $hosts Set of hostnames 235 | */ 236 | public function setHosts(array $hosts) 237 | { 238 | $this->hosts = $hosts; 239 | } 240 | 241 | /** 242 | * Geenrate the signature for the request values 243 | * 244 | * @param array $data Data for request 245 | * @throws \InvalidArgumentException when API key is invalid 246 | * @return Hashed request signature (string) 247 | */ 248 | public function generateSignature($data, $key = null) 249 | { 250 | if ($key === null) { 251 | $key = $this->getApiKey(); 252 | if ($key === null || empty($key)) { 253 | throw new \InvalidArgumentException('Invalid API key!'); 254 | } 255 | } 256 | 257 | $query = http_build_query($data); 258 | $query = mb_convert_encoding(str_replace('%3A', ':', $query), 'UTF-8', 'ISO-8859-1'); 259 | 260 | $hash = preg_replace( 261 | '/\+/', '%2B', 262 | // base64_encode(hash_hmac('sha1', http_build_query($data), $key, true)) 263 | base64_encode(hash_hmac('sha1', $query, $key, true)) 264 | ); 265 | return $hash; 266 | } 267 | 268 | /** 269 | * Check the One-time Password with API request 270 | * 271 | * @param string $otp One-time password 272 | * @param integer $clientId Client ID for API 273 | * @throws \InvalidArgumentException when OTP length is invalid 274 | * @return \Yubikey\Response object 275 | */ 276 | public function check($otp, $multi = false) 277 | { 278 | $otp = trim($otp); 279 | if (strlen($otp) < 32 || strlen($otp) > 48) { 280 | throw new \InvalidArgumentException('Invalid OTP length'); 281 | } 282 | 283 | $this->setOtp($otp); 284 | $this->setYubikeyId(); 285 | 286 | $clientId = $this->getClientId(); 287 | if ($clientId === null) { 288 | throw new \InvalidArgumentException('Client ID cannot be null'); 289 | } 290 | 291 | $nonce = $this->generateNonce(); 292 | $params = array( 293 | 'id' => $clientId, 294 | 'otp' => $otp, 295 | 'nonce' => $nonce, 296 | 'timestamp' => '1' 297 | ); 298 | ksort($params); 299 | 300 | $url = '/wsapi/2.0/verify?'.http_build_query($params).'&h='.$this->generateSignature($params); 301 | $hosts = ($multi === false) ? array(array_shift($this->hosts)) : $this->hosts; 302 | 303 | return $this->request($url, $hosts, $otp, $nonce); 304 | } 305 | 306 | /** 307 | * Generate a good nonce for the request 308 | * 309 | * @return string Generated hash 310 | */ 311 | public function generateNonce() 312 | { 313 | if (function_exists('openssl_random_pseudo_bytes') === true) { 314 | $hash = md5(openssl_random_pseudo_bytes(32)); 315 | } else { 316 | $hash = md5(uniqid(mt_rand())); 317 | } 318 | return $hash; 319 | } 320 | 321 | /** 322 | * Make the request(s) to the Yubi server(s) 323 | * 324 | * @param string $url URL for request 325 | * @param array $hosts Set of hosts to request 326 | * @param string $otp One-time password string 327 | * @param string $nonce Generated nonce 328 | * @return array Set of responses 329 | */ 330 | public function request($url, array $hosts, $otp, $nonce) 331 | { 332 | $client = new \Yubikey\Client(); 333 | $pool = new \Yubikey\RequestCollection(); 334 | 335 | // Make the requests for the host(s) 336 | $prefix = ($this->getUseSecure() === true) ? 'https' : 'http'; 337 | foreach ($hosts as $host) { 338 | $link = $prefix.'://'.$host.$url; 339 | $pool->add(new \Yubikey\Request($link)); 340 | } 341 | $responses = $client->send($pool); 342 | $responseCount = count($responses); 343 | 344 | for ($i = 0; $i < $responseCount; $i++) { 345 | $responses[$i]->setInputOtp($otp)->setInputNonce($nonce); 346 | 347 | if ($this->validateResponseSignature($responses[$i]) === false) { 348 | unset($responses[$i]); 349 | } 350 | } 351 | 352 | return $responses; 353 | } 354 | 355 | /** 356 | * Validate the signature on the response 357 | * 358 | * @param \Yubikey\Response $response Response instance 359 | * @return boolean Pass/fail status of signature validation 360 | */ 361 | public function validateResponseSignature(\Yubikey\Response $response) 362 | { 363 | $params = array(); 364 | foreach ($response->getProperties() as $property) { 365 | $value = $response->$property; 366 | if ($value !== null) { 367 | $params[$property] = $value; 368 | } 369 | } 370 | ksort($params); 371 | 372 | $signature = $this->generateSignature($params); 373 | return $this->hash_equals($signature, $response->getHash(true)); 374 | } 375 | 376 | /** 377 | * Polyfill PHP 5.6.0's hash_equals() feature 378 | */ 379 | public function hash_equals($a, $b) 380 | { 381 | if (\function_exists('hash_equals')) { 382 | return \hash_equals($a, $b); 383 | } 384 | if (\strlen($a) !== \strlen($b)) { 385 | return false; 386 | } 387 | $res = 0; 388 | $len = \strlen($a); 389 | for ($i = 0; $i < $len; ++$i) { 390 | $res |= \ord($a[$i]) ^ \ord($b[$i]); 391 | } 392 | return $res === 0; 393 | } 394 | 395 | /** 396 | * Extract the yubikey ID from the OTP 397 | */ 398 | public function setYubikeyId() 399 | { 400 | $this->yubikeyid = substr($this->getOtp(), 0, -32); 401 | return $this; 402 | } 403 | 404 | /** 405 | * Get the yubikey ID from the OTP 406 | * 407 | * @param string Optional OTP to extract the ID from 408 | * 409 | * @return string Yubikey ID string 410 | */ 411 | public function getYubikeyId($otp = '') 412 | { 413 | if (!empty($otp)) { 414 | return substr($otp, 0, -32); 415 | } 416 | 417 | return $this->yubikeyid; 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | check($_SERVER['argv'][1]); 20 | 21 | print_r($result); 22 | var_export($result->success()); 23 | } else { 24 | echo "No key value specified\n\n"; 25 | } 26 | ?> -------------------------------------------------------------------------------- /tests/ValidateTest.php: -------------------------------------------------------------------------------- 1 | validate = new \Yubikey\Validate($this->apiKey, $this->clientId); 14 | } 15 | 16 | /** 17 | * Test the getter and setter for the API key 18 | * @covers \Yubikey\Validate::setApiKey 19 | * @covers \Yubikey\Validate::getApiKey 20 | */ 21 | public function testGetSetApiKey() 22 | { 23 | $preKey = 'testing1234567890'; 24 | $key = base64_encode($preKey); 25 | 26 | $this->validate->setApiKey($key); 27 | $this->assertEquals($this->validate->getApiKey(), $preKey); 28 | } 29 | 30 | /** 31 | * Test the setting of a non-base64 encoded API key 32 | * @covers \Yubikey\Validate::setApiKey 33 | * @expectedException \InvalidArgumentException 34 | */ 35 | public function testSetInvalidApiKey() 36 | { 37 | $key = 'testing1234^%$#^#'; 38 | $this->validate->setApiKey($key); 39 | } 40 | 41 | /** 42 | * Test the getter and setter for the One-time password 43 | * @covers \Yubikey\Validate::setOtp 44 | * @covers \Yubikey\Validate::getOtp 45 | */ 46 | public function testGetSetOtp() 47 | { 48 | $otp = base64_encode('testing1234567890'); 49 | 50 | $this->validate->setOtp($otp); 51 | $this->assertEquals($this->validate->getOtp(), $otp); 52 | } 53 | 54 | /** 55 | * Test that the getter/setter for the Client ID works correctly 56 | * @covers \Yubikey\Validate::getClientId 57 | * @covers \Yubikey\Validate::setClientId 58 | */ 59 | public function testGetSetClientId() 60 | { 61 | $clientId = 12345; 62 | 63 | $this->validate->setClientId($clientId); 64 | $this->assertEquals($clientId, $this->validate->getClientId()); 65 | } 66 | 67 | /** 68 | * Test thta the getter/setter for the "use secure" setting works correctly 69 | * @covers \Yubikey\Validate::setUseSecure 70 | * @covers \Yubikey\Validate::getUseSecure 71 | */ 72 | public function testGetSetUseSecure() 73 | { 74 | $useSecure = true; 75 | 76 | $this->validate->setUseSecure($useSecure); 77 | $this->assertEquals($useSecure, $this->validate->getUseSecure()); 78 | } 79 | 80 | /** 81 | * Test that an exception is thrown when the "use secure" valus 82 | * is not boolean 83 | * 84 | * @expectedException \InvalidArgumentException 85 | * @covers \Yubikey\Validate::setUseSecure 86 | */ 87 | public function testSetUseSecureInvalid() 88 | { 89 | $useSecure = 'invalid'; 90 | $this->validate->setUseSecure($useSecure); 91 | } 92 | 93 | /** 94 | * Test that the getter/setter for the host works correctly 95 | * @covers \Yubikey\Validate::setHost 96 | * @covers \Yubikey\Validate::getHost 97 | */ 98 | public function testGetSetHost() 99 | { 100 | $host = 'test.foo.com'; 101 | 102 | $this->validate->setHost($host); 103 | $this->assertEquals($this->validate->getHost(), $host); 104 | } 105 | 106 | /** 107 | * Test that a valid random host is selected if none was previously set 108 | * @covers \Yubikey\Validate::getHost 109 | */ 110 | public function testGetRandomHost() 111 | { 112 | $host1 = $this->validate->getHost(); 113 | $this->assertNotEquals($host1, null); 114 | } 115 | 116 | /** 117 | * Test that the signature generation is valid 118 | * @covers \Yubikey\Validate::generateSignature 119 | */ 120 | public function testSignatureGenerate() 121 | { 122 | $data = array('foo' => 'bar'); 123 | $key = $this->validate->getApiKey(); 124 | $hash = preg_replace( 125 | '/\+/', '%2B', 126 | base64_encode(hash_hmac('sha1', http_build_query($data), $key, true)) 127 | ); 128 | 129 | $signature = $this->validate->generateSignature($data); 130 | $this->assertEquals($hash, $signature); 131 | } 132 | 133 | /** 134 | * Test that an exception is thrown when the API is invalid (null or empty) 135 | * @covers \Yubikey\Validate::generateSignature 136 | * @expectedException \InvalidArgumentException 137 | */ 138 | public function testSignatureGenerateNoApiKey() 139 | { 140 | $key = null; 141 | $data = array('foo' => 'bar'); 142 | $validate = new \Yubikey\Validate($key, $this->clientId); 143 | $hash = preg_replace( 144 | '/\+/', '%2B', 145 | base64_encode(hash_hmac('sha1', http_build_query($data), $key, true)) 146 | ); 147 | 148 | $signature = $validate->generateSignature($data); 149 | } 150 | 151 | /** 152 | * Add a new Host to the list 153 | * @covers \Yubikey\Validate::addHost 154 | */ 155 | public function testAddNewHost() 156 | { 157 | $this->validate->addHost('test.com'); 158 | $this->assertTrue( 159 | in_array('test.com', $this->validate->getHosts()) 160 | ); 161 | } 162 | 163 | /** 164 | * Set the new Hosts list (override) 165 | * @covers \Yubikey\Validate::setHosts 166 | * @covers \Yubikey\Validate::getHosts 167 | */ 168 | public function testSetHosts() 169 | { 170 | $hosts = array('foo.com'); 171 | $this->validate->setHosts($hosts); 172 | 173 | $this->assertEquals( 174 | $this->validate->getHosts(), 175 | $hosts 176 | ); 177 | } 178 | } 179 | 180 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | . 6 | 7 | 8 | --------------------------------------------------------------------------------