├── 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 | [](https://github.com/ADmad/cakephp-jwt-auth/actions?query=workflow%3ACI+branch%3Amaster)
4 | [](https://codecov.io/github/ADmad/cakephp-jwt-auth)
5 | [](https://packagist.org/packages/ADmad/cakephp-jwt-auth)
6 | [](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 |
--------------------------------------------------------------------------------