├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── phpunit.xml.dist ├── src └── Phalcon │ └── Auth │ ├── Adapter.php │ ├── AdapterInterface.php │ ├── Auth.php │ ├── Middleware │ └── Micro.php │ └── TokenGetter │ ├── AdapterInterface.php │ ├── Handler │ ├── Adapter.php │ ├── Header.php │ └── QueryStr.php │ └── TokenGetter.php └── tests ├── Phalcon ├── AuthTest.php ├── MiddlewareMicroTest.php ├── TokenGetterHeaderTest.php ├── TokenGetterQueryStrTest.php └── TokenGetterTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | phpunit.phar 3 | phpunit.phar.asc 4 | composer.phar 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dan Bangayan 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phalcon-jwt-auth 2 | 3 | A simple JWT middleware for Phalcon Micro to handle stateless authentication. 4 | 5 | ## Installation 6 | ```bash 7 | $ composer require dmkit/phalcon-jwt-auth 8 | ``` 9 | or in your composer.json 10 | ```json 11 | { 12 | "require": { 13 | "dmkit/phalcon-jwt-auth" : "dev-master" 14 | } 15 | } 16 | 17 | ``` 18 | then run 19 | ```bash 20 | $ composer update 21 | ``` 22 | 23 | ## Usage 24 | 25 | ### Configuration - Loading the config service 26 | 27 | in config.ini or in any config file 28 | ```ini 29 | [jwtAuth] 30 | 31 | ; JWT Secret Key 32 | secretKey = 923753F2317FC1EE5B52DF23951B 33 | 34 | ; JWT default Payload 35 | 36 | ;; expiry time in minutes 37 | payload[exp] = 1440 38 | payload[iss] = phalcon-jwt-auth 39 | 40 | ; Micro Applications do not have a controller or dispatcher 41 | ; so to know the resource being called we have to check the actual URL. 42 | 43 | ; If you want to disable the middleware on certain routes or resource: 44 | ;; index 45 | ignoreUri[] = / 46 | 47 | ;; regex pattern with http methods 48 | ignoreUri[] = regex:/application/ 49 | ignoreUri[] = regex:/users/:POST,PUT 50 | 51 | ;; literal strings 52 | ignoreUri[] = /auth/user:POST,PUT 53 | ignoreUri[] = /auth/application 54 | ``` 55 | 56 | in bootstrap or index file 57 | ```php 58 | use Phalcon\Mvc\Micro; 59 | use Phalcon\Config\Adapter\Ini as ConfigIni; 60 | use Phalcon\Di\FactoryDefault; 61 | use Dmkit\Phalcon\Auth\Middleware\Micro as AuthMicro; 62 | 63 | // set default services 64 | $di = new FactoryDefault(); 65 | 66 | /** 67 | * IMPORTANT: 68 | * You must set "config" service that will load the configuration file. 69 | */ 70 | $config = new ConfigIni( APP_PATH . "app/config/config.ini"); 71 | $di->set( 72 | "config", 73 | function () use($config) { 74 | return $config; 75 | } 76 | ); 77 | 78 | $app = new Micro($di); 79 | 80 | // AUTH MICRO 81 | $auth = new AuthMicro($app); 82 | 83 | $app->handle(); 84 | ``` 85 | 86 | ### Configuration - Don't want to use a config file? then pass the config instead 87 | in bootstrap or index file 88 | ```php 89 | use Phalcon\Mvc\Micro; 90 | use Phalcon\Config\Adapter\Ini as ConfigIni; 91 | use Phalcon\Di\FactoryDefault; 92 | use Dmkit\Phalcon\Auth\Middleware\Micro as AuthMicro; 93 | 94 | // set default services 95 | $di = new FactoryDefault(); 96 | 97 | $app = new Micro($di); 98 | 99 | // SETUP THE CONFIG 100 | $authConfig = [ 101 | 'secretKey' => '923753F2317FC1EE5B52DF23951B1', 102 | 'payload' => [ 103 | 'exp' => 1440, 104 | 'iss' => 'phalcon-jwt-auth' 105 | ], 106 | 'ignoreUri' => [ 107 | '/', 108 | 'regex:/application/', 109 | 'regex:/users/:POST,PUT', 110 | '/auth/user:POST,PUT', 111 | '/auth/application' 112 | ] 113 | ]; 114 | 115 | // AUTH MICRO 116 | $auth = new AuthMicro($app, $authConfig); 117 | 118 | $app->handle(); 119 | ``` 120 | 121 | ### Authentication 122 | To make authenticated requests via http, you will need to set an authorization headers as follows: 123 | ``` 124 | Authorization: Bearer {yourtokenhere} 125 | ``` 126 | or pass the token as a query string 127 | ``` 128 | ?_token={yourtokenhere} 129 | ``` 130 | 131 | ### Callbacks 132 | 133 | By default if the authentication fails, the middleware will stop the execution of routes and will immediately return a response of 401 Unauthorized. If you want to add your own handler: 134 | ```php 135 | $auth->onUnauthorized(function($authMicro, $app) { 136 | 137 | $response = $app["response"]; 138 | $response->setStatusCode(401, 'Unauthorized'); 139 | $response->setContentType("application/json"); 140 | 141 | // to get the error messages 142 | $response->setContent(json_encode([$authMicro->getMessages()[0]])); 143 | $response->send(); 144 | 145 | // return false to stop the execution 146 | return false; 147 | }); 148 | ``` 149 | 150 | If you want an additional checking on the authentication, like intentionally expiring a token based on the payload issued date, you may do so: 151 | ```php 152 | $auth->onCheck(function($auth) { 153 | // to get the payload 154 | $data = $auth->data(); 155 | 156 | if($data['iat'] <= strtotime('-1 day')) ) { 157 | // return false to invalidate the authentication 158 | return false; 159 | } 160 | 161 | }); 162 | ``` 163 | 164 | ### The Auth service 165 | 166 | You can access the middleware by calling the "auth" service. 167 | ```php 168 | print_r( $app['auth']->data() ); 169 | 170 | print_r( $app->getDI()->get('auth')->data('email') ); 171 | 172 | // in your contoller 173 | print_r( $this->auth->data() ); 174 | ``` 175 | If you want to change the service name: 176 | ```php 177 | AuthMicro::$diName = 'jwtAuth'; 178 | ``` 179 | 180 | ### Creating a token 181 | 182 | In your controller or route handler 183 | ```php 184 | $payload = [ 185 | 'sub' => $user->id, 186 | 'email' => $user->email, 187 | 'username' => $user->username, 188 | 'role' => 'admin', 189 | 'iat' => time(), 190 | ]; 191 | $token = $this->auth->make($payload); 192 | ``` 193 | 194 | ### Accessing the authenticated user / data 195 | In your controller or route handler 196 | ```php 197 | echo $this->auth->id(); // will look for sub or id payload 198 | 199 | echo $this->auth->data(); // return all payload 200 | 201 | echo $this->auth->data('email'); 202 | ``` 203 | 204 | 205 | ### Extending 206 | If you want to add your own middleware or play around: 207 | ```php 208 | Dmkit\Phalcon\Auth\Auth.php and its adapters - does all the authentication 209 | 210 | Dmkit\Phalcon\Auth\TokenGetter\TokenGetter.php and its adapters - does the parsing or getting of token 211 | ``` 212 | 213 | ### JWT 214 | Phalcon JWT Auth uses the Firebase JWT library. To learn more about it and JSON Web Tokens in general, visit: https://github.com/firebase/php-jwt 215 | https://jwt.io/introduction/ 216 | 217 | ### Tests 218 | Install PHPUnit https://phpunit.de/getting-started.html 219 | ```php 220 | $ phpunit --configuration phpunit.xml.dist 221 | PHPUnit 5.6.5 by Sebastian Bergmann and contributors. 222 | 223 | ......["missing token"].["members option"].["members put"].["members put"].["Expired token"].["members post"].... 15 / 15 (100%) 224 | 225 | Time: 73 ms, Memory: 10.00MB 226 | 227 | OK (15 tests, 27 assertions) 228 | 229 | ``` 230 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dmkit/phalcon-jwt-auth", 3 | "description": "A simple JWT middleware for Phalcon Micro to handle stateless authentication", 4 | "keywords": ["phalcon", "jwt", "authentication"], 5 | "type": "library", 6 | "authors": [ 7 | { 8 | "name": "Dan Bangayan", 9 | "email": "danny.bangayan@gmail.com", 10 | "role": "Developer" 11 | } 12 | ], 13 | "minimum-stability": "dev", 14 | "require": { 15 | "php": ">=7.0", 16 | "ext-phalcon": "^3.0", 17 | "firebase/php-jwt": "^5.0" 18 | }, 19 | "autoload": { 20 | "psr-4": { "Dmkit\\Phalcon\\Auth\\": "src/Phalcon/Auth/" } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "496cba365a771cb808ba800ca70f4c02", 8 | "content-hash": "9be19dcdabfd8358605647c83c52f881", 9 | "packages": [ 10 | { 11 | "name": "firebase/php-jwt", 12 | "version": "v4.0.0", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/firebase/php-jwt.git", 16 | "reference": "dccf163dc8ed7ed6a00afc06c51ee5186a428d35" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/firebase/php-jwt/zipball/dccf163dc8ed7ed6a00afc06c51ee5186a428d35", 21 | "reference": "dccf163dc8ed7ed6a00afc06c51ee5186a428d35", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": ">=5.3.0" 26 | }, 27 | "type": "library", 28 | "autoload": { 29 | "psr-4": { 30 | "Firebase\\JWT\\": "src" 31 | } 32 | }, 33 | "notification-url": "https://packagist.org/downloads/", 34 | "license": [ 35 | "BSD-3-Clause" 36 | ], 37 | "authors": [ 38 | { 39 | "name": "Neuman Vong", 40 | "email": "neuman+pear@twilio.com", 41 | "role": "Developer" 42 | }, 43 | { 44 | "name": "Anant Narayanan", 45 | "email": "anant@php.net", 46 | "role": "Developer" 47 | } 48 | ], 49 | "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", 50 | "homepage": "https://github.com/firebase/php-jwt", 51 | "time": "2016-07-18 04:51:16" 52 | } 53 | ], 54 | "packages-dev": [], 55 | "aliases": [], 56 | "minimum-stability": "dev", 57 | "stability-flags": [], 58 | "prefer-stable": false, 59 | "prefer-lowest": false, 60 | "platform": { 61 | "php": ">=7.0", 62 | "ext-phalcon": "^3.0" 63 | }, 64 | "platform-dev": [] 65 | } 66 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./tests 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Phalcon/Auth/Adapter.php: -------------------------------------------------------------------------------- 1 | leeway = $this->minToSec($mins); 46 | } 47 | 48 | /** 49 | * Sets algorith for hashing JWT. 50 | * See available Algos on JWT::$supported_algs 51 | * 52 | * @param int $mins 53 | * 54 | */ 55 | public function setAlgo(string $alg) { 56 | $this->algo = $alg; 57 | } 58 | 59 | /** 60 | * Decodes JWT. 61 | * 62 | * @param string $token 63 | * @param string $key 64 | * 65 | * @return array 66 | */ 67 | protected function decode($token, $key) 68 | { 69 | try { 70 | if($this->leeway) { 71 | JWT::$leeway = $this->leeway; 72 | } 73 | 74 | $payload = (array) JWT::decode($token, $key, [$this->algo]); 75 | 76 | return $payload; 77 | 78 | } catch(\Exception $e) { 79 | $this->appendMessage($e->getMessage()); 80 | return false; 81 | 82 | } 83 | } 84 | 85 | /** 86 | * Encodes array into JWT. 87 | * 88 | * @param array $payload 89 | * @param string $key 90 | * 91 | * @return string 92 | */ 93 | protected function encode($payload, $key) 94 | { 95 | if( isset($payload['exp']) ) { 96 | $payload['exp'] = time() + $this->minToSec($payload['exp']); 97 | } 98 | return JWT::encode($payload, $key, $this->algo); 99 | } 100 | 101 | /** 102 | * Adds string to error messages. 103 | * 104 | * @param string $msg 105 | * 106 | */ 107 | public function appendMessage(string $msg) 108 | { 109 | $this->errorMsgs[] = $msg; 110 | } 111 | 112 | /** 113 | * Returns error messages 114 | * 115 | * @return array 116 | */ 117 | public function getMessages() 118 | { 119 | return $this->errorMsgs; 120 | } 121 | 122 | /** 123 | * Returns JWT payload sub or payload id. 124 | * 125 | * @return string 126 | */ 127 | public function id() 128 | { 129 | return $this->payload['sub'] ?? $this->payload['id'] ?? NULL; 130 | } 131 | 132 | /** 133 | * Returns payload or value of payload key. 134 | * 135 | * @param array $payload 136 | * @param string $key 137 | * 138 | * @return array|string 139 | */ 140 | public function data($field=NULL) 141 | { 142 | return ( !$field ? $this->payload : $this->payload[$field] ); 143 | } 144 | } -------------------------------------------------------------------------------- /src/Phalcon/Auth/AdapterInterface.php: -------------------------------------------------------------------------------- 1 | encode($payload, $key); 28 | } 29 | 30 | /** 31 | * Adds callback on check method. 32 | * 33 | * @param callable $callback 34 | * 35 | */ 36 | public function onCheck(callable $callback) 37 | { 38 | $this->_onCheckCb[] = $callback; 39 | } 40 | 41 | /** 42 | * Checks and validates JWT. 43 | * Calls the oncheck callbacks and pass self as parameter. 44 | * 45 | * @param Dmkit\Phalcon\Auth\TokenGetter\AdapterInterface $parser 46 | * @param string $key 47 | * 48 | * @return bool 49 | */ 50 | public function check(TokenGetter $parser, string $key) : bool 51 | { 52 | $token = $parser->parse(); 53 | 54 | if(!$token) { 55 | $this->appendMessage('missing token'); 56 | return false; 57 | } 58 | 59 | $payload = $this->decode($token, $key); 60 | if(!$payload || empty($payload)) { 61 | return false; 62 | } 63 | 64 | $this->payload = $payload; 65 | 66 | // if any of the callback return false, this will immediately return false 67 | foreach($this->_onCheckCb as $callback) { 68 | if( $callback($this) === false ) { 69 | return false; 70 | } 71 | } 72 | 73 | return true; 74 | } 75 | } -------------------------------------------------------------------------------- /src/Phalcon/Auth/Middleware/Micro.php: -------------------------------------------------------------------------------- 1 | getDI()->has(self::$configDi)) { 69 | throw new \InvalidArgumentException('missing DI config jwtAuth and config param'); 70 | } 71 | 72 | if(!$config && !isset($app[self::$configDi]->{self::$configSection})) { 73 | throw new \InvalidArgumentException('missing DI config jwtAuth and config param'); 74 | } 75 | 76 | $this->config = $config ?? $app[self::$configDi]->{self::$configSection}; 77 | 78 | if( !is_array($this->config) ) { 79 | $this->config = (array) $this->config; 80 | } 81 | 82 | if(isset($this->config['ignoreUri'])) { 83 | $this->ignoreUri = $this->config['ignoreUri']; 84 | } 85 | 86 | // secret key is required 87 | if(!isset($this->config['secretKey'])) { 88 | throw new \InvalidArgumentException('missing jwt secret key'); 89 | } 90 | 91 | $this->secretKey = $this->config['secretKey']; 92 | $this->payload = (array) $this->config['payload'] ?? []; 93 | 94 | $this->app = $app; 95 | $this->auth = $auth; 96 | if($auth === NULL) { 97 | $this->auth = new Auth; 98 | } 99 | 100 | $this->setDi(); 101 | $this->setEventChecker(); 102 | } 103 | 104 | 105 | /** 106 | * Ignore OPTIONS for CORS support 107 | * 108 | */ 109 | public function setIgnoreOptionsMethod() 110 | { 111 | $this->ignoreOptionsMethod = true; 112 | } 113 | 114 | /** 115 | * Checks if OPTIONS METHOD Should be ignored 116 | * 117 | */ 118 | public function isIgnoreOptionsMethod() 119 | { 120 | return $this->ignoreOptionsMethod; 121 | } 122 | 123 | /** 124 | * Sets DI 125 | * 126 | */ 127 | protected function setDi() 128 | { 129 | $this->app[self::$diName] = $this; 130 | } 131 | 132 | /** 133 | * Sets event authentication. 134 | * 135 | */ 136 | protected function setEventChecker() 137 | { 138 | $diName = self::$diName; 139 | 140 | $eventsManager = $this->app->getEventsManager() ?? new EventsManager(); 141 | $eventsManager->attach( 142 | "micro:beforeExecuteRoute", 143 | function (Event $event, $app) use($diName) { 144 | $auth = $app[$diName]; 145 | 146 | // check if it has CORS support 147 | if ($auth->isIgnoreOptionsMethod() && $app['request']->getMethod() == 'OPTIONS') { 148 | return true; 149 | } 150 | 151 | if($auth->isIgnoreUri()) { 152 | /** 153 | * Let's try to parse if there's a token 154 | * but we don't want to get an invalid token 155 | */ 156 | if( !$auth->check() && $this->getMessages()[0] != 'missing token') 157 | { 158 | return $auth->unauthorized(); 159 | } 160 | 161 | return true; 162 | } 163 | 164 | if($auth->check()) { 165 | return true; 166 | } 167 | 168 | return $auth->unauthorized(); 169 | } 170 | ); 171 | 172 | $this->app->setEventsManager($eventsManager); 173 | } 174 | 175 | /** 176 | * Checks the uri and method if it has a match in the passed self::$ignoreUris. 177 | * 178 | * @param string $requestUri 179 | * @param string $requestMethod HTTP METHODS 180 | * 181 | * @return bool 182 | */ 183 | protected function hasMatchIgnoreUri($requestUri, $requestMethod) 184 | { 185 | foreach($this->ignoreUri as $uri) { 186 | if(strpos($uri, 'regex:') === false) { 187 | $type = 'str'; 188 | } else { 189 | $type = 'regex'; 190 | $uri = str_replace('regex:', '', $uri); 191 | } 192 | 193 | list($pattern, $methods) = ( strpos($uri, ':') === false ? [$uri, false] : explode(':', $uri ) ); 194 | $methods = ( !$methods || empty($methods) ? false : explode(',', $methods) ); 195 | 196 | $match = ( $type == 'str' ? $requestUri == $pattern : preg_match("#{$pattern}#", $requestUri) ); 197 | if( $match && (!$methods || in_array($requestMethod, $methods)) ) { 198 | return true; 199 | } 200 | } 201 | 202 | return false; 203 | } 204 | 205 | /** 206 | * Checks if the URI and HTTP METHOD can bypass the authentication. 207 | * 208 | * @return bool 209 | */ 210 | public function isIgnoreUri() 211 | { 212 | if(!$this->ignoreUri) { 213 | return false; 214 | } 215 | 216 | // access request object 217 | $request = $this->app['request']; 218 | 219 | // url 220 | $uri = $request->getURI(); 221 | 222 | // http method 223 | $method = $request->getMethod(); 224 | 225 | return $this->hasMatchIgnoreUri($uri, $method); 226 | } 227 | 228 | /** 229 | * Authenticates. 230 | * 231 | * @return bool 232 | */ 233 | public function check() 234 | { 235 | $request = $this->app['request']; 236 | $getter = new TokenGetter( new Header($request), new QueryStr($request)); 237 | return $this->auth->check($getter, $this->secretKey); 238 | } 239 | 240 | /** 241 | * Authenticates. 242 | * 243 | * @return bool 244 | */ 245 | public function make($data) 246 | { 247 | $payload = array_merge($this->payload, $data); 248 | return $this->auth->make($payload, $this->secretKey); 249 | } 250 | 251 | /** 252 | * Adds a callback to the Check call 253 | * 254 | * @param callable $callback 255 | */ 256 | public function onCheck($callback) 257 | { 258 | $this->auth->onCheck($callback); 259 | } 260 | 261 | /** 262 | * Sets the unauthorized return 263 | * 264 | * @param callable $callback 265 | */ 266 | public function onUnauthorized(callable $callback) 267 | { 268 | $this->_onUnauthorized = $callback; 269 | } 270 | 271 | /** 272 | * Calls the unauthorized function / callback 273 | * 274 | * @return bool return false to cancel the router 275 | */ 276 | public function unauthorized() { 277 | if($this->_onUnauthorized) { 278 | return call_user_func($this->_onUnauthorized, $this, $this->app); 279 | } 280 | 281 | $response = $this->app["response"]; 282 | $response->setStatusCode(401, 'Unauthorized'); 283 | $response->setContentType("application/json"); 284 | $response->setContent(json_encode([$this->getMessages()[0]])); 285 | 286 | // CORS 287 | if($this->isIgnoreOptionsMethod()) { 288 | $response->setHeader("Access-Control-Allow-Origin", '*') 289 | ->setHeader("Access-Control-Allow-Methods", 'GET,PUT,POST,DELETE,OPTIONS') 290 | ->setHeader("Access-Control-Allow-Headers", 'Origin, X-Requested-With, Content-Range, Content-Disposition, Content-Type, Authorization') 291 | ->setHeader("Access-Control-Allow-Credentials", true); 292 | } 293 | 294 | $response->send(); 295 | return false; 296 | } 297 | 298 | /** 299 | * Returns error messages 300 | * 301 | * @return array 302 | */ 303 | public function getMessages() 304 | { 305 | return $this->auth->getMessages(); 306 | } 307 | 308 | /** 309 | * Returns JWT payload sub or payload id. 310 | * 311 | * @return string 312 | */ 313 | public function id() 314 | { 315 | return $this->auth->id(); 316 | } 317 | 318 | /** 319 | * Returns payload or value of payload key. 320 | * 321 | * @param array $payload 322 | * @param string $key 323 | * 324 | * @return array|string 325 | */ 326 | public function data($field=NULL) 327 | { 328 | return $this->auth->data($field); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/Phalcon/Auth/TokenGetter/AdapterInterface.php: -------------------------------------------------------------------------------- 1 | _Request = $request; 28 | } 29 | 30 | /** 31 | * Sets the key for fetching 32 | * 33 | */ 34 | public function setKey(string $key) 35 | { 36 | $this->key = $key; 37 | } 38 | } -------------------------------------------------------------------------------- /src/Phalcon/Auth/TokenGetter/Handler/Header.php: -------------------------------------------------------------------------------- 1 | _Request->getHeader($this->key); 27 | 28 | if(!$raw_token) { 29 | return ''; 30 | } 31 | 32 | return trim( str_ireplace($this->prefix, '', $raw_token)); 33 | } 34 | 35 | /** 36 | * Sets the header value prefix 37 | * 38 | */ 39 | public function setPrefix(string $prefix) 40 | { 41 | $this->prefix = $prefix; 42 | } 43 | } -------------------------------------------------------------------------------- /src/Phalcon/Auth/TokenGetter/Handler/QueryStr.php: -------------------------------------------------------------------------------- 1 | _Request->getQuery($this->key) ?? '')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Phalcon/Auth/TokenGetter/TokenGetter.php: -------------------------------------------------------------------------------- 1 | getters = $getters; 23 | } 24 | 25 | /** 26 | * Calls the getters parser and returns the token 27 | * 28 | * @return string 29 | */ 30 | public function parse() : string 31 | { 32 | foreach($this->getters as $getter) 33 | { 34 | $token = $getter->parse(); 35 | if($token) { 36 | return $token; 37 | } 38 | } 39 | return ''; 40 | } 41 | } -------------------------------------------------------------------------------- /tests/Phalcon/AuthTest.php: -------------------------------------------------------------------------------- 1 | secretKey = 'secret key'; 24 | 25 | $this->options = [ 26 | 'sub' => 123, 27 | 'exp' => 120 28 | ]; 29 | 30 | $options = $this->options; 31 | $options['exp'] = strtotime('+2 hours'); 32 | 33 | $this->jwt = JWT::encode($options, $this->secretKey); 34 | } 35 | 36 | public function testMake() 37 | { 38 | $auth = new Auth; 39 | 40 | // pass exp as constructor 41 | $token = $auth->make($this->options, $this->secretKey); 42 | $this->assertEquals($this->jwt, $token); 43 | } 44 | 45 | public function testWithEmptyAuth() 46 | { 47 | $auth = new Auth; 48 | $auth->id(); 49 | $this->assertEquals(NULL, $auth->id()); 50 | } 51 | 52 | public function testCheckSuccess() 53 | { 54 | $response = $this->createMock(RequestInterface::class); 55 | $response->method('getQuery')->willReturn($this->jwt); 56 | $response->method('getHeader')->willReturn(''); 57 | 58 | $query = new QueryStr($response); 59 | $header = new Header($response); 60 | 61 | $parser = new TokenGetter($header, $query); 62 | 63 | $auth = new Auth; 64 | 65 | $this->assertTrue($auth->check($parser, $this->secretKey)); 66 | 67 | $this->assertEquals(123, $auth->id()); 68 | 69 | $options = $this->options; 70 | $options['exp'] = strtotime('+2 hours'); 71 | 72 | $this->assertEquals($options, $auth->data()); 73 | $this->assertEquals($options['sub'], $auth->data('sub')); 74 | } 75 | 76 | public function testCheckCallback() 77 | { 78 | $response = $this->createMock(RequestInterface::class); 79 | $response->method('getQuery')->willReturn($this->jwt); 80 | 81 | 82 | $auth = new Auth; 83 | 84 | $auth->onCheck(function($auth) { 85 | $auth->appendMessage('callback 1'); 86 | }); 87 | 88 | $auth->onCheck(function($auth) { 89 | $auth->appendMessage('callback 2'); 90 | return false; 91 | }); 92 | 93 | $auth->onCheck(function($auth) { 94 | $auth->appendMessage('callback 3'); 95 | }); 96 | 97 | $this->assertTrue( !$auth->check(new QueryStr($response), $this->secretKey) ); 98 | 99 | // makse sure callback were properly called 100 | $expected_errors = [ 101 | 'callback 1', 'callback 2' 102 | ]; 103 | $this->assertEquals($expected_errors, $auth->getMessages()); 104 | } 105 | 106 | 107 | public function testCheckFail() 108 | { 109 | // let's expired the jwt 110 | $response = $this->createMock(RequestInterface::class); 111 | $response->method('getQuery')->willReturn($this->jwt); 112 | 113 | $auth = new Auth; 114 | 115 | JWT::$timestamp = strtotime('+1 week'); 116 | 117 | $this->assertTrue( !$auth->check(new QueryStr($response), $this->secretKey) ); 118 | 119 | $expected_errors = ['Expired token']; 120 | 121 | $this->assertEquals($expected_errors, $auth->getMessages()); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /tests/Phalcon/MiddlewareMicroTest.php: -------------------------------------------------------------------------------- 1 | app = new Micro($di); 24 | 25 | $this->config = [ 26 | 'secretKey' => 'secret key', 27 | 'payload' => [ 28 | "sub" => "1234567890", 29 | "name" => "John Doe", 30 | "admin" => true 31 | ], 32 | 'ignoreUri' => [ 33 | 'regex:/members:PUT' 34 | ] 35 | ]; 36 | 37 | $config = new class{}; 38 | $config->jwtAuth = $this->config; 39 | 40 | // let's setup the DI config here 41 | $this->app['config'] = function() use($config) { return $config; }; 42 | 43 | $this->middleware = new AuthMicro($this->app); 44 | 45 | $app = $this->app; 46 | 47 | $this->app->get('/', function() use($app) { 48 | $response = $app["response"]; 49 | $response->setStatusCode(200); 50 | $response->setContentType("application/json"); 51 | $response->setContent(json_encode(['index get'])); 52 | $response->send(); 53 | }); 54 | 55 | $this->app->get('/members', function() use($app) { 56 | $response = $app["response"]; 57 | $response->setStatusCode(200); 58 | $response->setContentType("application/json"); 59 | $response->setContent(json_encode(['members get'])); 60 | $response->send(); 61 | }); 62 | 63 | $this->app->post('/members', function() use($app) { 64 | $response = $app["response"]; 65 | $response->setStatusCode(200); 66 | $response->setContentType("application/json"); 67 | $response->setContent(json_encode(['members post'])); 68 | $response->send(); 69 | }); 70 | 71 | $this->app->put('/members', function() use($app) { 72 | $response = $app["response"]; 73 | $response->setStatusCode(200); 74 | $response->setContentType("application/json"); 75 | $response->setContent(json_encode(['members put'])); 76 | $response->send(); 77 | }); 78 | 79 | $this->app->options('/members', function() use($app) { 80 | $response = $app["response"]; 81 | $response->setStatusCode(204); 82 | $response->setContentType("application/json"); 83 | $response->setContent(json_encode(['members option'])); 84 | $response->send(); 85 | }); 86 | 87 | } 88 | 89 | public function testLookForTokenFail() 90 | { 91 | // override for testing 92 | $_SERVER['REQUEST_URI'] = '/members'; 93 | 94 | // call this on test methods instead 95 | $this->app->handle('/members'); 96 | 97 | $this->assertEquals(401, $this->app['response']->getStatusCode()); 98 | $this->assertEquals('["missing token"]', $this->app['response']->getContent()); 99 | } 100 | 101 | public function testIgnoreOptionMethod() 102 | { 103 | // override for testing 104 | $_SERVER['REQUEST_URI'] = '/members'; 105 | $_SERVER["REQUEST_METHOD"] = "OPTIONS"; 106 | 107 | $this->middleware->setIgnoreOptionsMethod(); 108 | 109 | // call this on test methods instead 110 | $this->app->handle('/members'); 111 | 112 | $this->assertEquals(204, $this->app['response']->getStatusCode()); 113 | } 114 | 115 | public function testIgnoreUri() 116 | { 117 | $_SERVER['REQUEST_URI'] = '/members'; 118 | $_SERVER["REQUEST_METHOD"] = "PUT"; 119 | 120 | // call this on test methods instead 121 | $this->app->handle('/members'); 122 | 123 | $this->assertEquals(200, $this->app['response']->getStatusCode()); 124 | $this->assertEquals('["members put"]', $this->app['response']->getContent()); 125 | } 126 | 127 | public function testIgnoreUriWithToken() 128 | { 129 | $_SERVER['REQUEST_URI'] = '/members'; 130 | $_SERVER["REQUEST_METHOD"] = "PUT"; 131 | 132 | $payload = $this->config['payload']; 133 | 134 | $jwt = JWT::encode($payload, $this->config['secretKey']); 135 | 136 | $_GET['_token'] = $jwt; 137 | 138 | // call this on test methods instead 139 | $this->app->handle('/members'); 140 | 141 | $this->assertEquals(200, $this->app['response']->getStatusCode()); 142 | $this->assertEquals('["members put"]', $this->app['response']->getContent()); 143 | $this->assertEquals($payload['sub'], $this->app['auth']->id()); 144 | } 145 | 146 | public function testPassedExpiredToken() 147 | { 148 | $_SERVER['REQUEST_URI'] = '/members'; 149 | $_SERVER["REQUEST_METHOD"] = "POST"; 150 | 151 | $payload = $this->config['payload']; 152 | // let's expired the token 153 | $payload['exp'] = -20; 154 | $jwt = JWT::encode($payload, $this->config['secretKey']); 155 | 156 | $_GET['_token'] = $jwt; 157 | 158 | // call this on test methods instead 159 | $this->app->handle('/members'); 160 | 161 | $this->assertEquals(401, $this->app['response']->getStatusCode()); 162 | $this->assertEquals('["Expired token"]', $this->app['response']->getContent()); 163 | } 164 | 165 | public function testPasssedValidToken() 166 | { 167 | $_SERVER['REQUEST_URI'] = '/members'; 168 | $_SERVER["REQUEST_METHOD"] = "POST"; 169 | 170 | $payload = $this->config['payload']; 171 | // let's expired the token 172 | $jwt = JWT::encode($payload, $this->config['secretKey']); 173 | 174 | $_GET['_token'] = $jwt; 175 | 176 | // call this on test methods instead 177 | $this->app->handle('/members'); 178 | 179 | $this->assertEquals(200, $this->app['response']->getStatusCode()); 180 | $this->assertEquals('["members post"]', $this->app['response']->getContent()); 181 | 182 | // make sure data is correct 183 | $this->assertEquals($payload, $this->app['auth']->data()); 184 | } 185 | 186 | } -------------------------------------------------------------------------------- /tests/Phalcon/TokenGetterHeaderTest.php: -------------------------------------------------------------------------------- 1 | createMock(RequestInterface::class); 14 | $response->method('getHeader')->willReturn('Bearer '.$token); 15 | 16 | $header = new Header($response); 17 | $this->assertEquals($token, $header->parse()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Phalcon/TokenGetterQueryStrTest.php: -------------------------------------------------------------------------------- 1 | createMock(RequestInterface::class); 14 | $response->method('getQuery')->willReturn($token); 15 | 16 | $query = new QueryStr($response); 17 | $this->assertEquals($token, $query->parse()); 18 | } 19 | } -------------------------------------------------------------------------------- /tests/Phalcon/TokenGetterTest.php: -------------------------------------------------------------------------------- 1 | createMock(RequestInterface::class); 16 | $response->method('getHeader')->willReturn('Bearer '.$token); 17 | 18 | $header = new Header($response); 19 | 20 | $tokenGetter = new TokenGetter($header); 21 | $this->assertEquals($token, $tokenGetter->parse()); 22 | } 23 | 24 | public function testParserMulti() 25 | { 26 | $token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ'; 27 | 28 | $response = $this->createMock(RequestInterface::class); 29 | $response->method('getHeader')->willReturn(''); // return empty on first attemp 30 | $response->method('getQuery')->willReturn($token); 31 | 32 | $header = new Header($response); 33 | $query = new QueryStr($response); 34 | 35 | $tokenGetter = new TokenGetter($header, $query); 36 | $this->assertEquals($token, $tokenGetter->parse()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |