ADmad / cakephp-jwt-auth
1
<?php
2
declare(strict_types=1);
3

4
namespace ADmad\JwtAuth\Auth;
5

6
use Cake\Auth\BaseAuthenticate;
7
use Cake\Controller\ComponentRegistry;
8
use Cake\Core\Configure;
9
use Cake\Http\Exception\UnauthorizedException;
10
use Cake\Http\Response;
11
use Cake\Http\ServerRequest;
12
use Cake\Utility\Security;
13
use Exception;
14
use Firebase\JWT\JWT;
15

16
/**
17
 * An authentication adapter for authenticating using JSON Web Tokens.
18
 *
19
 * ```
20
 *  $this->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 <token>". 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 2
    public function __construct(ComponentRegistry $registry, array $config)
92
    {
93
        $defaultConfig = [
94 2
            '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 2
        $this->setConfig($defaultConfig);
105

106 2
        if (empty($config['allowedAlgs'])) {
107 2
            $config['allowedAlgs'] = ['HS256'];
108
        }
109

110 2
        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 2
    public function authenticate(ServerRequest $request, Response $response)
121
    {
122 2
        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 2
    public function getUser(ServerRequest $request)
132
    {
133 2
        $payload = $this->getPayload($request);
134

135 2
        if (empty($payload)) {
136 2
            return false;
137
        }
138

139 2
        if (!$this->_config['queryDatasource']) {
140 2
            return json_decode(json_encode($payload), true);
141
        }
142

143 2
        if (!isset($payload->sub)) {
144 2
            return false;
145
        }
146

147 2
        $user = $this->_findUser((string)$payload->sub);
148 2
        if (!$user) {
149 0
            return false;
150
        }
151

152 2
        unset($user[$this->_config['fields']['password']]);
153

154 2
        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 2
    public function getPayload(?ServerRequest $request = null)
164
    {
165 2
        if (!$request) {
166 0
            return $this->_payload;
167
        }
168

169 2
        $payload = null;
170

171 2
        $token = $this->getToken($request);
172 2
        if ($token) {
173 2
            $payload = $this->_decode($token);
174
        }
175

176 2
        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 2
    public function getToken(?ServerRequest $request = null)
186
    {
187 2
        $config = $this->_config;
188

189 2
        if ($request === null) {
190 0
            return $this->_token;
191
        }
192

193 2
        $header = $request->getHeaderLine($config['header']);
194 2
        if ($header && stripos($header, $config['prefix']) === 0) {
195 2
            return $this->_token = str_ireplace($config['prefix'] . ' ', '', $header);
196
        }
197

198 2
        if (!empty($this->_config['cookie'])) {
199 2
            $token = $request->getCookie($this->_config['cookie']);
200 2
            if ($token !== null) {
201
                /** @psalm-suppress PossiblyInvalidCast */
202 2
                $token = (string)$token;
203
            }
204

205 2
            return $this->_token = $token;
206
        }
207

208 2
        if (!empty($this->_config['parameter'])) {
209 2
            $token = $request->getQuery($this->_config['parameter']);
210 2
            if ($token !== null) {
211
                /** @psalm-suppress PossiblyInvalidCast */
212 2
                $token = (string)$token;
213
            }
214

215 2
            return $this->_token = $token;
216
        }
217

218 2
        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 2
    protected function _decode(string $token)
228
    {
229 2
        $config = $this->_config;
230
        try {
231 2
            $payload = JWT::decode(
232
                $token,
233 2
                $config['key'] ?: Security::getSalt(),
234 2
                $config['allowedAlgs']
235
            );
236

237 2
            return $payload;
238 2
        } catch (Exception $e) {
239 2
            if (Configure::read('debug')) {
240 2
                throw $e;
241
            }
242 2
            $this->_error = $e;
243
        }
244

245 2
        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 2
    public function unauthenticated(ServerRequest $request, Response $response)
260
    {
261 2
        if (!$this->_config['unauthenticatedException']) {
262 2
            return;
263
        }
264

265 2
        $message = $this->_error
266 0
            ? $this->_error->getMessage()
267 2
            : $this->_registry->get('Auth')->getConfig('authError');
268

269
        /** @var \Throwable $exception */
270 2
        $exception = new $this->_config['unauthenticatedException']($message);
271 2
        throw $exception;
272
    }
273
}

Read our documentation on viewing source code .

Loading