├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── src └── Klimesf │ └── Security │ ├── DI │ └── JWTUserStorageExtension.php │ ├── IIdentitySerializer.php │ ├── IdentitySerializer.php │ ├── JWT │ ├── FirebaseJWTWrapper.php │ └── IJsonWebTokenService.php │ └── JWTUserStorage.php └── tests └── php.ini-unix /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /vendor/ 3 | composer.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - 7.0 8 | - hhvm 9 | 10 | matrix: 11 | allow_failures: 12 | - php: 7.0 13 | - php: hhvm 14 | 15 | before_install: 16 | - composer self-update 17 | 18 | install: 19 | - composer install --no-interaction --prefer-source 20 | 21 | before_script: 22 | - composer create-project --prefer-source --no-interaction jakub-onderka/php-parallel-lint vendor/php-parallel-lint ~0.8 23 | - php vendor/php-parallel-lint/parallel-lint.php -e php,phpt --exclude vendor . 24 | - composer create-project --prefer-source --no-interaction nette/code-checker vendor/code-checker ~2.2 25 | - php vendor/code-checker/src/code-checker.php -d src 26 | - php vendor/code-checker/src/code-checker.php -d tests 27 | 28 | script: vendor/bin/tester -p php -c ./tests/php.ini-unix ./tests/ 29 | 30 | after_failure: 31 | - 'for i in $(find ./tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Filip Klimeš 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nette-jwt-user-storage 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/klimesf/nette-jwt-user-storage/version)](https://packagist.org/packages/klimesf/nette-jwt-user-storage) 4 | [![License](https://poser.pugx.org/klimesf/nette-jwt-user-storage/license)](https://packagist.org/packages/klimesf/nette-jwt-user-storage) 5 | [![Build Status](https://travis-ci.org/klimesf/nette-jwt-user-storage.svg)](https://travis-ci.org/klimesf/nette-jwt-user-storage) 6 | 7 | **DISCONTINUED** 8 | 9 | This repository is abandoned. You can use slepic's fork https://github.com/slepic/nette-jwt-user-storage. 10 | 11 | --- 12 | 13 | Nette IUserStorage implementation using JWT access token instead of PHP sessions. 14 | 15 | > Disclaimer: If you don't know what JWT is, please refer to 16 | > [JWT draft](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32) or to [JWT homepage](http://jwt.io/). 17 | 18 | On user login, the application stores `jwt_access_token` cookie instead of bad old `PHPSESSID` one. 19 | The cookie contains an encoded JWT signed by the application. The user authentication is then based 20 | on verifying the JWT rather than the session. 21 | 22 | > Warning: CSRF protection rules still apply! 23 | 24 | This means you no longer need to solve PHP session implementation, scaling and testing problems. 25 | All the things that you would normally store in the `SessionStorage` can be stored in a key-value 26 | storage, where the JWT is a key. 27 | 28 | This also means your application is ready to become SPA in the future. :) 29 | 30 | 31 | Configuration 32 | ------------- 33 | 34 | Register the extension in your `config.neon`. 35 | 36 | ```yml 37 | extensions: 38 | jwtUserStorage: Klimesf\Security\DI\JWTUserStorageExtension 39 | ``` 40 | 41 | Then configure its required properties. 42 | 43 | ```yml 44 | JWTUserStorage: 45 | privateKey: 'secret-cat' # this secret is used to sign the JWT 46 | algorithm: 'HS256' # this is the signing algorithm 47 | ``` 48 | 49 | Both the JWT and the cookie in which it's stored is by default set to expire in 20 days. If you want to fiddle 50 | with expiration time, use `expiration` option: 51 | 52 | ```yml 53 | JWTUserStorage: 54 | expiration: 20 days # sets JWT and cookie expiration time to 20 days (this is the default option) 55 | expiration: 20 minutes # sets JWT and cookie expiration time to 20 minutes 56 | expiration: false # sets JWT and cookie to never expire 57 | ``` 58 | 59 | By default, `jti` and `iat` (see [JWT draft](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32)) are added 60 | to your JWTs. If you don't want to use them, set `generateJti` and `generateIat` options to false. 61 | 62 | ```yml 63 | JWTUserStorage: 64 | generateJti: false # disables jti generation for your JWT access tokens 65 | generateIat: false # disables iat generation for your JWT access tokens 66 | ``` 67 | 68 | If you want to define your own `Nette\Security\IIdentity` serializer, which serializes your identity implementation 69 | into the JWT body, you can implement `Klimesf\Security\IIdentitySerializer` 70 | 71 | ``` 72 | namespace Your\Own; 73 | 74 | class IdentitySerializer implements \Klimesf\Security\IIdentitySerializer 75 | { 76 | // ... 77 | } 78 | ``` 79 | 80 | and register it in configuration. 81 | 82 | ```yml 83 | JWTUserStorage: 84 | identitySerializer: Your\Own\IdentitySerializer 85 | ``` 86 | 87 | 88 | And that's it, you're ready to go! 89 | 90 | 91 | Known issues 92 | ------------ 93 | 94 | - If you are developing an app with JWT User Storage and you still see `PHPSESSID` in your cookies, it's 95 | probably because [Tracy\Tracy](https://github.com/tracy/tracy) uses it. 96 | 97 | 98 | Discussion threads 99 | ------------------ 100 | 101 | - Czech discussion thread on [Nette Forum](https://forum.nette.org/cs/24081-nette-jwt-user-storage-dejte-sbohem-php-session#p161518) 102 | 103 | 104 | Literature 105 | ---------- 106 | 107 | - [Stormpath: Where to store JWTs](https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage/) 108 | - [Reddit: JWT vs session cookies](https://www.reddit.com/r/webdev/comments/3afcs9/jwt_vs_session_cookies_authentication/) 109 | - [Dev Kimchi](http://devkimchi.com/1622/can-json-web-token-jwt-be-an-alternative-for-session/) 110 | - [JTI Generation](https://github.com/bshaffer/oauth2-server-php/issues/265) 111 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "klimesf/nette-jwt-user-storage", 3 | "description": "Nette IUserStorage implementation using JWT instead of sessions.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Filip Klimes", 9 | "email": "filip@filipklimes.cz" 10 | } 11 | ], 12 | "minimum-stability": "stable", 13 | "require": { 14 | "php": ">= 5.4.0", 15 | "nette/security": "^2.3", 16 | "nette/http": "^2.3", 17 | "firebase/php-jwt": "^3.0", 18 | "nette/di": "^2.3" 19 | }, 20 | "require-dev": { 21 | "nette/tester": "^1.5" 22 | }, 23 | "autoload": { 24 | "psr-0": { 25 | "Klimesf\\Security": "src/" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Klimesf/Security/DI/JWTUserStorageExtension.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class JWTUserStorageExtension extends CompilerExtension 14 | { 15 | 16 | private $defaults = [ 17 | 'identitySerializer' => 'Klimesf\Security\IdentitySerializer', 18 | 'generateJti' => true, 19 | 'generateIat' => true, 20 | 'expiration' => '20 days', 21 | ]; 22 | 23 | public function loadConfiguration() 24 | { 25 | $builder = $this->getContainerBuilder(); 26 | $config = $this->getConfig($this->defaults); 27 | if (!array_key_exists('privateKey', $config) || !array_key_exists('algorithm', $config)) { 28 | throw new \UnexpectedValueException("Please configure the JWTUserStorage extensions using the section " . 29 | "'{$this->name}:' in your config file."); 30 | } 31 | 32 | $builder->addDefinition($this->prefix('firebaseJWTWrapper')) 33 | ->setClass('Klimesf\Security\JWT\FirebaseJWTWrapper'); 34 | 35 | $userStorageDefinition = $builder->addDefinition($this->prefix('jwtUserStorage')) 36 | ->setClass('Klimesf\Security\JWTUserStorage', 37 | [$config['privateKey'], $config['algorithm']]); 38 | $userStorageDefinition->addSetup('setGenerateIat', [$config['generateIat']]); 39 | $userStorageDefinition->addSetup('setGenerateJti', [$config['generateJti']]); 40 | 41 | // If expiration date is set, add service setup 42 | if ($config['expiration']) { 43 | $userStorageDefinition->addSetup('setExpiration', [$config['expiration']]); 44 | } 45 | 46 | $builder->addDefinition($this->prefix('identitySerializer')) 47 | ->setClass($config['identitySerializer']); 48 | 49 | // Disable Nette's default IUserStorage implementation 50 | $builder->getDefinition('security.userStorage')->setAutowired(false); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Klimesf/Security/IIdentitySerializer.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface IIdentitySerializer 15 | { 16 | 17 | /** 18 | * Serializes the IIdentity into an array, which will then be stored in 19 | * the JWT access token. 20 | * @param IIdentity $identity 21 | * @return array 22 | */ 23 | public function serialize(IIdentity $identity); 24 | 25 | /** 26 | * Deserializes the identity data from an array contained in the JWT and 27 | * loads into into IIdentity. 28 | * @param array $jwtData 29 | * @return IIdentity 30 | */ 31 | public function deserialize($jwtData); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Klimesf/Security/IdentitySerializer.php: -------------------------------------------------------------------------------- 1 | 12 | * @copyright 2015, Startupedia s.r.o. 13 | */ 14 | class IdentitySerializer implements IIdentitySerializer 15 | { 16 | 17 | /** 18 | * Serializes the IIdentity into an array, which will then be stored in 19 | * the JWT access token. 20 | * @param IIdentity $identity 21 | * @return array 22 | */ 23 | public function serialize(IIdentity $identity) 24 | { 25 | $jwtData['sub'] = $identity->getId(); 26 | $jwtData['roles'] = $identity->getRoles(); 27 | return $jwtData; 28 | } 29 | 30 | 31 | /** 32 | * Deserializes the identity data from an array contained in the JWT and 33 | * loads into into IIdentity. 34 | * @param array $jwtData 35 | * @return IIdentity 36 | */ 37 | public function deserialize($jwtData) 38 | { 39 | return array_key_exists('sub', $jwtData) && array_key_exists('roles', $jwtData) 40 | ? new Identity($jwtData['sub'], $jwtData['roles']) 41 | : null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Klimesf/Security/JWT/FirebaseJWTWrapper.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class FirebaseJWTWrapper implements IJsonWebTokenService 19 | { 20 | 21 | /** 22 | * Converts and signs a PHP object or array into a JWT string. 23 | * @param object|array $payload PHP object or array 24 | * @param string $key The secret key. 25 | * If the algorithm used is asymmetric, this is the private key 26 | * @param string $alg The signing algorithm. 27 | * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' 28 | * @param array $head An array with header elements to attach 29 | * @return string A signed JWT 30 | * @uses jsonEncode 31 | * @uses urlsafeB64Encode 32 | */ 33 | function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null) 34 | { 35 | return JWT::encode($payload, $key, $alg, $keyId, $head); 36 | } 37 | 38 | /** 39 | * Decodes a JWT string into a PHP object. 40 | * @param string $jwt The JWT 41 | * @param string|array|null $key The key, or map of keys. 42 | * If the algorithm used is asymmetric, this is the public key 43 | * @param array $allowed_algs List of supported verification algorithms 44 | * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' 45 | * @return object The JWT's payload as a PHP object 46 | * @throws DomainException Algorithm was not provided 47 | * @throws UnexpectedValueException Provided JWT was invalid 48 | * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed 49 | * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' 50 | * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' 51 | * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim 52 | * @uses jsonDecode 53 | * @uses urlsafeB64Decode 54 | */ 55 | function decode($jwt, $key, $allowed_algs = array()) 56 | { 57 | return JWt::decode($jwt, $key, $allowed_algs); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Klimesf/Security/JWT/IJsonWebTokenService.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface IJsonWebTokenService 18 | { 19 | 20 | /** 21 | * Converts and signs a PHP object or array into a JWT string. 22 | * 23 | * @param object|array $payload PHP object or array 24 | * @param string $key The secret key. 25 | * If the algorithm used is asymmetric, this is the private key 26 | * @param string $alg The signing algorithm. 27 | * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' 28 | * @param array $head An array with header elements to attach 29 | * 30 | * @return string A signed JWT 31 | * 32 | * @uses jsonEncode 33 | * @uses urlsafeB64Encode 34 | */ 35 | function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null); 36 | 37 | /** 38 | * Decodes a JWT string into a PHP object. 39 | * 40 | * @param string $jwt The JWT 41 | * @param string|array|null $key The key, or map of keys. 42 | * If the algorithm used is asymmetric, this is the public key 43 | * @param array $allowed_algs List of supported verification algorithms 44 | * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' 45 | * 46 | * @return object The JWT's payload as a PHP object 47 | * 48 | * @throws DomainException Algorithm was not provided 49 | * @throws UnexpectedValueException Provided JWT was invalid 50 | * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed 51 | * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' 52 | * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' 53 | * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim 54 | * 55 | * @uses jsonDecode 56 | * @uses urlsafeB64Decode 57 | */ 58 | function decode($jwt, $key, $allowed_algs = array()); 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Klimesf/Security/JWTUserStorage.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class JWTUserStorage implements IUserStorage 20 | { 21 | 22 | /** Name of the JWT access token cookie. */ 23 | const COOKIE_NAME = 'jwt_access_token'; 24 | 25 | /** 26 | * @var Request 27 | */ 28 | private $request; 29 | 30 | /** 31 | * @var Response 32 | */ 33 | private $response; 34 | 35 | /** 36 | * @var IJsonWebTokenService 37 | */ 38 | private $jwtService; 39 | 40 | /** 41 | * @var string 42 | */ 43 | private $privateKey; 44 | 45 | /** 46 | * @var string 47 | */ 48 | private $algorithm; 49 | 50 | /** 51 | * @var boolean 52 | */ 53 | private $generateJti = true; 54 | 55 | /** 56 | * @var boolean 57 | */ 58 | private $generateIat = true; 59 | 60 | /** 61 | * @var array 62 | */ 63 | private $jwtData; 64 | 65 | /** 66 | * @var string 67 | */ 68 | private $expirationTime; 69 | 70 | /** 71 | * @var int 72 | */ 73 | private $logoutReason; 74 | 75 | /** 76 | * @var IIdentitySerializer 77 | */ 78 | private $identitySerializer; 79 | 80 | /** 81 | * @var boolean 82 | */ 83 | private $cookieSaved; 84 | 85 | /** 86 | * JWTUserStorage constructor. 87 | * @param string $privateKey 88 | * @param string $algorithm 89 | * @param Request $request 90 | * @param Response $response 91 | * @param IJsonWebTokenService $jsonWebTokenService 92 | * @param IIdentitySerializer $identitySerializer 93 | */ 94 | public function __construct($privateKey, $algorithm, Request $request, 95 | Response $response, IJsonWebTokenService $jsonWebTokenService, 96 | IIdentitySerializer $identitySerializer) 97 | { 98 | $this->privateKey = $privateKey; 99 | $this->algorithm = $algorithm; 100 | $this->request = $request; 101 | $this->response = $response; 102 | $this->jwtService = $jsonWebTokenService; 103 | $this->identitySerializer = $identitySerializer; 104 | } 105 | 106 | /** 107 | * @param boolean $generateJti 108 | */ 109 | public function setGenerateJti($generateJti) 110 | { 111 | $this->generateJti = $generateJti; 112 | } 113 | 114 | /** 115 | * @param boolean $generateIat 116 | */ 117 | public function setGenerateIat($generateIat) 118 | { 119 | $this->generateIat = $generateIat; 120 | } 121 | 122 | /** 123 | * Sets the authenticated status of this user. 124 | * @param bool 125 | * @return $this 126 | */ 127 | function setAuthenticated($state) 128 | { 129 | $this->jwtData['is_authenticated'] = $state; 130 | if (!$state) { 131 | $this->logoutReason = self::MANUAL; 132 | } 133 | $this->saveJWTCookie(); 134 | return $this; 135 | } 136 | 137 | /** 138 | * Is this user authenticated? 139 | * @return bool 140 | */ 141 | function isAuthenticated() 142 | { 143 | $this->loadJWTCookie(); 144 | return array_key_exists('is_authenticated', $this->jwtData) ? $this->jwtData['is_authenticated'] : false; 145 | } 146 | 147 | /** 148 | * Sets the user identity. 149 | * @return void 150 | */ 151 | function setIdentity(IIdentity $identity = null) 152 | { 153 | if (!$identity) { 154 | $this->jwtData = ['is_authenticated' => false]; 155 | return; 156 | } 157 | $this->jwtData = array_merge( 158 | $this->jwtData, 159 | $this->identitySerializer->serialize($identity) 160 | ); 161 | $this->saveJWTCookie(); 162 | } 163 | 164 | /** 165 | * Returns current user identity, if any. 166 | * @return IIdentity|NULL 167 | */ 168 | function getIdentity() 169 | { 170 | $this->loadJWTCookie(); 171 | return $this->identitySerializer->deserialize($this->jwtData); 172 | } 173 | 174 | /** 175 | * Enables log out from the persistent storage after inactivity. 176 | * @param string|int|\DateTime $time number of seconds or timestamp 177 | * @param int $flags Log out when the browser is closed | Clear the identity from persistent storage? 178 | * @return void 179 | */ 180 | function setExpiration($time, $flags = 0) 181 | { 182 | $this->expirationTime = $flags & self::BROWSER_CLOSED ? 0 : $time; 183 | if ($time) { 184 | $time = DateTime::from($time)->format('U'); 185 | $this->jwtData['exp'] = $time; 186 | } else { 187 | unset($this->jwtData['exp']); 188 | } 189 | $this->saveJWTCookie(); 190 | } 191 | 192 | /** 193 | * Why was user logged out? 194 | * @return int 195 | */ 196 | function getLogoutReason() 197 | { 198 | return $this->logoutReason; 199 | } 200 | 201 | /** 202 | * Saves the JWT Access Token into HTTP cookie. 203 | */ 204 | private function saveJWTCookie() 205 | { 206 | if (empty($this->jwtData)) { 207 | $this->response->deleteCookie(self::COOKIE_NAME); 208 | return; 209 | } 210 | 211 | if ($this->generateIat) { 212 | $this->jwtData['iat'] = DateTime::from('NOW')->format('U'); 213 | } 214 | 215 | // Unset JTI if there was any 216 | unset($this->jwtData['jti']); 217 | if ($this->generateJti) { 218 | // Generate new JTI 219 | $this->jwtData['jti'] = hash('sha256', serialize($this->jwtData) . Random::generate(10)); 220 | } 221 | // Encode the JWT and set the cookie 222 | $jwt = $this->jwtService->encode($this->jwtData, $this->privateKey, $this->algorithm); 223 | $this->response->setCookie(self::COOKIE_NAME, $jwt, $this->expirationTime); 224 | $this->cookieSaved = true; // Set cookie saved flag to true, so loadJWTCookie() doesn't rewrite our data 225 | } 226 | 227 | /** 228 | * Loads JWT from HTTP cookie and stores the data into the $jwtData variable. 229 | * @return array|bool The JWT data as array or FALSE if there is no JWT cookie. 230 | */ 231 | private function loadJWTCookie() 232 | { 233 | if ($this->cookieSaved) { 234 | return true; 235 | } 236 | 237 | $jwtCookie = $this->request->getCookie(self::COOKIE_NAME); 238 | if (!$jwtCookie) { 239 | $this->logoutReason = self::INACTIVITY | self::BROWSER_CLOSED; 240 | return false; 241 | } 242 | 243 | try { 244 | $this->jwtData = (array) $this->jwtService->decode($jwtCookie, $this->privateKey, [$this->algorithm]); 245 | 246 | } catch (ExpiredException $e) { 247 | $this->logoutReason = self::INACTIVITY; 248 | return false; 249 | } 250 | 251 | return $this->jwtData; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /tests/php.ini-unix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klimesf/nette-jwt-user-storage/016e0fd66e57f4292c9d6fafbc289317e2ce163f/tests/php.ini-unix --------------------------------------------------------------------------------