├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── src └── Phalcon │ └── Session │ └── Adapter │ └── Jwt.php └── tests ├── Phalcon └── Session │ └── Adapter │ └── JwtTest.php └── _bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | vendor 4 | build 5 | phpunit.xml 6 | composer.lock 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Rootwork InfoTech LLC 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of phalcon-jwt nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phalcon JWT 2 | JWT session drop-in for Phalcon 2. 3 | 4 | ## Installation 5 | 6 | Install composer in a common location or in your project: 7 | 8 | ```bash 9 | curl -s http://getcomposer.org/installer | php 10 | ``` 11 | 12 | Create the composer.json file as follows: 13 | 14 | ```json 15 | { 16 | "require": { 17 | "rootwork/phalcon-jwt": "dev-master" 18 | } 19 | } 20 | ``` 21 | 22 | Run the composer installer: 23 | 24 | ```bash 25 | php composer.phar install 26 | ``` 27 | 28 | ## Usage 29 | 30 | ### Loading the JWT session service 31 | ```php 32 | $jwtKey = 'c8cb6ae1fb193e1e9d3d2d6553479755bbe59e34e2b965629ee4346e4c4902646c93ccd6cd7fd6d2392f300d251632e64bf1a1c260adf1b7219e8caa6dc7d27e'; 33 | $di = new FactoryDefault(); 34 | 35 | // Load the Jwt session 36 | $di->setShared('session', function () use ($config) { 37 | $session = new Jwt(['key' => $jwtKey]); 38 | $session->start(); 39 | 40 | return $session; 41 | }); 42 | ``` 43 | 44 | ### Starting a new session 45 | ```php 46 | // In your login controller/action 47 | $session = $this->session; 48 | $session->set('sub', $userId); 49 | $session->write(); 50 | ``` 51 | 52 | ### Accessing an active session via the user's JWT cookie 53 | ```php 54 | // Usually in a security plugin 55 | if ($sub = $this->session->get('sub')) { 56 | if ($user = Users::findFirstById($sub)) { 57 | $this->getDi()->setShared('user', $user); 58 | } else { 59 | $this->getDi()->getShared('session')->destroy(); 60 | } 61 | } 62 | ``` 63 | 64 | ### Ending the session 65 | ```php 66 | // Logging the user out 67 | $this->session->destroy(); 68 | ``` 69 | 70 | ## Generating a secret key 71 | 72 | Easily done from a PHP prompt. 73 | 74 | ``` 75 | php -a 76 | echo bin2hex(openssl_random_pseudo_bytes(64)); 77 | c8cb6ae1fb193e1e9d3d2d6553479755bbe59e34e2b965629ee4346e4c4902646c93ccd6cd7fd6d2392f300d251632e64bf1a1c260adf1b7219e8caa6dc7d27e 78 | ``` 79 | 80 | Then in your code: 81 | ```php 82 | // In the real world, this would go in your application configuration. 83 | $jwtKey = 'c8cb6ae1fb193e1e9d3d2d6553479755bbe59e34e2b965629ee4346e4c4902646c93ccd6cd7fd6d2392f300d251632e64bf1a1c260adf1b7219e8caa6dc7d27e'; 84 | ``` 85 | 86 | ## About JWTs 87 | Phalcon JWT uses the Firebase JWT library. To learn more about it and JSON Web Tokens in general, visit: 88 | https://github.com/firebase/php-jwt 89 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rootwork/phalcon-jwt", 3 | "description": "A JWT implementation for Phalcon 2", 4 | "license": "BSD-3-Clause", 5 | "keywords": ["rootwork", "phalcon", "jwt"], 6 | "homepage": "https://github.com/rootworkit/phalcon-jwt", 7 | "authors": [ 8 | { 9 | "name": "Mike Soule", 10 | "email": "mike@rootwork.it" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.4", 15 | "firebase/php-jwt": "^3.0" 16 | }, 17 | "require-dev": { 18 | "phalcon/devtools": "dev-master", 19 | "ext-phalcon": ">=2.0.4", 20 | "phpunit/phpunit": "5.1.*" 21 | }, 22 | "autoload": { 23 | "psr-4": { "Rootwork\\Phalcon\\": "src/Phalcon/" } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { "Rootwork\\Test\\": "tests/" } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Phalcon/Session/Adapter/Jwt.php: -------------------------------------------------------------------------------- 1 | 16 | * @package Rootwork\Phalcon\Session\Adapter 17 | * 18 | * @property string $iss 19 | * @property string $sub 20 | * @property string $aud 21 | * @property string $exp 22 | * @property string $nbf 23 | * @property string $iat 24 | * @property string $jti 25 | * @property string $typ 26 | */ 27 | class Jwt extends Adapter implements AdapterInterface 28 | { 29 | 30 | /** 31 | * @var array 32 | */ 33 | protected $defaultOptions = [ 34 | 'algorithm' => 'HS256', 35 | 'lifetime' => 900, 36 | 'tokenName' => 'X-Token', 37 | ]; 38 | 39 | /** 40 | * JWT payload 41 | * 42 | * @var array 43 | */ 44 | protected $payload = [ 45 | 'iss' => null, // Issuer: server name 46 | 'sub' => null, // Subject: Usually a user ID 47 | 'aud' => null, // Audience: Who the claim is meant for (rarely used) 48 | 'exp' => null, // Expire: time token expires 49 | 'nbf' => null, // Not before: time token becomes valid 50 | 'iat' => null, // Issued at: time when the token was generated 51 | 'jti' => null, // Json Token Id: an unique identifier for the token 52 | 'typ' => null, // Type: Mirrors the typ header (rarely used) 53 | ]; 54 | 55 | /** 56 | * @var bool 57 | */ 58 | protected $authenticated = false; 59 | 60 | /** 61 | * Class constructor. 62 | * 63 | * @param array $options 64 | * @throws Exception 65 | */ 66 | public function __construct($options = null) 67 | { 68 | if (!isset($options['key'])) { 69 | throw new Exception('An encryption key is required'); 70 | } 71 | 72 | parent::__construct($options); 73 | } 74 | 75 | /** 76 | * Start the session 77 | * 78 | * @return bool 79 | */ 80 | public function start() 81 | { 82 | if (!headers_sent() && $this->status() !== self::SESSION_ACTIVE) { 83 | $this->decode(); 84 | $this->_started = true; 85 | 86 | return true; 87 | } 88 | 89 | return false; 90 | } 91 | 92 | /** 93 | * Set a session value 94 | * 95 | * @param string $index 96 | * @param mixed $value 97 | */ 98 | public function set($index, $value) 99 | { 100 | $this->payload[$index] = $value; 101 | } 102 | 103 | /** 104 | * Get a session value 105 | * 106 | * @param string $index 107 | * @param mixed $default 108 | * @param bool $remove 109 | * 110 | * @return mixed|null 111 | */ 112 | public function get($index, $default = null, $remove = false) 113 | { 114 | if (array_key_exists($index, $this->payload)) { 115 | $value = $this->payload[$index]; 116 | 117 | if ($remove) { 118 | unset($this->payload[$index]); 119 | } 120 | 121 | return $value; 122 | } 123 | 124 | return $default; 125 | } 126 | 127 | /** 128 | * @param string $index 129 | * @return bool 130 | */ 131 | public function has($index) 132 | { 133 | return isset($this->payload[$index]); 134 | } 135 | 136 | /** 137 | * Remove a value from the token 138 | * 139 | * @param string $index 140 | */ 141 | public function remove($index) 142 | { 143 | unset($this->payload[$index]); 144 | } 145 | 146 | /** 147 | * Set the token ID 148 | * 149 | * @param string $id 150 | */ 151 | public function setId($id) 152 | { 153 | $this->payload['jti'] = $id; 154 | } 155 | 156 | /** 157 | * Get the token ID 158 | * 159 | * @return mixed 160 | */ 161 | public function getId() 162 | { 163 | if (empty($this->payload['jti'])) { 164 | $this->regenerateId(); 165 | } 166 | 167 | return $this->payload['jti']; 168 | } 169 | 170 | /** 171 | * Get the payload 172 | * 173 | * @return array 174 | */ 175 | public function getPayload() 176 | { 177 | return $this->payload; 178 | } 179 | 180 | /** 181 | * Encode and return the JWT. 182 | * 183 | * @return string 184 | */ 185 | public function getEncodedJwt() 186 | { 187 | $now = time(); 188 | 189 | $this->jti = $this->getId(); 190 | $this->iat = $now; 191 | $this->nbf = $now; 192 | $this->exp = $now + $this->_options['lifetime']; 193 | $this->iss = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null; 194 | 195 | $payload = array_filter($this->payload); 196 | 197 | return JwtUtil::encode( 198 | $payload, 199 | $this->_options['key'], 200 | $this->_options['algorithm'] 201 | ); 202 | } 203 | 204 | /** 205 | * Write the token out to the client 206 | * 207 | * @return bool 208 | */ 209 | public function write() 210 | { 211 | $jwt = $this->getEncodedJwt(); 212 | 213 | return setcookie($this->getName(), $jwt, $this->exp, '/', null, null, true); 214 | } 215 | 216 | /** 217 | * Destroy the session 218 | * 219 | * @param bool $ignored 220 | * @return bool 221 | */ 222 | public function destroy($ignored = false) 223 | { 224 | $this->payload = array_fill_keys(array_keys($this->payload), null); 225 | $this->authenticated = false; 226 | 227 | return setcookie($this->getName(), null, 1, '/'); 228 | } 229 | 230 | /** 231 | * Check if the session is authenticated 232 | * 233 | * @return bool 234 | */ 235 | public function isAuthenticated() 236 | { 237 | return $this->authenticated; 238 | } 239 | 240 | /** 241 | * Check the session status 242 | * 243 | * @return int 244 | */ 245 | public function status() 246 | { 247 | if ($this->_started) { 248 | return self::SESSION_ACTIVE; 249 | } 250 | 251 | return self::SESSION_NONE; 252 | } 253 | 254 | /** 255 | * @param array $options 256 | */ 257 | public function setOptions(array $options) 258 | { 259 | $options = array_merge($this->defaultOptions, $options); 260 | 261 | parent::setOptions($options); 262 | } 263 | 264 | /** 265 | * Decode the request token 266 | * 267 | * @return bool 268 | */ 269 | protected function decode() 270 | { 271 | try { 272 | $payload = JwtUtil::decode( 273 | $this->getJwt(), 274 | $this->_options['key'], 275 | [$this->_options['algorithm']] 276 | ); 277 | 278 | foreach ($payload as $key => $val) { 279 | $this->set($key, $val); 280 | } 281 | 282 | $this->authenticated = true; 283 | 284 | return true; 285 | } catch (\Exception $e) { 286 | // Handle exception 287 | } 288 | 289 | return false; 290 | } 291 | 292 | /** 293 | * Get the JWT for this request 294 | * 295 | * @return string|null 296 | */ 297 | protected function getJwt() 298 | { 299 | $name = $this->getName(); 300 | 301 | if (isset($_COOKIE[$name])) { 302 | return $_COOKIE[$name]; 303 | } 304 | 305 | if (isset($_REQUEST[$name])) { 306 | return $_REQUEST[$name]; 307 | } 308 | 309 | if (isset($_SERVER[$name])) { 310 | return $_SERVER[$name]; 311 | } 312 | 313 | return null; 314 | } 315 | 316 | /** 317 | * Set the name to use for the session token (used as cookie or header name) 318 | * 319 | * @param string $name 320 | */ 321 | public function setName($name) 322 | { 323 | $this->_options['tokenName'] = $name; 324 | } 325 | 326 | /** 327 | * Get the token name 328 | * 329 | * @return string 330 | */ 331 | public function getName() 332 | { 333 | return $this->_options['tokenName']; 334 | } 335 | 336 | /** 337 | * Regenerate the token ID 338 | * 339 | * @param bool|true $ignored 340 | * @return $this 341 | */ 342 | public function regenerateId($ignored = true) 343 | { 344 | $this->set('jti', base64_encode(mcrypt_create_iv(32))); 345 | return $this; 346 | } 347 | 348 | /** 349 | * Override the parent destructor 350 | */ 351 | public function __destruct() 352 | { 353 | $this->_started = false; 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /tests/Phalcon/Session/Adapter/JwtTest.php: -------------------------------------------------------------------------------- 1 | 15 | * @package Rootwork\Test\Phalcon\Session\Adapter\Jwt 16 | */ 17 | class JwtTest extends TestCase 18 | { 19 | 20 | /** @var string JWT hash key */ 21 | public $jwtKey; 22 | 23 | /** @var array Cookies generated during tests */ 24 | public static $cookies = []; 25 | 26 | /** 27 | * Set up the test. 28 | */ 29 | public function setUp() 30 | { 31 | $this->jwtKey = bin2hex(openssl_random_pseudo_bytes(64)); 32 | } 33 | 34 | /** 35 | * Test reading and writing values to the JWT key. 36 | */ 37 | public function testReadAndWriteSession() 38 | { 39 | $session = new Jwt(['key' => $this->jwtKey]); 40 | $session->write(); 41 | 42 | $cookie = current(self::$cookies); 43 | 44 | $this->assertEquals(1, count(self::$cookies)); 45 | $this->assertEquals('X-Token', $cookie['name']); 46 | $this->assertNotFalse(base64_decode($session->jti)); 47 | $this->assertEquals(44, strlen($session->jti)); 48 | } 49 | 50 | /** 51 | * Test starting a session. 52 | * 53 | * @param bool $expected 54 | * @param string $arrName 55 | * 56 | * @dataProvider provideStart 57 | */ 58 | public function testStart($expected, $arrName = null) 59 | { 60 | $tmp = new Jwt(['key' => $this->jwtKey]); 61 | $tmp->write(); 62 | 63 | $cookie = current(self::$cookies); 64 | $jwt = $cookie['value']; 65 | unset($tmp); 66 | 67 | switch ($arrName) { 68 | case '_COOKIE': 69 | $_COOKIE['X-Token'] = $jwt; 70 | break; 71 | case '_REQUEST': 72 | $_REQUEST['X-Token'] = $jwt; 73 | break; 74 | case '_SERVER': 75 | $_SERVER['X-Token'] = $jwt; 76 | break; 77 | } 78 | 79 | $session = new Jwt(['key' => $this->jwtKey]); 80 | $actual = $session->start(); 81 | 82 | if (!$expected) { 83 | $actual = $session->start(); 84 | } 85 | 86 | $this->assertEquals($expected, $actual); 87 | } 88 | 89 | /** 90 | * Provides data for testing start(). 91 | * 92 | * @return array 93 | */ 94 | public function provideStart() 95 | { 96 | return [ 97 | [true, '_COOKIE'], 98 | [true, '_REQUEST'], 99 | [true, '_SERVER'], 100 | [false, null], 101 | ]; 102 | } 103 | } 104 | } 105 | 106 | namespace Rootwork\Phalcon\Session\Adapter { 107 | 108 | /** 109 | * Override for built-in setcookie() function. 110 | * 111 | * @param string $name 112 | * @param string $value 113 | * @param int $expire 114 | * @param string $path 115 | * @param string $domain 116 | * @param bool $secure 117 | * @param bool $httpOnly 118 | * @return bool 119 | */ 120 | function setcookie( 121 | $name, 122 | $value = "", 123 | $expire = 0, 124 | $path = "", 125 | $domain = "", 126 | $secure = false, 127 | $httpOnly = false 128 | ) { 129 | /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ 130 | \Rootwork\Test\Phalcon\Session\Adapter\JwtTest::$cookies[$name] = [ 131 | 'name' => $name, 132 | 'value' => $value, 133 | 'expire' => $expire, 134 | 'path' => $path, 135 | 'domain' => $domain, 136 | 'secure' => $secure, 137 | 'httpOnly' => $httpOnly 138 | ]; 139 | 140 | return true; 141 | } 142 | 143 | /** 144 | * Override for built-in headers_sent() function. 145 | * 146 | * @return bool 147 | */ 148 | function headers_sent() 149 | { 150 | return false; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /tests/_bootstrap.php: -------------------------------------------------------------------------------- 1 |