├── phpstan.neon ├── psalm.xml ├── LICENSE.txt ├── phpunit.xml.dist ├── composer.json ├── README.md └── src └── Auth └── JwtAuthenticate.php /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | checkMissingIterableValueType: false 4 | paths: 5 | - src/ 6 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-Present ADmad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ./tests/TestCase 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ./src/ 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admad/cakephp-jwt-auth", 3 | "type": "cakephp-plugin", 4 | "description": "CakePHP plugin for authenticating using JSON Web Tokens", 5 | "keywords": [ 6 | "cakephp", 7 | "authenticate", 8 | "authentication", 9 | "jwt" 10 | ], 11 | "homepage": "http://github.com/ADmad/cakephp-jwt-auth", 12 | "authors": [ 13 | { 14 | "name":"ADmad", 15 | "role":"Author", 16 | "homepage":"https://github.com/ADmad" 17 | } 18 | ], 19 | "license": "MIT", 20 | "support": { 21 | "source":"https://github.com/ADmad/cakephp-jwt-auth", 22 | "issues":"https://github.com/ADmad/cakephp-jwt-auth/issues" 23 | }, 24 | "require": { 25 | "cakephp/cakephp": "^4.0", 26 | "firebase/php-jwt": "^5.0" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "~8.5.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "ADmad\\JwtAuth\\": "src" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "ADmad\\JwtAuth\\Test\\": "tests" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakePHP JWT Authenticate plugin 2 | 3 | [![Build Status](https://img.shields.io/github/workflow/status/ADmad/cakephp-jwt-auth/CI/master?style=flat-square)](https://github.com/ADmad/cakephp-jwt-auth/actions?query=workflow%3ACI+branch%3Amaster) 4 | [![Coverage Status](https://img.shields.io/codecov/c/github/ADmad/cakephp-jwt-auth.svg?style=flat-square)](https://codecov.io/github/ADmad/cakephp-jwt-auth) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/ADmad/cakephp-jwt-auth.svg?style=flat-square)](https://packagist.org/packages/ADmad/cakephp-jwt-auth) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) 7 | 8 | Plugin containing AuthComponent's authenticate class for authenticating using 9 | [JSON Web Tokens](http://jwt.io/). You can read about JSON Web Token 10 | specification in detail [here](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-27). 11 | 12 | ## Installation 13 | 14 | ```sh 15 | composer require admad/cakephp-jwt-auth 16 | ``` 17 | 18 | ## Usage 19 | 20 | Load the plugin using Cake's console: 21 | 22 | ```sh 23 | ./bin/cake plugin load ADmad/JwtAuth 24 | ``` 25 | 26 | ## Configuration: 27 | 28 | Setup `AuthComponent`: 29 | 30 | ```php 31 | // In your controller, for e.g. src/Api/AppController.php 32 | public function initialize(): void 33 | { 34 | parent::initialize(); 35 | 36 | $this->loadComponent('Auth', [ 37 | 'storage' => 'Memory', 38 | 'authenticate' => [ 39 | 'ADmad/JwtAuth.Jwt' => [ 40 | 'userModel' => 'Users', 41 | 'fields' => [ 42 | 'username' => 'id' 43 | ], 44 | 45 | 'parameter' => 'token', 46 | 47 | // Boolean indicating whether the "sub" claim of JWT payload 48 | // should be used to query the Users model and get user info. 49 | // If set to `false` JWT's payload is directly returned. 50 | 'queryDatasource' => true, 51 | ] 52 | ], 53 | 54 | 'unauthorizedRedirect' => false, 55 | 'checkAuthIn' => 'Controller.initialize', 56 | 57 | // If you don't have a login action in your application, set 58 | // 'loginAction' to empty string to prevent getting a MissingRouteException. 59 | 'loginAction' => '', 60 | ]); 61 | } 62 | ``` 63 | 64 | ## Working 65 | 66 | The authentication class checks for the token in two locations: 67 | 68 | - `HTTP_AUTHORIZATION` environment variable: 69 | 70 | It first checks if token is passed using `Authorization` request header. 71 | The value should be of form `Bearer `. The `Authorization` header name 72 | and token prefix `Bearer` can be customized using options `header` and `prefix` 73 | respectively. 74 | 75 | - The query string variable specified using `parameter` config: 76 | 77 | Next it checks if the token is present in query string. The default variable 78 | name is `token` and can be customzied by using the `parameter` config shown 79 | above. 80 | 81 | ### Known Issue 82 | Some servers don't populate `$_SERVER['HTTP_AUTHORIZATION']` when 83 | `Authorization` header is set. So it's up to you to ensure that either 84 | `$_SERVER['HTTP_AUTHORIZATION']` or `$_ENV['HTTP_AUTHORIZATION']` is set. 85 | 86 | For e.g. for apache you could use the following: 87 | 88 | ``` 89 | RewriteEngine On 90 | RewriteCond %{HTTP:Authorization} ^(.*) 91 | RewriteRule .* - [e=HTTP_AUTHORIZATION:%1] 92 | ``` 93 | 94 | or 95 | 96 | ``` 97 | SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 98 | ``` 99 | 100 | ## Token Generation 101 | 102 | You can use `\Firebase\JWT\JWT::encode()` of the [firebase/php-jwt](https://github.com/firebase/php-jwt) 103 | lib, which this plugin depends on, to generate tokens. 104 | 105 | **The payload must have the "sub" (subject) claim whose value is used to query the 106 | Users model and find record matching the "id" field.** 107 | 108 | Ideally you should also specify the token expiry time using `exp` claim. 109 | 110 | You can set the `queryDatasource` option to `false` to directly return the token's 111 | payload as user info without querying datasource for matching user record. 112 | 113 | ## Further reading 114 | 115 | For an end to end usage example check out [this](http://www.bravo-kernel.com/2015/04/how-to-add-jwt-authentication-to-a-cakephp-3-rest-api/) blog post by Bravo Kernel. 116 | -------------------------------------------------------------------------------- /src/Auth/JwtAuthenticate.php: -------------------------------------------------------------------------------- 1 | Auth->config('authenticate', [ 21 | * 'ADmad/JwtAuth.Jwt' => [ 22 | * 'parameter' => 'token', 23 | * 'userModel' => 'Users', 24 | * 'fields' => [ 25 | * 'username' => 'id' 26 | * ], 27 | * ] 28 | * ]); 29 | * ``` 30 | * 31 | * @copyright 2015-Present ADmad 32 | * @license MIT 33 | * @see http://jwt.io 34 | * @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token 35 | */ 36 | class JwtAuthenticate extends BaseAuthenticate 37 | { 38 | /** 39 | * Parsed token. 40 | * 41 | * @var string|null 42 | */ 43 | protected $_token; 44 | 45 | /** 46 | * Payload data. 47 | * 48 | * @var object|null 49 | */ 50 | protected $_payload; 51 | 52 | /** 53 | * Exception. 54 | * 55 | * @var \Throwable|null 56 | */ 57 | protected $_error; 58 | 59 | /** 60 | * Constructor. 61 | * 62 | * Settings for this object. 63 | * 64 | * - `cookie` - Cookie name to check. Defaults to `false`. 65 | * - `header` - Header name to check. Defaults to `'authorization'`. 66 | * - `prefix` - Token prefix. Defaults to `'bearer'`. 67 | * - `parameter` - The url parameter name of the token. Defaults to `token`. 68 | * First $_SERVER['HTTP_AUTHORIZATION'] is checked for token value. 69 | * Its value should be of form "Bearer ". If empty this query string 70 | * paramater is checked. 71 | * - `allowedAlgs` - List of supported verification algorithms. 72 | * Defaults to ['HS256']. See API of JWT::decode() for more info. 73 | * - `queryDatasource` - Boolean indicating whether the `sub` claim of JWT 74 | * token should be used to query the user model and get user record. If 75 | * set to `false` JWT's payload is directly retured. Defaults to `true`. 76 | * - `userModel` - The model name of users, defaults to `Users`. 77 | * - `fields` - Key `username` denotes the identifier field for fetching user 78 | * record. The `sub` claim of JWT must contain identifier value. 79 | * Defaults to ['username' => 'id']. 80 | * - `finder` - Finder method. 81 | * - `unauthenticatedException` - Fully namespaced exception name. Exception to 82 | * throw if authentication fails. Set to false to do nothing. 83 | * Defaults to '\Cake\Http\Exception\UnauthorizedException'. 84 | * - `key` - The key, or map of keys used to decode JWT. If not set, value 85 | * of Security::salt() will be used. 86 | * 87 | * @param \Cake\Controller\ComponentRegistry $registry The Component registry 88 | * used on this request. 89 | * @param array $config Array of config to use. 90 | */ 91 | public function __construct(ComponentRegistry $registry, array $config) 92 | { 93 | $defaultConfig = [ 94 | 'cookie' => false, 95 | 'header' => 'authorization', 96 | 'prefix' => 'bearer', 97 | 'parameter' => 'token', 98 | 'queryDatasource' => true, 99 | 'fields' => ['username' => 'id'], 100 | 'unauthenticatedException' => UnauthorizedException::class, 101 | 'key' => null, 102 | ]; 103 | 104 | $this->setConfig($defaultConfig); 105 | 106 | if (empty($config['allowedAlgs'])) { 107 | $config['allowedAlgs'] = ['HS256']; 108 | } 109 | 110 | parent::__construct($registry, $config); 111 | } 112 | 113 | /** 114 | * Get user record based on info available in JWT. 115 | * 116 | * @param \Cake\Http\ServerRequest $request The request object. 117 | * @param \Cake\Http\Response $response Response object. 118 | * @return false|array User record array or false on failure. 119 | */ 120 | public function authenticate(ServerRequest $request, Response $response) 121 | { 122 | return $this->getUser($request); 123 | } 124 | 125 | /** 126 | * Get user record based on info available in JWT. 127 | * 128 | * @param \Cake\Http\ServerRequest $request Request object. 129 | * @return false|array User record array or false on failure. 130 | */ 131 | public function getUser(ServerRequest $request) 132 | { 133 | $payload = $this->getPayload($request); 134 | 135 | if (empty($payload)) { 136 | return false; 137 | } 138 | 139 | if (!$this->_config['queryDatasource']) { 140 | return json_decode(json_encode($payload), true); 141 | } 142 | 143 | if (!isset($payload->sub)) { 144 | return false; 145 | } 146 | 147 | $user = $this->_findUser((string)$payload->sub); 148 | if (!$user) { 149 | return false; 150 | } 151 | 152 | unset($user[$this->_config['fields']['password']]); 153 | 154 | return $user; 155 | } 156 | 157 | /** 158 | * Get payload data. 159 | * 160 | * @param \Cake\Http\ServerRequest|null $request Request instance or null 161 | * @return object|null Payload object on success, null on failurec 162 | */ 163 | public function getPayload(?ServerRequest $request = null) 164 | { 165 | if (!$request) { 166 | return $this->_payload; 167 | } 168 | 169 | $payload = null; 170 | 171 | $token = $this->getToken($request); 172 | if ($token) { 173 | $payload = $this->_decode($token); 174 | } 175 | 176 | return $this->_payload = $payload; 177 | } 178 | 179 | /** 180 | * Get token from header or query string. 181 | * 182 | * @param \Cake\Http\ServerRequest|null $request Request object. 183 | * @return string|null Token string if found else null. 184 | */ 185 | public function getToken(?ServerRequest $request = null) 186 | { 187 | $config = $this->_config; 188 | 189 | if ($request === null) { 190 | return $this->_token; 191 | } 192 | 193 | $header = $request->getHeaderLine($config['header']); 194 | if ($header && stripos($header, $config['prefix']) === 0) { 195 | return $this->_token = str_ireplace($config['prefix'] . ' ', '', $header); 196 | } 197 | 198 | if (!empty($this->_config['cookie'])) { 199 | $token = $request->getCookie($this->_config['cookie']); 200 | if ($token !== null) { 201 | /** @psalm-suppress PossiblyInvalidCast */ 202 | $token = (string)$token; 203 | } 204 | 205 | return $this->_token = $token; 206 | } 207 | 208 | if (!empty($this->_config['parameter'])) { 209 | $token = $request->getQuery($this->_config['parameter']); 210 | if ($token !== null) { 211 | /** @psalm-suppress PossiblyInvalidCast */ 212 | $token = (string)$token; 213 | } 214 | 215 | return $this->_token = $token; 216 | } 217 | 218 | return $this->_token; 219 | } 220 | 221 | /** 222 | * Decode JWT token. 223 | * 224 | * @param string $token JWT token to decode. 225 | * @return object|null The JWT's payload as a PHP object, null on failure. 226 | */ 227 | protected function _decode(string $token) 228 | { 229 | $config = $this->_config; 230 | try { 231 | $payload = JWT::decode( 232 | $token, 233 | $config['key'] ?: Security::getSalt(), 234 | $config['allowedAlgs'] 235 | ); 236 | 237 | return $payload; 238 | } catch (Exception $e) { 239 | if (Configure::read('debug')) { 240 | throw $e; 241 | } 242 | $this->_error = $e; 243 | } 244 | 245 | return null; 246 | } 247 | 248 | /** 249 | * Handles an unauthenticated access attempt. Depending on value of config 250 | * `unauthenticatedException` either throws the specified exception or returns 251 | * null. 252 | * 253 | * @param \Cake\Http\ServerRequest $request A request object. 254 | * @param \Cake\Http\Response $response A response object. 255 | * @throws \Cake\Http\Exception\UnauthorizedException Or any other 256 | * configured exception. 257 | * @return void 258 | */ 259 | public function unauthenticated(ServerRequest $request, Response $response) 260 | { 261 | if (!$this->_config['unauthenticatedException']) { 262 | return; 263 | } 264 | 265 | $message = $this->_error 266 | ? $this->_error->getMessage() 267 | : $this->_registry->get('Auth')->getConfig('authError'); 268 | 269 | /** @var \Throwable $exception */ 270 | $exception = new $this->_config['unauthenticatedException']($message); 271 | throw $exception; 272 | } 273 | } 274 | --------------------------------------------------------------------------------